Upgrade to 4.0.8 with mod of ExternalAuth + absolute paths to ticket-menu.
authorMikal Kolbein Gule <m.k.gule@usit.uio.no>
Wed, 2 Jan 2013 10:14:35 +0000 (11:14 +0100)
committerMikal Kolbein Gule <m.k.gule@usit.uio.no>
Mon, 7 Jan 2013 09:50:26 +0000 (10:50 +0100)
110 files changed:
bin/rt
docs/UPGRADING-2.0
docs/UPGRADING-3.0
docs/UPGRADING-3.2
docs/UPGRADING-3.4
docs/UPGRADING-3.6
docs/UPGRADING-3.8
docs/UPGRADING-4.0
docs/UPGRADING.mysql
docs/web_deployment.pod
etc/RT_Config.pm
etc/initialdata
etc/schema.SQLite
lib/RT/Action/CreateTickets.pm
lib/RT/Action/SendEmail.pm
lib/RT/Approval/Rule/Passed.pm
lib/RT/Article.pm
lib/RT/Articles.pm
lib/RT/Attachment.pm
lib/RT/Config.pm
lib/RT/Crypt/GnuPG.pm
lib/RT/Dashboard.pm
lib/RT/Generated.pm
lib/RT/Handle.pm
lib/RT/I18N.pm
lib/RT/Interface/Email.pm
lib/RT/Interface/Email/Auth/GnuPG.pm
lib/RT/Interface/Web.pm
lib/RT/Interface/Web/Menu.pm
lib/RT/Pod/HTML.pm [new file with mode: 0644]
lib/RT/Pod/HTMLBatch.pm [new file with mode: 0644]
lib/RT/Pod/Search.pm [new file with mode: 0644]
lib/RT/Queue.pm
lib/RT/Record.pm
lib/RT/Scrip.pm
lib/RT/Scrips.pm
lib/RT/Search/Googleish.pm
lib/RT/SearchBuilder.pm
lib/RT/Shredder.pm
lib/RT/Template.pm
lib/RT/Test.pm
lib/RT/Ticket.pm
lib/RT/Tickets.pm
lib/RT/URI.pm
lib/RT/User.pm
local/html/Callbacks/UiOCallbacks/Elements/Tabs/Privileged
local/plugins/RT-Authen-ExternalAuth/html/Callbacks/ExternalAuth/autohandler/Session
sbin/rt-clean-sessions
sbin/rt-email-dashboards
sbin/rt-fulltext-indexer
sbin/rt-server
sbin/rt-server.fcgi
sbin/rt-shredder
sbin/rt-test-dependencies
sbin/rt-validate-aliases [new file with mode: 0755]
sbin/standalone_httpd
share/html/Admin/Groups/Modify.html
share/html/Admin/Queues/Modify.html
share/html/Admin/Users/GnuPG.html
share/html/Approvals/Elements/PendingMyApproval
share/html/Approvals/autohandler
share/html/Dashboards/Subscription.html
share/html/Elements/CSRF
share/html/Elements/ColumnMap
share/html/Elements/EditCustomField
share/html/Elements/GnuPG/SignEncryptWidget
share/html/Elements/Header
share/html/Elements/HeaderJavascript
share/html/Elements/ListActions
share/html/Elements/Login
share/html/Elements/LoginRedirectWarning [new file with mode: 0644]
share/html/Elements/MessageBox
share/html/Elements/QueueSummaryByStatus
share/html/Elements/RT__CustomField/ColumnMap
share/html/Elements/SelectWatcherType
share/html/Elements/Tabs
share/html/Helpers/Autocomplete/Users
share/html/NoAuth/css/aileron/boxes.css
share/html/NoAuth/css/aileron/ticket.css
share/html/NoAuth/css/ballard/boxes.css
share/html/NoAuth/css/ballard/layout.css
share/html/NoAuth/css/ballard/nav.css
share/html/NoAuth/css/ballard/ticket-search.css
share/html/NoAuth/css/ballard/ticket.css
share/html/NoAuth/css/base/forms.css
share/html/NoAuth/css/base/jquery-ui-timepicker-addon.css [new file with mode: 0644]
share/html/NoAuth/css/base/jquery-ui.css
share/html/NoAuth/css/base/jquery-ui.custom.modified.css
share/html/NoAuth/css/base/login.css
share/html/NoAuth/css/base/main.css
share/html/NoAuth/css/base/superfish-navbar.css
share/html/NoAuth/css/base/superfish.css
share/html/NoAuth/css/base/ticket-form.css
share/html/NoAuth/css/web2/nav.css
share/html/NoAuth/iCal/dhandler
share/html/NoAuth/js/jquery-ui-1.8.4.custom.min.js
share/html/NoAuth/js/jquery-ui-patch-datepicker.js
share/html/NoAuth/js/jquery-ui-timepicker-addon.js [new file with mode: 0644]
share/html/NoAuth/js/util.js
share/html/Prefs/Other.html
share/html/REST/1.0/Forms/ticket/default
share/html/Search/Chart.html
share/html/Search/Elements/SelectPersonType
share/html/Search/Results.html
share/html/Ticket/Attachment/dhandler
share/html/Ticket/Elements/ShowMembers
share/html/Ticket/Elements/ShowMessageHeaders
share/html/Ticket/Elements/ShowTransactionAttachments
share/html/m/_elements/raw_style
share/html/m/_elements/wrapper

diff --git a/bin/rt b/bin/rt
index 0a1737f..bc2fde0 100755 (executable)
--- a/bin/rt
+++ b/bin/rt
@@ -420,7 +420,7 @@ sub show {
         }
         elsif (my $spec = is_object_spec($_, $type)) {
             push @objects, $spec;
-            $rawprint = 1 if $_ =~ /\/content$/ or $_ !~ /^ticket/;
+            $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
         }
         else {
             my $datum = /^-/ ? "option" : "argument";
index a935552..792276f 100644 (file)
@@ -1,7 +1,7 @@
-UPGRADING FROM 2.x:
+=head1 UPGRADING FROM 2.x
 
-The core RT distribution does not contain the tool to upgrade RT from
-version 2.0; the tool, can be downloaded from CPAN at
+The core RT distribution does not contain the tool to upgrade RT from version
+2.0; the tool, can be downloaded from CPAN at
 http://search.cpan.org/dist/RT-Extension-RT2toRT3/
 
 Further instructions may be found in that distribution's README file.
index 625ca4b..1bc1b55 100644 (file)
@@ -1,18 +1,20 @@
-UPGRADING FROM 3.0.x - Changes:
+=head1 UPGRADING FROM 3.0.0 AND EARLIER
 
-= Installation =
+=head2 Installation
 
 We recommend you move your existing /opt/rt3 tree completely out
 of the way before installing the new version of RT, to make sure
 that you don't inadvertently leave old files hanging around.
 
-= Rights changes =
+
+=head2 Rights changes
 
 Now, if you want RT to automatically create new users upon ticket
 submission, you MUST grant 'Everyone' the right to create tickets.
 Granting this right only to "Unprivileged Users" is now insufficient.
 
-= Web server configuration
+
+=head2 Web server configuration
 
 The configuration for RT's web interface has changed.  Please refer to
 docs/web_deployment.pod for instructions.
index c0b8ceb..4641209 100644 (file)
@@ -1,11 +1,10 @@
-UPGRADING FROM 3.2 and earlier - Changes:
+=head1 UPGRADING FROM 3.2.0 AND EARLIER
 
-= Rights changes =
+There have been a number of rights changes.  Now, if you want any user to be
+able to access the Admin tools (a.k.a.  the Configuration tab), you must grant
+that user the "ShowConfigTab" right.  Making the user a privileged user is no
+longer sufficient.
 
-Now, if you want any user to be able to access the Admin tools (a.k.a.
-the Configuration tab), you must grant that user the "ShowConfigTab"
-right.  Making the user a privileged user is no longer sufficient.
-
-"SuperUser" users are no longer automatically added to the list of users
-who can own tickets in a queue. You now need to explicitly give them the
+"SuperUser" users are no longer automatically added to the list of users who
+can own tickets in a queue. You now need to explicitly give them the
 "OwnTicket" right.
index 4dca045..89454bd 100644 (file)
@@ -1,12 +1,11 @@
-UPGRADING FROM 3.3.14 and earlier - Changes:
+=head1 UPGRADING FROM 3.3.14 AND EARLIER
 
 The "ModifyObjectCustomFieldValues" right name was too long. It has been
 changed to "ModifyCustomField"
 
 
-UPGRADING FROM 3.3.11 and earlier - Changes:
+=head1 UPGRADING FROM 3.3.11 AND EARLIER
 
-Custom Fields now have an additional right, "ModifyCustomField".  This
-right governs whether a user can modify an object's custom field values
-for a particular custom field. This includes adding, deleting and
-changing values.
+Custom Fields now have an additional right, "ModifyCustomField".  This right
+governs whether a user can modify an object's custom field values for a
+particular custom field. This includes adding, deleting and changing values.
index 3c27709..da656c9 100644 (file)
@@ -1,29 +1,27 @@
-UPGRADING FROM 3.6.X and earlier - Changes:
+=head1 UPGRADING FROM 3.6.0 AND EARLIER
 
-As there are a large number of code changes, it is highly recommended
-that you install RT into a fresh directory, and then reinstall your
-customizations.
+As there are a large number of code changes, it is highly recommended that you
+install RT into a fresh directory, and then reinstall your customizations.
 
-The database schema has changed significantly for mysql 4.1 and above;
-please read UPGRADING.mysql for more details.
+The database schema has changed significantly for mysql 4.1 and above; please
+read UPGRADING.mysql for more details.
 
-The configuration format has been made stricter. All options MUST be set
-using the Set function; the historical "@XXX = (...) unless @XXX;" is no
-longer allowed.
+The configuration format has been made stricter. All options MUST be set using
+the Set function; the historical "@XXX = (...) unless @XXX;" is no longer
+allowed.
 
 The RTx::Shredder extension has been integrated into core, and several
 features have been added, so you MUST uninstall it before upgrading.
 
-A new interface for making links in text clickable, and doing other
-arbitrary text replacements, has been integrated into RT.  You can read
-more in `perldoc docs/extending/clickable_links.pod`.
+A new interface for making links in text clickable, and doing other arbitrary
+text replacements, has been integrated into RT.  You can read more in `perldoc
+docs/extending/clickable_links.pod`.
 
-A new feature has been added that allows users to forward
-messages. There is a new option in the config ($ForwardFromUser), new
-rights, and a new template.
+A new feature has been added that allows users to forward messages. There is a
+new option in the config ($ForwardFromUser), new rights, and a new template.
 
-New global templates have been added with "Error: " prefixed to the name
-to make it possible to configure error messages sent to users.
+New global templates have been added with "Error: " prefixed to the name to
+make it possible to configure error messages sent to users.
 
 You can read about the new GnuPG integration in `perldoc
 lib/RT/Crypt/GnuPG.pm`.
@@ -31,19 +29,19 @@ lib/RT/Crypt/GnuPG.pm`.
 New scrip conditions 'On Close' and 'On Reopen' have been added.
 
 
-UPGRADING FROM 3.5.7 and earlier - Changes:
+=head1 UPGRADING FROM 3.5.7 AND EARLIER
 
 Scrips are now prepared and committed in order alphanumerically by
-description.  This means that you can prepend a number (00, 07, 15, 24)
-to the beginning of each scrip's description, and they will run in that
-order.  Depending on your database, the old ordering may have been by
-scrip id number -- if that is the case, simply prepend the scrip id
-number to the beginning of its description.
+description.  This means that you can prepend a number (00, 07, 15, 24) to the
+beginning of each scrip's description, and they will run in that order.
+Depending on your database, the old ordering may have been by scrip id number
+-- if that is the case, simply prepend the scrip id number to the beginning of
+its description.
 
 
-UPGRADING FROM 3.5.1 and earlier - Changes:
+=head1 UPGRADING FROM 3.5.1 AND EARLIER
 
 The default for $RedistributeAutoGeneratedMessages has changed to
 'privileged', to make out-of-the-box installations more resistant to
-mail loops. If you rely on the old default of redistributing to all
-watchers, you'll need to set it explicitly now.
+mail loops. If you rely on the old default of redistributing to all watchers,
+you'll need to set it explicitly now.
index cb53030..cfe01df 100644 (file)
-UPGRADING FROM 3.8.8 and earlier - Changes:
+=head1 UPGRADING FROM 3.8.8 AND EARLIER
 
-Previous versions of RT used a password hashing scheme which was too
-easy to reverse, which could allow attackers with read access to the RT
-database to possibly compromise users' passwords.  Even if RT does no
-password authentication itself, it may still store these weak password
-hashes -- using ExternalAuth does not guarantee that you are not
-vulnerable!  To upgrade stored passwords to a stronger hash, run:
+Previous versions of RT used a password hashing scheme which was too easy to
+reverse, which could allow attackers with read access to the RT database to
+possibly compromise users' passwords.  Even if RT does no password
+authentication itself, it may still store these weak password hashes -- using
+ExternalAuth does not guarantee that you are not vulnerable!  To upgrade
+stored passwords to a stronger hash, run:
 
     perl etc/upgrade/vulnerable-passwords
 
-We have also proved that it's possible to delete a notable set of
-records from Transactions table without losing functionality. To delete
-these records, run the following script:
+We have also proved that it's possible to delete a notable set of records from
+Transactions table without losing functionality. To delete these records, run
+the following script:
 
     perl -I /opt/rt4/local/lib -I /opt/rt4/lib etc/upgrade/shrink_transactions_table.pl
 
-If you chose not to run the shrink_cgm_table.pl script when you upgraded
-to 3.8, you should read more about it below and run it at this point.
+If you chose not to run the shrink_cgm_table.pl script when you upgraded to
+3.8, you should read more about it below and run it at this point.
 
-The default for $MessageBoxWrap is now SOFT and $MessageBoxWidth is now
-unset by default.  This means the message box will expand to fill all
-the available width.  $MessageBoxWrap is also overridable by the user
-now.  These changes accommodate the new default two column layout for
-ticket create and update pages.  You may turn this layout off by setting
-$UseSideBySideLayout to 0.  To retain the original behavior, set
-$MessageBoxWrap to HARD and $MessageBoxWidth to 72.
+The default for $MessageBoxWrap is now SOFT and $MessageBoxWidth is now unset
+by default.  This means the message box will expand to fill all the available
+width.  $MessageBoxWrap is also overridable by the user now.  These changes
+accommodate the new default two column layout for ticket create and update
+pages.  You may turn this layout off by setting $UseSideBySideLayout to 0.  To
+retain the original behavior, set $MessageBoxWrap to HARD and $MessageBoxWidth
+to 72.
 
 
-UPGRADING FROM 3.8.7 and earlier - Changes:
+=head1 UPGRADING FROM 3.8.7 AND EARLIER
 
-RT's ChartFont option has been changed from a string to a hash which
-lets you specify per-language fonts. RT now comes with a better default
-font for charts, too.  You should either update your 'ChartFont' option
-to match the new format, or consider trying the new default.
+RT's ChartFont option has been changed from a string to a hash which lets you
+specify per-language fonts. RT now comes with a better default font for
+charts, too.  You should either update your 'ChartFont' option to match the
+new format, or consider trying the new default.
 
-RT now gives you more precise control over the order in which custom
-fields are displayed.  This change requires some small changes to your
-currently saved custom field orders.  RT will automatically clean up
-your existing custom fields when you run the standard database upgrade
-steps.  After that cleanup, you should make sure that custom fields are
-ordered in a way that you and your users find pleasing.
+RT now gives you more precise control over the order in which custom fields
+are displayed.  This change requires some small changes to your currently
+saved custom field orders.  RT will automatically clean up your existing
+custom fields when you run the standard database upgrade steps.  After that
+cleanup, you should make sure that custom fields are ordered in a way that you
+and your users find pleasing.
 
 
-UPGRADING FROM 3.8.6 and earlier - Changes:
+=head1 UPGRADING FROM 3.8.6 AND EARLIER
 
-For MySQL and Oracle users:
-If you upgraded from a version of RT earlier than 3.7.81, you should
-already have a CachedGroupMembers3 index on your CachedGroupMembers
-table.  If you did a clean install of RT somewhere in the 3.8 release
-series, you most likely don't have this index.  You can add it manually
-with:
+For MySQL and Oracle users: if you upgraded from a version of RT earlier than
+3.7.81, you should already have a CachedGroupMembers3 index on your
+CachedGroupMembers table.  If you did a clean install of RT somewhere in the
+3.8 release series, you most likely don't have this index.  You can add it
+manually with:
 
   CREATE INDEX CachedGroupMembers3 on CachedGroupMembers (MemberId, ImmediateParentId);
 
 
-UPGRADING FROM 3.8.5 and earlier - Changes:
+=head1 UPGRADING FROM 3.8.5 AND EARLIER
 
 You can now forward an entire Ticket history (in addition to specific
-transactions) but this requires a new Template called "Forward Ticket".
-This template will be added as part of the standard database upgrade
-step.
+transactions) but this requires a new Template called "Forward Ticket".  This
+template will be added as part of the standard database upgrade step.
 
-Custom fields with categories can optionally be split out into
-hierarchical custom fields.  If you wish to convert your old
-category-based custom fields, run:
+Custom fields with categories can optionally be split out into hierarchical
+custom fields.  If you wish to convert your old category-based custom fields,
+run:
 
     perl etc/upgrade/split-out-cf-categories
 
-It will prompt you for each custom field with categories that it finds,
-and the name of the custom field to create to store the categories.
+It will prompt you for each custom field with categories that it finds, and
+the name of the custom field to create to store the categories.
 
-If you were using the LocalizedDateTime RT::Date formatter from custom
-code, and passing a DateFormat or TimeFormat argument, you need to
-switch from the strftime methods to the cldr methods; that is,
+If you were using the LocalizedDateTime RT::Date formatter from custom code,
+and passing a DateFormat or TimeFormat argument, you need to switch from the
+strftime methods to the cldr methods; that is,
 'full_date_format' becomes 'date_format_full'.
 
 You may also have done this from your RT_SiteConfig.pm, using:
+
     Set($DateTimeFormat, {
         Format => 'LocalizedDateTime',
         DateFormat => 'medium_date_format',
     );
+
 Which would need to be changed to:
+
     Set($DateTimeFormat, {
         Format => 'LocalizedDateTime',
         DateFormat => 'date_format_medium',
     );
 
 
-UPGRADING FROM 3.8.3 and earlier - Changes:
+=head1 UPGRADING FROM 3.8.3 AND EARLIER
 
 Arguments to the NotifyGroup Scrip Action will be updated as part of the
 standard database upgrade process.
 
 
-UPGRADING FROM 3.8.2 and earlier - Changes:
+=head1 UPGRADING FROM 3.8.2 AND EARLIER
 
 A new scrip condition, 'On Reject', has been added.
 
 
-UPGRADING FROM 3.8.1 and earlier - Changes:
+=head1 UPGRADING FROM 3.8.1 AND EARLIER
 
-When using Oracle, $DatabaseName is now used as SID, so RT can connect
-without environment variables or tnsnames.ora file. Because of this
-change, your RT instance may loose its ability to connect to your DB; to
-resolve this, you will need to update RT's configuration and restart
-your web server.  Example configuration:
+When using Oracle, $DatabaseName is now used as SID, so RT can connect without
+environment variables or tnsnames.ora file. Because of this change, your RT
+instance may loose its ability to connect to your DB; to resolve this, you
+will need to update RT's configuration and restart your web server.  Example
+configuration:
 
     Set($DatabaseType, 'Oracle');
     Set($DatabaseHost, '192.168.0.1');
@@ -121,72 +122,70 @@ If you want a user to be able to access the Approvals tools (a.k.a.  the
 Approvals tab), you must grant that user the "ShowApprovalsTab" right.
 
 
-UPGRADING FROM 3.8.0 and earlier - Changes:
+=head1 UPGRADING FROM 3.8.0 AND EARLIER
 
-The TicketSQL syntax for bookmarked tickets has been changed.
-Specifically, the new phrasing is "id = '__Bookmarked__'", rather than
-the old "__Bookmarks__".  The old form will remain, for backwards
-compatibility.  The standard database upgrade process will only
-automatically change the global 'Bookmarked Tickets' search
+The TicketSQL syntax for bookmarked tickets has been changed.  Specifically,
+the new phrasing is "id = '__Bookmarked__'", rather than the old
+"__Bookmarks__".  The old form will remain, for backwards compatibility.  The
+standard database upgrade process will only automatically change the
+global 'Bookmarked Tickets' search
 
 
-UPGRADING FROM 3.7.85 and earlier - Changes:
+=head1 UPGRADING FROM 3.7.85 AND EARLIER
 
-We have proved that it is possible to delete a large set of records from
-the CachedGroupMembers table without losing functionality; in fact,
-failing to do so may result in occasional problems where RT miscounts
-users, particularly in the chart functionality.  To delete these records
-run the following script:
+We have proved that it is possible to delete a large set of records from the
+CachedGroupMembers table without losing functionality; in fact, failing to do
+so may result in occasional problems where RT miscounts users, particularly in
+the chart functionality.  To delete these records run the following script:
 
     perl -I /opt/rt4/local/lib -I /opt/rt4/lib etc/upgrade/shrink_cgm_table.pl
 
-After you run this, you will have significantly reduced the number of
-records in your CachedGroupMembers table, and may need to tell your
-database to refresh indexes/statistics.  Please consult your DBA for
-specific instructions for your database.
+After you run this, you will have significantly reduced the number of records
+in your CachedGroupMembers table, and may need to tell your database to
+refresh indexes/statistics.  Please consult your DBA for specific instructions
+for your database.
 
 
-UPGRADING FROM 3.7.81 and earlier - Changes:
+=head1 UPGRADING FROM 3.7.81 AND EARLIER
 
-RT::Extension::BrandedQueues has been integrated into core, and the
-handling of subject tags has changed as a consequence.  You will need to
-modify any of your email templates which use the $rtname variable, in
-order to make them respect the per-queue subject tags. To edit your
-templates, log into RT as your administrative user, then click:
+RT::Extension::BrandedQueues has been integrated into core, and the handling
+of subject tags has changed as a consequence.  You will need to modify any of
+your email templates which use the $rtname variable, in order to make them
+respect the per-queue subject tags. To edit your templates, log into RT as
+your administrative user, then click:
 
     Configuration -> Global -> Templates -> Select -> <Some template name>
 
-The only template which ships with RT which needs updating is the
-"Autoreply" template, which includes this line:
+The only template which ships with RT which needs updating is the "Autoreply"
+template, which includes this line:
 
-    "There is no need to reply to this message right now.  Your ticket
-    has been assigned an ID of [{$rtname} #{$Ticket->id()}]."
+    "There is no need to reply to this message right now.  Your ticket has
+    been assigned an ID of [{$rtname} #{$Ticket->id()}]."
 
 Change this line to read:
 
-    "There is no need to reply to this message right now.  Your ticket
-    has been assigned an ID of { $Ticket->SubjectTag }."
+    "There is no need to reply to this message right now.  Your ticket has
+    been assigned an ID of { $Ticket->SubjectTag }."
 
-If you were previously using RT::Extension::BrandedQueues, you MUST
-uninstall it before upgrading. In addition, you must run the
+If you were previously using RT::Extension::BrandedQueues, you MUST uninstall
+it before upgrading. In addition, you must run the
 'etc/upgrade/3.8-branded-queues-extension' perl script.  This will
 convert the extension's configuration into the new format.  Finally, in
 templates where you were using the Tag method ($Ticket->QueueObj->Tag),
 you will need to replace it with $Ticket->SubjectTag
 
-RT::Action::LinearEscalate extension has been integrated into core,
-so you MUST uninstall it before upgrading.
+RT::Action::LinearEscalate extension has been integrated into core, so you
+MUST uninstall it before upgrading.
 
-RT::Extension::iCal has been integrated into core, so you MUST uninstall
-it before upgrading. In addition, you must run etc/upgrade/3.8-ical-extension
+RT::Extension::iCal has been integrated into core, so you MUST uninstall it
+before upgrading. In addition, you must run etc/upgrade/3.8-ical-extension
 script to convert old data.
 
 
-UPGRADING FROM 3.7.80 and earlier - Changes:
+=head1 UPGRADING FROM 3.7.80 AND EARLIER
 
-Added indexes to CachedGroupMembers for MySQL and Oracle.
-If you have previously installed RTx-Shredder, you may already
-have these indexes.  You can see the indexes by looking at
-etc/upgrade/3.7.81/schema.*
+Added indexes to CachedGroupMembers for MySQL and Oracle.  If you have
+previously installed RTx-Shredder, you may already have these indexes.  You
+can see the indexes by looking at etc/upgrade/3.7.81/schema.*
 
 These indexes may take a very long time to create.
index 4b64d2e..ad8d87b 100644 (file)
@@ -1,87 +1,99 @@
-Common Issues
+=head1 UPGRADING FROM BEFORE 4.0.0
 
-RT now defaults to a database name of rt4 and an installation root of /opt/rt4.
+=head2 Common issues
 
-If you are upgrading, you will likely want to specify that your database
-is still named rt3 (or import a backup of your database as rt4 so that
-you can feel more confident making the upgrade).
+RT now defaults to a database name of rt4 and an installation root of
+/opt/rt4.
 
-You really shouldn't install RT4 into your RT3 source tree (/opt/rt3)
-and instead should be using make install to set up a clean environment.
-This will allow you to evaluate your local modifications and configuration
-changes as you migrate to 4.0.
+If you are upgrading, you will likely want to specify that your database is
+still named rt3 (or import a backup of your database as rt4 so that you can
+feel more confident making the upgrade).
+
+You really shouldn't install RT4 into your RT3 source tree (/opt/rt3) and
+instead should be using make install to set up a clean environment.  This will
+allow you to evaluate your local modifications and configuration changes as
+you migrate to 4.0.
 
 If you choose to force RT to install into /opt/rt3, or another existing RT 3.x
 install location, you will encounter issues because we removed the _Overlay
-files (such as Ticket_Overlay.pm) and relocated other files.  You will
-need to manually remove these files after the upgrade or RT will fail.
-After making a complete backup of your /opt/rt3 install, you might use a
-command like the following to remove the _Overlay files:
+files (such as Ticket_Overlay.pm) and relocated other files.  You will need to
+manually remove these files after the upgrade or RT will fail.  After making a
+complete backup of your /opt/rt3 install, you might use a command like the
+following to remove the _Overlay files:
 
     find /opt/rt3/lib/ -type f -name '*_Overlay*' -delete
 
 RT has also changed how web deployment works; you will need to review
-docs/web_deployment.pod for current instructions.  The old
-`fastcgi_server`, `webmux.pl`, and `mason_handler.*` files will not
-work with RT 4.0, and should be removed to reduce confusion.
+docs/web_deployment.pod for current instructions.  The old `fastcgi_server`,
+`webmux.pl`, and `mason_handler.*` files will not work with RT 4.0, and should
+be removed to reduce confusion.
+
+
+=head2 RT_SiteConfig.pm
+
+You will need to carefully review your local settings when moving from 3.8 to
+4.0.
 
-*******
-RT_SiteConfig.pm
+If you were adding your own custom statuses in earlier versions of RT, using
+ActiveStatus or InactiveStatus you will need to port these to use the new
+Lifecycles functionality.  You can read more about it in RT_Config.pm.  In
+most cases, you can do this by extending the default active and inactive
+lists.
 
-You will need to carefully review your local settings when moving from
-3.8 to 4.0.
 
-If you were adding your own custom statuses in earlier versions of RT,
-using ActiveStatus or InactiveStatus you will need to port these to use
-the new Lifecycles functionality.  You can read more about it in
-RT_Config.pm.  In most cases, you can do this by extending the default
-active and inactive lists.
+=head2 Upgrading sessions on MySQL
 
-*******
-Upgrading sessions on MySQL
+In 4.0.0rc2, RT began shipping an updated schema for the sesions table that
+specificies a character set as well as making the table InnoDB.  As part of
+the upgrade process, your sessions table will be dropped and recreated with
+the new schema.
 
-In 4.0.0rc2, RT began shipping an updated schema for the sesions table
-that specificies a character set as well as making the table InnoDB.  As
-part of the upgrade process, your sessions table will be dropped and
-recreated with the new schema.
 
-*******
-UPGRADING FROM RT 3.8.x and RTFM 2.1 or greater
+=head2 Upgrading from installs with RTFM
 
-RT4 now includes an Articles functionality, merged from RTFM.
-You should not install and enable the RT::FM plugin separately on RT 4.
-If you have existing data in RTFM, you can use the etc/upgrade/upgrade-articles
-script to upgrade that data.
+RT4 now includes an Articles functionality, merged from RTFM.  You should not
+install and enable the RT::FM plugin separately on RT 4.  If you have existing
+data in RTFM, you can use the etc/upgrade/upgrade-articles script to upgrade
+that data.
 
-When running normal upgrade scripts, RT will warn if it finds existing
-RTFM tables that contain data and point you to the upgrade-articles script.
+When running normal upgrade scripts, RT will warn if it finds existing RTFM
+tables that contain data and point you to the upgrade-articles script.
 
-This script should be run from your RT tarball.  It will immediately
-begin populating your new RT4 tables with data from RTFM.  If you have
-browsed in the RT4 UI and created new classes and articles, this script
-will fail spectacularly.  Do *not* run this except on a fresh upgrade of
-RT.
+This script should be run from your RT tarball.  It will immediately begin
+populating your new RT4 tables with data from RTFM.  If you have browsed in
+the RT4 UI and created new classes and articles, this script will fail
+spectacularly.  Do *not* run this except on a fresh upgrade of RT.
 
 You can run this as
 
   etc/upgrade/upgrade-articles
 
-It will ouput a lot of data about what it is changing.  You should
-review this for errors.
+It will ouput a lot of data about what it is changing.  You should review this
+for errors.
 
-If you are running RTFM 2.0 with a release of RT, there isn't currently an upgrade
-script that can port RTFM's internal CustomField and Transaction data to RT4.
+If you are running RTFM 2.0 with a release of RT, there isn't currently an
+upgrade script that can port RTFM's internal CustomField and Transaction data
+to RT4.
 
 You must also remove RT::FM from your @Plugins line in RT_SiteConfig.pm.
 
-*******
-The deprecated classes RT::Action::Generic, RT::Condition::Generic and RT::Search::Generic
-have been removed, but you shouldn't have been using them anyway. You should have been using
-RT::Action, RT::Condition and RT::Search, respectively.
 
-* The "Rights Delegation" and "Personal Groups" features have been removed.
+=head2 Removals and updates
+
+The deprecated classes RT::Action::Generic, RT::Condition::Generic and
+RT::Search::Generic have been removed, but you shouldn't have been using them
+anyway. You should have been using RT::Action, RT::Condition and RT::Search,
+respectively.
+
+=over
+
+=item *
+
+The "Rights Delegation" and "Personal Groups" features have been removed.
 
-* Replace the following code in templates:
+=item *
+
+Replace the following code in templates:
 
     [{$Ticket->QueueObj->SubjectTag || $rtname} #{$Ticket->id}]
 
@@ -89,38 +101,45 @@ with
 
     { $Ticket->SubjectTag }
 
-* Unique names are now enforced for user defined groups.  New groups cannot be
-  created with a duplicate name and existing groups cannot be renamed to an
-  in-use name.  The admin interface will warn about existing groups with
-  duplicate names.  Although the groups will still function, some parts of the
-  interface (rights management, subgroup membership) may not work as expected
-  with duplicate names.  Running
+=item *
+
+Unique names are now enforced for user defined groups.  New groups cannot be
+created with a duplicate name and existing groups cannot be renamed to an
+in-use name.  The admin interface will warn about existing groups with
+duplicate names.  Although the groups will still function, some parts of the
+interface (rights management, subgroup membership) may not work as expected
+with duplicate names.  Running
 
     /opt/rt4/sbin/rt-validator --check
 
-  will report duplicate group names, and running it with --resolve will fix
-  duplicates by appending the group id to the name.
+will report duplicate group names, and running it with --resolve will fix
+duplicates by appending the group id to the name.
+
+Nota Bene: As a result of differing indexes in the schema files, Postgres and
+SQLite RT databases have enforced group name uniqueness for many years at the
+database level.
+
+=back
 
-  Nota Bene: As a result of differing indexes in the schema files, Postgres and
-  SQLite RT databases have enforced group name uniqueness for many years at the
-  database level.
 
-*******
 
-UPGRADING FROM 4.0.5 and earlier - Changes:
+=head1 UPGRADING FROM 4.0.5 AND EARLIER
+
+=head2 Schema updates
 
 The fix for an attribute truncation bug on MySQL requires a small ALTER TABLE.
 Be sure you run `make upgrade-database` to apply this change automatically.
 The bug primarily manifested when uploading large logos in the theme editor on
-MySQL.  Refer to etc/upgrade/4.0.6/schema.mysql for the actual ALTER TABLE that
-will be run.
+MySQL.  Refer to etc/upgrade/4.0.6/schema.mysql for the actual ALTER TABLE
+that will be run.
+
+
+=head2 Query Builder
 
-*******
 The web-based query builder now uses Queue limits to restrict the set of
 displayed statuses and owners.  As part of this change, the %cfqueues
-parameter was renamed to %Queues; if you have local modifications to any
-of the following Mason templates, this feature will not function
-correctly:
+parameter was renamed to %Queues; if you have local modifications to any of
+the following Mason templates, this feature will not function correctly:
 
     share/html/Elements/SelectOwner
     share/html/Elements/SelectStatus
index 77a6b38..a62dee7 100644 (file)
-If you did not start by reading the README file, please start there;
-these steps do not list the full upgrading process, merely a part which
-is sometimes necessary.
+If you did not start by reading the README file, please start there; these
+steps do not list the full upgrading process, merely a part which is sometimes
+necessary.
 
 This file applies if either:
 
- 1) You are upgrading RT from a version prior to 3.8.0, on any version
-    of MySQL
-............. OR .............
- 2) You are migrating from MySQL 4.0 to MySQL 4.1 or above
+=over
+
+=item 1.
+
+You are upgrading RT from a version prior to 3.8.0, on any version
+of MySQL
+
+=item 2.
+
+You are migrating from MySQL 4.0 to MySQL 4.1 or above
+
+=back
 
 If neither of the above cases apply, your should upgrade as per the
 instructions in the README.
 
-These changes are necessary because MySQL 4.1 and greater changed some
-aspects of character set handling that may result in RT failures; this
-will manifest as multiple login requests, corrupted binary attachments,
-and corrupted image custom fields, among others.  In order to resolve
-this issue, the upgrade process will need to modify the schema.
+These changes are necessary because MySQL 4.1 and greater changed some aspects
+of character set handling that may result in RT failures; this will manifest
+as multiple login requests, corrupted binary attachments, and corrupted image
+custom fields, among others.  In order to resolve this issue, the upgrade
+process will need to modify the schema.
+
+=over
+
+=item 1.
+
+If you are moving the database and/or upgrading MySQL
+
+=over
+
+=item 1a.
+
+Dump the database; with MySQL 4.1 and greater be sure to pass the mysqldump
+command the --default-character-set=binary option.  This is necessary because
+the data was originally encoded in Latin1.
+
+=item 1b.
+
+Configure the new MySQL to use Latin1 as the default character set everywhere,
+not UTF-8.  This is necessary so the import in the next step assumes the data
+is Latin1.
+
+=item 1c.
+
+Import the dump made in step 1a into the new MySQL server, using the
+--default-character-set=binary option on restore.  This will ensure that the
+data is imported as bytes, which will be interpreted as Latin1 thanks to step
+1b above.
+
+=item 1d.
+
+Test that your RT works as expected on this new database.
+
+=back
+
+=item 2.
+
+Backup RT's database using --default-character-set=binary  Furthermore, test
+that you can restore from this backup.
+
+=item 3.
+
+Follow instructions in the README file to step 6b.
+
+=item 4.
+
+Apply changes described in the README's step 6b, but only up to version
+3.7.87.
+
+=item 5.
+
+Apply the RT 3.8 schema upgrades. Included in RT is the script
+etc/upgrade/upgrade-mysql-schema.pl that will generate the appropriate SQL
+queries:
+
+    perl etc/upgrade/upgrade-mysql-schema.pl db user pass > queries.sql
+
+If your mysql database is on a remote host, you can run the script like this
+instead:
+
+    perl etc/upgrade/upgrade-mysql-schema.pl db:host user pass > queries.sql
+
+=item 6.
+
+Check the sanity of the SQL queries in the queries.sql file yourself, or
+consult with your DBA.
+
+=item 7.
+
+Apply the queries. Note that this step can take a while; it may also require
+additional space on your hard drive comparable with size of your tables.
 
- 1) If you are moving the database and/or upgrading MySQL
-   1a) Dump the database; with MySQL 4.1 and greater be sure to pass
-       the mysqldump command the --default-character-set=binary option.
-       This is necessary because the data was originally encoded in
-       Latin1.
+    mysql -u root -p rt3 < queries.sql
 
-   1b) Configure the new MySQL to use Latin1 as the default character
-       set everywhere, not UTF-8.  This is necessary so the import in
-       the next step assumes the data is Latin1.
+NOTE that 'rt3' is the default name of the RT database, change it in the
+command above if your database is named differently.
 
-   1c) Import the dump made in step 1a into the new MySQL server, using
-       the --default-character-set=binary option on restore.  This will
-       ensure that the data is imported as bytes, which will be
-       interpreted as Latin1 thanks to step 1b above.
+This step should not produce any errors or warnings. If you see any, restore
+your database from the backup you made at step 1, and send a report to the
+rt-users@lists.bestpractical.com mailing list.
 
-   1d) Test that your RT works as expected on this new database.
+=item 8.
 
- 2) Backup RT's database using --default-character-set=binary
-    Furthermore, test that you can restore from this backup.
+Re-run the `make upgrade-database` command from step 6b of the README,
+applying the rest of the upgrades, starting with 3.7.87, and follow the
+README's remaining steps.
 
- 3) Follow instructions in the README file to step 6b.
+=item 9.
 
- 4) Apply changes described in the README's step 6b, but only up to
-    version 3.7.87.
+Test everything. The most important parts you have to test:
 
- 5) Apply the RT 3.8 schema upgrades. Included in RT is the script
-    etc/upgrade/upgrade-mysql-schema.pl that will generate the
-    appropriate SQL queries:
+=over
 
-        perl etc/upgrade/upgrade-mysql-schema.pl db user pass > queries.sql
+=item *
 
-    If your mysql database is on a remote host, you can run the script
-    like this instead:
+binary attachments, like docs, PDFs, and images
 
-        perl etc/upgrade/upgrade-mysql-schema.pl db:host user pass > queries.sql
+=item *
 
- 6) Check the sanity of the SQL queries in the queries.sql file
-    yourself, or consult with your DBA.
+binary custom fields
 
- 7) Apply the queries. Note that this step can take a while; it may also
-    require additional space on your hard drive comparable with size of
-    your tables.
+=item *
 
-        mysql -u root -p rt3 < queries.sql
+everything that may contain characters other than ASCII
 
-    NOTE that 'rt3' is the default name of the RT database, change it in
-    the command above if your database is named differently.
+=back
 
-    This step should not produce any errors or warnings. If you see any,
-    restore your database from the backup you made at step 1, and send a
-    report to the rt-users@lists.bestpractical.com mailing list.
 
- 8) Re-run the `make upgrade-database` command from step 6b of the
-    README, applying the rest of the upgrades, starting with 3.7.87, and
-    follow the README's remaining steps.
+=item 10.
 
- 9) Test everything. The most important parts you have to test:
-     * binary attachments, like docs, PDFs, and images
-     * binary custom fields
-     * everything that may contain characters other than ASCII
+If you were upgrading from MySQL 4.0, you may now, if you wish, reconfigure
+your newer MySQL instance to use UTF-8 as the default character set, as step 7
+above adjusted the character sets on all existing tables to contain UTF-8
+encoded data, rather than Latin1.
 
-10) If you were upgrading from MySQL 4.0, you may now, if you wish,
-    reconfigure your newer MySQL instance to use UTF-8 as the default
-    character set, as step 7 above adjusted the character sets on all
-    existing tables to contain UTF-8 encoded data, rather than Latin1.
+=back
index 4c3f73f..5d2cd4c 100644 (file)
@@ -23,7 +23,7 @@ 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> directory, or the non-standalone servers
+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.
 
index 15b7cb0..226c2f6 100644 (file)
@@ -347,7 +347,8 @@ Set($StoreLoops, undef);
 =item C<$MaxAttachmentSize>
 
 C<$MaxAttachmentSize> sets the maximum size (in bytes) of attachments
-stored in the database.
+stored in the database.  This setting is irrelevant unless one of
+$TruncateLongAttachments or $DropLongAttachments (below) are set.
 
 =cut
 
@@ -615,6 +616,9 @@ Set($NotifyActor, 0);
 By default, RT records each message it sends out to its own internal
 database.  To change this behavior, set C<$RecordOutgoingEmail> to 0
 
+If this is disabled, users' digest mail delivery preferences
+(i.e. EmailFrequency) will also be ignored.
+
 =cut
 
 Set($RecordOutgoingEmail, 1);
@@ -867,8 +871,8 @@ Set(@JSFiles, qw/
     jquery-1.4.2.min.js
     jquery_noconflict.js
     jquery-ui-1.8.4.custom.min.js
+    jquery-ui-timepicker-addon.js
     jquery-ui-patch-datepicker.js
-    ui.timepickr.js
     titlebox-state.js
     util.js
     userautocomplete.js
@@ -1731,12 +1735,12 @@ Set($ForceApprovalsView, 0);
 
 =head1 Extra security
 
-=over 4
-
 This is a list of extra security measures to enable that help keep your RT
 safe.  If you don't know what these mean, you should almost certainly leave the
 defaults alone.
 
+=over 4
+
 =item C<$DisallowExecuteCode>
 
 If set to a true value, the C<ExecuteCode> right will be removed from
@@ -1781,7 +1785,7 @@ backwards compatability.
 
 Set($RestrictLoginReferrer, 0);
 
-=item C<$ReferrerWhitelist>
+=item C<@ReferrerWhitelist>
 
 This is a list of hostname:port combinations that RT will treat as being
 part of RT's domain. This is particularly useful if you access RT as
@@ -1794,6 +1798,16 @@ If the "RT has detected a possible cross-site request forgery" error is triggere
 by a host:port sent by your browser that you believe should be valid, you can copy
 the host:port from the error message into this list.
 
+Simple wildcards, similar to SSL certificates, are allowed.  For example:
+
+    *.example.com:80    # matches foo.example.com
+                        # but not example.com
+                        #      or foo.bar.example.com
+
+    www*.example.com:80 # matches www3.example.com
+                        #     and www-test.example.com
+                        #     and www.example.com
+
 =cut
 
 Set(@ReferrerWhitelist, qw());
@@ -2237,10 +2251,11 @@ all possible transitions in each lifecycle using the following format:
 
 =head3 Statuses available during ticket creation
 
-By default users can create tickets with any status, except
-deleted. If you want to restrict statuses available during creation
-then describe transition from '' (empty string), like in the example
-above.
+By default users can create tickets with a status of new,
+open, or resolved, but cannot create tickets with a status of
+rejected, stalled, or deleted. If you want to change the statuses
+available during creation, update the transition from '' (empty
+string), like in the example above.
 
 =head3 Protecting status changes with rights
 
@@ -2541,7 +2556,7 @@ 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__,__Disabled__,__Lifecycle__},
 
     Groups =>
         q{'<a href="__WebPath__/Admin/Groups/Modify.html?id=__id__">__id__</a>/TITLE:#'}
@@ -2693,6 +2708,8 @@ Set($LinkTransactionsRun1Scrip, 0);
 This option has been deprecated.  You can configure this site-wide
 with L</Lifecycles> (see L</Labeling and defining actions>).
 
+=back
+
 =cut
 
 1;
index 3da4a97..b4d3eb6 100644 (file)
@@ -1,4 +1,4 @@
-# Initial data for a fresh RT3 Installation.
+# Initial data for a fresh RT installation.
 
 @Users = (
     {  Name         => 'root',
index 138971c..6897be2 100644 (file)
@@ -3,7 +3,7 @@
 CREATE TABLE Attachments (
   id INTEGER PRIMARY KEY  ,
   TransactionId INTEGER  ,
-  Parent integer NULL  ,
+  Parent integer NULL DEFAULT 0 ,
   MessageId varchar(160) NULL  ,
   Subject varchar(255) NULL  ,
   Filename varchar(255) NULL  ,
@@ -11,7 +11,7 @@ CREATE TABLE Attachments (
   ContentEncoding varchar(80) NULL  ,
   Content LONGTEXT NULL  ,
   Headers LONGTEXT NULL  ,
-  Creator integer NULL  ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL 
   
 ) ;
@@ -30,12 +30,12 @@ CREATE TABLE Queues (
   CommentAddress varchar(120) NULL  ,
   Lifecycle varchar(32) NULL  ,
   SubjectTag varchar(120) NULL  ,
-  InitialPriority integer NULL  ,
-  FinalPriority integer NULL  ,
-  DefaultDueIn integer NULL  ,
-  Creator integer NULL  ,
+  InitialPriority integer NULL DEFAULT 0 ,
+  FinalPriority integer NULL DEFAULT 0 ,
+  DefaultDueIn integer NULL DEFAULT 0 ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  ,
-  LastUpdatedBy integer NULL  ,
+  LastUpdatedBy integer NULL DEFAULT 0 ,
   LastUpdated DATETIME NULL  ,
   Disabled int2 NOT NULL DEFAULT 0 
  
@@ -51,11 +51,11 @@ CREATE TABLE Links (
   Base varchar(240) NULL  ,
   Target varchar(240) NULL  ,
   Type varchar(20) NOT NULL  ,
-  LocalTarget integer NULL  ,
-  LocalBase integer NULL  ,
-  LastUpdatedBy integer NULL  ,
+  LocalTarget integer NULL DEFAULT 0 ,
+  LocalBase integer NULL DEFAULT 0 ,
+  LastUpdatedBy integer NULL DEFAULT 0 ,
   LastUpdated DATETIME NULL  ,
-  Creator integer NULL  ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  
   
 ) ;
@@ -106,9 +106,9 @@ CREATE TABLE ScripConditions (
   Argument varchar(255) NULL  ,
   ApplicableTransTypes varchar(60) NULL  ,
 
-  Creator integer NULL  ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  ,
-  LastUpdatedBy integer NULL  ,
+  LastUpdatedBy integer NULL DEFAULT 0 ,
   LastUpdated DATETIME NULL  
   
 ) ;
@@ -119,8 +119,8 @@ CREATE TABLE ScripConditions (
 CREATE TABLE Transactions (
   id INTEGER PRIMARY KEY  ,
   ObjectType varchar(255) NULL  ,
-  ObjectId integer NULL  ,
-  TimeTaken integer NULL  ,
+  ObjectId integer NULL DEFAULT 0 ,
+  TimeTaken integer NULL DEFAULT 0 ,
   Type varchar(20) NULL  ,
   Field varchar(40) NULL  ,
   OldValue varchar(255) NULL  ,
@@ -130,7 +130,7 @@ CREATE TABLE Transactions (
   NewReference integer NULL  ,
   Data varchar(255) NULL  ,
 
-  Creator integer NULL  ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  
   
 ) ;
@@ -143,19 +143,19 @@ CREATE INDEX Transactions1 ON Transactions (ObjectType, ObjectId);
 CREATE TABLE Scrips (
   id INTEGER PRIMARY KEY  ,
   Description varchar(255),
-  ScripCondition integer NULL  ,
-  ScripAction integer NULL  ,
+  ScripCondition integer NULL DEFAULT 0 ,
+  ScripAction integer NULL DEFAULT 0 ,
   ConditionRules text NULL  ,
   ActionRules text NULL  ,
   CustomIsApplicableCode text NULL  ,
   CustomPrepareCode text NULL  ,
   CustomCommitCode text NULL  ,
   Stage varchar(32) NULL  ,
-  Queue integer NULL  ,
-  Template integer NULL  ,
-  Creator integer NULL  ,
+  Queue integer NULL DEFAULT 0 ,
+  Template integer NULL DEFAULT 0 ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  ,
-  LastUpdatedBy integer NULL  ,
+  LastUpdatedBy integer NULL DEFAULT 0 ,
   LastUpdated DATETIME NULL  
   
 ) ;
@@ -167,7 +167,7 @@ CREATE TABLE ACL (
   id INTEGER PRIMARY KEY  ,
   PrincipalType varchar(25) NOT NULL,
 
-  PrincipalId INTEGER,
+  PrincipalId INTEGER DEFAULT 0,
   RightName varchar(25) NOT NULL  ,
   ObjectType varchar(25) NOT NULL  ,
   ObjectId INTEGER default 0,
@@ -185,8 +185,8 @@ CREATE TABLE ACL (
 
 CREATE TABLE GroupMembers (
   id INTEGER PRIMARY KEY  ,
-  GroupId integer NULL,
-  MemberId integer NULL,
+  GroupId integer NULL DEFAULT 0,
+  MemberId integer NULL DEFAULT 0,
   Creator integer NOT NULL DEFAULT 0  ,
   Created DATETIME NULL  ,
   LastUpdatedBy integer NOT NULL DEFAULT 0  ,
@@ -250,9 +250,9 @@ CREATE TABLE Users (
   Timezone char(50) NULL  ,
   PGPKey text NULL,
 
-  Creator integer NULL  ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  ,
-  LastUpdatedBy integer NULL  ,
+  LastUpdatedBy integer NULL DEFAULT 0 ,
   LastUpdated DATETIME NULL  
   
 ) ;
@@ -270,20 +270,20 @@ CREATE INDEX Users4 ON Users (EmailAddress);
 
 CREATE TABLE Tickets (
   id INTEGER PRIMARY KEY  ,
-  EffectiveId integer NULL  ,
-  Queue integer NULL  ,
+  EffectiveId integer NULL DEFAULT 0 ,
+  Queue integer NULL DEFAULT 0 ,
   Type varchar(16) NULL  ,
-  IssueStatement integer NULL  ,
-  Resolution integer NULL  ,
-  Owner integer NULL  ,
+  IssueStatement integer NULL DEFAULT 0 ,
+  Resolution integer NULL DEFAULT 0 ,
+  Owner integer NULL DEFAULT 0 ,
   Subject varchar(200) NULL DEFAULT '[no subject]' ,
-  InitialPriority integer NULL  ,
-  FinalPriority integer NULL  ,
-  Priority integer NULL  ,
-  TimeEstimated integer NULL  ,
-  TimeWorked integer NULL  ,
+  InitialPriority integer NULL DEFAULT 0 ,
+  FinalPriority integer NULL DEFAULt 0 ,
+  Priority integer NULL DEFAULT 0 ,
+  TimeEstimated integer NULL DEFAULT 0 ,
+  TimeWorked integer NULL DEFAULT 0 ,
   Status varchar(64) NULL  ,
-  TimeLeft integer NULL  ,
+  TimeLeft integer NULL DEFAULT 0 ,
   Told DATETIME NULL  ,
   Starts DATETIME NULL  ,
   Started DATETIME NULL  ,
@@ -291,9 +291,9 @@ CREATE TABLE Tickets (
   Resolved DATETIME NULL  ,
 
 
-  LastUpdatedBy integer NULL  ,
+  LastUpdatedBy integer NULL DEFAULT 0 ,
   LastUpdated DATETIME NULL  ,
-  Creator integer NULL  ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  ,
   Disabled int2 NOT NULL DEFAULT 0
   
@@ -315,9 +315,9 @@ CREATE TABLE ScripActions (
   Description varchar(255) NULL  ,
   ExecModule varchar(60) NULL  ,
   Argument varchar(255) NULL  ,
-  Creator integer NULL  ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  ,
-  LastUpdatedBy integer NULL  ,
+  LastUpdatedBy integer NULL DEFAULT 0 ,
   LastUpdated DATETIME NULL  
   
 ) ;
@@ -333,11 +333,11 @@ CREATE TABLE Templates (
   Description varchar(255) NULL  ,
   Type varchar(16) NULL  ,
   Language varchar(16) NULL  ,
-  TranslationOf integer NULL  ,
+  TranslationOf integer NULL DEFAULT 0 ,
   Content blob NULL  ,
   LastUpdated DATETIME NULL  ,
-  LastUpdatedBy integer NULL  ,
-  Creator integer NULL  ,
+  LastUpdatedBy integer NULL DEFAULT 0 ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  
   
 ) ;
@@ -437,10 +437,10 @@ CREATE TABLE Attributes (
   Content LONGTEXT NULL  ,
   ContentType varchar(16),
   ObjectType varchar(25) NOT NULL  ,
-  ObjectId INTEGER default 0,
-  Creator integer NULL  ,
+  ObjectId INTEGER ,
+  Creator integer NULL DEFAULT 0 ,
   Created DATETIME NULL  ,
-  LastUpdatedBy integer NULL  ,
+  LastUpdatedBy integer NULL DEFAULT 0 ,
   LastUpdated DATETIME NULL  
  
 ) ;
@@ -483,22 +483,22 @@ Parent integer NOT NULL DEFAULT 0,
 Name varchar(255) NOT NULL DEFAULT '',
 Description varchar(255) NOT NULL DEFAULT '',
 ObjectType varchar(64) NOT NULL DEFAULT '',
-ObjectId integer NOT NULL
+ObjectId integer NOT NULL DEFAULT 0
 );
 
 
 CREATE TABLE ObjectTopics (
 id INTEGER PRIMARY KEY,
-Topic integer NOT NULL,
+Topic integer NOT NULL DEFAULT 0,
 ObjectType varchar(64) NOT NULL DEFAULT '',
-ObjectId integer NOT NULL
+ObjectId integer NOT NULL DEFAULT 0
 );
 
 CREATE TABLE ObjectClasses (
 id INTEGER PRIMARY KEY,
-Class integer NOT NULL,
+Class integer NOT NULL DEFAULT 0,
 ObjectType varchar(64) NOT NULL DEFAULT '',
-ObjectId integer NOT NULL,
+ObjectId integer NOT NULL DEFAULT 0,
 Creator integer NOT NULL DEFAULT 0,
 Created TIMESTAMP NULL,
 LastUpdatedBy integer NOT NULL DEFAULT 0,
index 34a6217..29fef5e 100644 (file)
@@ -567,7 +567,8 @@ sub Parse {
         $self->_ParseMultilineTemplate(%args);
     } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
         $self->_ParseXSVTemplate(%args);
-
+    } else {
+        RT->Logger->error("Invalid Template Content (Couldn't find ===, and is not a csv/tsv template) - unable to parse: $args{Content}");
     }
 }
 
index 4ae1a8b..2a7a2e3 100644 (file)
@@ -99,47 +99,31 @@ activated in the config.
 sub Commit {
     my $self = shift;
 
-    $self->DeferDigestRecipients() if RT->Config->Get('RecordOutgoingEmail');
+    return abs $self->SendMessage( $self->TemplateObj->MIMEObj )
+        unless RT->Config->Get('RecordOutgoingEmail');
+
+    $self->DeferDigestRecipients();
     my $message = $self->TemplateObj->MIMEObj;
 
     my $orig_message;
-    if (   RT->Config->Get('RecordOutgoingEmail')
-        && RT->Config->Get('GnuPG')->{'Enable'} )
-    {
-
-        # it's hacky, but we should know if we're going to crypt things
-        my $attachment = $self->TransactionObj->Attachments->First;
-
-        my %crypt;
-        foreach my $argument (qw(Sign Encrypt)) {
-            if ( $attachment
-                && defined $attachment->GetHeader("X-RT-$argument") )
-            {
-                $crypt{$argument} = $attachment->GetHeader("X-RT-$argument");
-            } else {
-                $crypt{$argument} = $self->TicketObj->QueueObj->$argument();
-            }
-        }
-        if ( $crypt{'Sign'} || $crypt{'Encrypt'} ) {
-            $orig_message = $message->dup;
-        }
-    }
+    $orig_message = $message->dup if RT::Interface::Email::WillSignEncrypt(
+        Attachment => $self->TransactionObj->Attachments->First,
+        Ticket     => $self->TicketObj,
+    );
 
     my ($ret) = $self->SendMessage($message);
-    if ( $ret > 0 && RT->Config->Get('RecordOutgoingEmail') ) {
-        if ($orig_message) {
-            $message->attach(
-                Type        => 'application/x-rt-original-message',
-                Disposition => 'inline',
-                Data        => $orig_message->as_string,
-            );
-        }
-        $self->RecordOutgoingMailTransaction($message);
-        $self->RecordDeferredRecipients();
-    }
-
+    return abs( $ret ) if $ret <= 0;
 
-    return ( abs $ret );
+    if ($orig_message) {
+        $message->attach(
+            Type        => 'application/x-rt-original-message',
+            Disposition => 'inline',
+            Data        => $orig_message->as_string,
+        );
+    }
+    $self->RecordOutgoingMailTransaction($message);
+    $self->RecordDeferredRecipients();
+    return 1;
 }
 
 =head2 Prepare
index f364bc9..000a8dc 100644 (file)
@@ -80,10 +80,8 @@ sub Commit {
             }
 
         }
-        $obj->SetStatus(
-            Status => $obj->QueueObj->Lifecycle->DefaultStatus('approved') || 'open',
-            Force => 1,
-        );
+        $obj->SetStatus( Status => $obj->FirstActiveStatus, Force => 1 )
+            if $obj->FirstActiveStatus;
     }
 
     my $passed = !$top->HasUnresolvedDependencies( Type => 'approval' );
@@ -98,6 +96,11 @@ sub Commit {
     $top->Correspond( MIMEObj => $template->MIMEObj );
 
     if ($passed) {
+        my $new_status = $top->QueueObj->Lifecycle->DefaultStatus('approved') || 'open';
+        if ( $new_status ne $top->Status ) {
+            $top->SetStatus( $new_status );
+        }
+
         $self->RunScripAction('Notify Owner', 'Approval Ready for Owner',
                               TicketObj => $top);
     }
index 24b952a..678aa11 100644 (file)
@@ -102,7 +102,7 @@ sub Create {
         @_
     );
 
-    my $class = RT::Class->new($RT::SystemUser);
+    my $class = RT::Class->new( $self->CurrentUser );
     $class->Load( $args{'Class'} );
     unless ( $class->Id ) {
         return ( 0, $self->loc('Invalid Class') );
index 8dd661d..47d0ebe 100644 (file)
@@ -360,6 +360,7 @@ sub LimitCustomField {
             QUOTEVALUE      => $args{'QUOTEVALUE'},
             ENTRYAGGREGATOR => 'AND', #$args{'ENTRYAGGREGATOR'},
             SUBCLAUSE       => $clause,
+            CASESENSITIVE   => 0,
         );
         $self->SUPER::Limit(
             ALIAS           => $ObjectValuesAlias,
@@ -380,6 +381,7 @@ sub LimitCustomField {
             QUOTEVALUE      => $args{'QUOTEVALUE'},
             ENTRYAGGREGATOR => $args{'ENTRYAGGREGATOR'},
             SUBCLAUSE       => $clause,
+            CASESENSITIVE   => 0,
         );
         $self->SUPER::Limit(
             ALIAS           => $ObjectValuesAlias,
@@ -389,6 +391,7 @@ sub LimitCustomField {
             QUOTEVALUE      => $args{'QUOTEVALUE'},
             ENTRYAGGREGATOR => $args{'ENTRYAGGREGATOR'},
             SUBCLAUSE       => $clause,
+            CASESENSITIVE   => 0,
         );
     }
 }
index fb17da3..f1d9a63 100644 (file)
@@ -600,8 +600,8 @@ sub DelHeader {
 
     my $newheader = '';
     foreach my $line ($self->_SplitHeaders) {
-        next if $line =~ /^\Q$tag\E:\s+(.*)$/is;
-       $newheader .= "$line\n";
+        next if $line =~ /^\Q$tag\E:\s+/i;
+        $newheader .= "$line\n";
     }
     return $self->__Set( Field => 'Headers', Value => $newheader);
 }
@@ -617,9 +617,7 @@ sub AddHeader {
 
     my $newheader = $self->__Value( 'Headers' );
     while ( my ($tag, $value) = splice @_, 0, 2 ) {
-        $value = '' unless defined $value;
-        $value =~ s/\s+$//s;
-        $value =~ s/\r+\n/\n /g;
+        $value = $self->_CanonicalizeHeaderValue($value);
         $newheader .= "$tag: $value\n";
     }
     return $self->__Set( Field => 'Headers', Value => $newheader);
@@ -632,24 +630,39 @@ Replace or add a Header to the attachment's headers.
 =cut
 
 sub SetHeader {
-    my $self = shift;
-    my $tag = shift;
+    my $self  = shift;
+    my $tag   = shift;
+    my $value = $self->_CanonicalizeHeaderValue(shift);
 
+    my $replaced  = 0;
     my $newheader = '';
-    foreach my $line ($self->_SplitHeaders) {
-        if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
-           $newheader .= "$tag: $_[0]\n";
-           undef $tag;
+    foreach my $line ( $self->_SplitHeaders ) {
+        if ( $line =~ /^\Q$tag\E:\s+/i ) {
+            # replace first instance, skip all the rest
+            unless ($replaced) {
+                $newheader .= "$tag: $value\n";
+                $replaced = 1;
+            }
+        } else {
+            $newheader .= "$line\n";
         }
-       else {
-           $newheader .= "$line\n";
-       }
     }
 
-    $newheader .= "$tag: $_[0]\n" if defined $tag;
+    $newheader .= "$tag: $value\n" unless $replaced;
     $self->__Set( Field => 'Headers', Value => $newheader);
 }
 
+sub _CanonicalizeHeaderValue {
+    my $self  = shift;
+    my $value = shift;
+
+    $value = '' unless defined $value;
+    $value =~ s/\s+$//s;
+    $value =~ s/\r*\n/\n /g;
+
+    return $value;
+}
+
 =head2 SplitHeaders
 
 Returns an array of this attachment object's headers, with one header 
@@ -676,6 +689,12 @@ sub _SplitHeaders {
     my $self = shift;
     my $headers = (shift || $self->_Value('Headers'));
     my @headers;
+    # XXX TODO: splitting on \n\w is _wrong_ as it treats \n[ as a valid
+    # continuation, which it isn't.  The correct split pattern, per RFC 2822,
+    # is /\n(?=[^ \t]|\z)/.  That is, only "\n " or "\n\t" is a valid
+    # continuation.  Older values of X-RT-GnuPG-Status contain invalid
+    # continuations and rely on this bogus split pattern, however, so it is
+    # left as-is for now.
     for (split(/\n(?=\w|\z)/,$headers)) {
         push @headers, $_;
 
index 301b9f5..ba338bb 100644 (file)
@@ -400,8 +400,8 @@ our %META = (
             Description => q|What tickets to display in the 'More about requestor' box|,                #loc
             Values      => [qw(Active Inactive All None)],
             ValuesLabel => {
-                Active   => "Show the Requestor's 10 highest priority open tickets",                  #loc
-                Inactive => "Show the Requestor's 10 highest priority closed tickets",      #loc
+                Active   => "Show the Requestor's 10 highest priority active tickets",                  #loc
+                Inactive => "Show the Requestor's 10 highest priority inactive tickets",      #loc
                 All      => "Show the Requestor's 10 highest priority tickets",      #loc
                 None     => "Show no tickets for the Requestor", #loc
             },
@@ -738,7 +738,7 @@ our %META = (
 
             my %seen;
             foreach my $encoding ( grep defined && length, splice @$value ) {
-                next if $seen{ $encoding }++;
+                next if $seen{ $encoding };
                 if ( $encoding eq '*' ) {
                     unshift @$value, '*';
                     next;
index ab444d0..2330478 100644 (file)
@@ -900,6 +900,19 @@ sub FindProtectedParts {
             $RT::Logger->warning( "Entity of type ". $entity->effective_type ." has no body" );
             return ();
         }
+
+        # Deal with "partitioned" PGP mail, which (contrary to common
+        # sense) unnecessarily applies a base64 transfer encoding to PGP
+        # mail (whose content is already base64-encoded).
+        if ( $entity->bodyhandle->is_encoded and $entity->head->mime_encoding ) {
+            pipe( my ($read_decoded, $write_decoded) );
+            my $decoder = MIME::Decoder->new( $entity->head->mime_encoding );
+            if ($decoder) {
+                eval { $decoder->decode($io, $write_decoded) };
+                $io = $read_decoded;
+            }
+        }
+
         while ( defined($_ = $io->getline) ) {
             next unless /^-----BEGIN PGP (SIGNED )?MESSAGE-----/;
             my $type = $1? 'signed': 'encrypted';
@@ -1064,9 +1077,13 @@ sub VerifyDecrypt {
         }
         if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
             my $method = $args{'AddStatus'} ? 'add' : 'set';
+            # Let the header be modified so continuations are handled
+            my $modify = $status_on->head->modify;
+            $status_on->head->modify(1);
             $status_on->head->$method(
                 'X-RT-GnuPG-Status' => $res[-1]->{'status'}
             );
+            $status_on->head->modify($modify);
         }
     }
     foreach my $item( grep $_->{'Type'} eq 'encrypted', @protected ) {
@@ -1083,9 +1100,13 @@ sub VerifyDecrypt {
         }
         if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
             my $method = $args{'AddStatus'} ? 'add' : 'set';
+            # Let the header be modified so continuations are handled
+            my $modify = $status_on->head->modify;
+            $status_on->head->modify(1);
             $status_on->head->$method(
                 'X-RT-GnuPG-Status' => $res[-1]->{'status'}
             );
+            $status_on->head->modify($modify);
         }
     }
     return @res;
@@ -1683,6 +1704,7 @@ my %ignore_keyword = map { $_ => 1 } qw(
     BEGIN_ENCRYPTION SIG_ID VALIDSIG
     ENC_TO BEGIN_DECRYPTION END_DECRYPTION GOODMDC
     TRUST_UNDEFINED TRUST_NEVER TRUST_MARGINAL TRUST_FULLY TRUST_ULTIMATE
+    DECRYPTION_INFO
 );
 
 sub ParseStatus {
@@ -2106,7 +2128,9 @@ sub GetKeysInfo {
     eval {
         local $SIG{'CHLD'} = 'DEFAULT';
         my $method = $type eq 'private'? 'list_secret_keys': 'list_public_keys';
-        my $pid = safe_run_child { $gnupg->$method( handles => $handles, $email? (command_args => $email) : () ) };
+        my $pid = safe_run_child { $gnupg->$method( handles => $handles, $email
+                                                        ? (command_args => [ "--", $email])
+                                                        : () ) };
         close $handle{'stdin'};
         waitpid $pid, 0;
     };
@@ -2300,7 +2324,7 @@ sub DeleteKey {
         my $pid = safe_run_child { $gnupg->wrap_call(
             handles => $handles,
             commands => ['--delete-secret-and-public-key'],
-            command_args => [$key],
+            command_args => ["--", $key],
         ) };
         close $handle{'stdin'};
         while ( my $str = readline $handle{'status'} ) {
index 14ffa6a..2e2bbc4 100644 (file)
@@ -454,6 +454,36 @@ sub CurrentUserCanCreateAny {
     return 0;
 }
 
+=head2 Delete
+
+Deletes the dashboard and related subscriptions.
+Returns a tuple of status and message, where status is true upon success.
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    my $id = $self->id;
+    my ( $status, $msg ) = $self->SUPER::Delete(@_);
+    if ( $status ) {
+        # delete all the subscriptions
+        my $subscriptions = RT::Attributes->new( RT->SystemUser );
+        $subscriptions->Limit(
+            FIELD => 'Name',
+            VALUE => 'Subscription',
+        );
+        $subscriptions->Limit(
+            FIELD => 'Description',
+            VALUE => "Subscription to dashboard $id",
+        );
+        while ( my $subscription = $subscriptions->Next ) {
+            $subscription->Delete();
+        }
+    }
+
+    return ( $status, $msg );
+}
+
 RT::Base->_ImportOverlays();
 
 1;
index 59b839e..4c8a5db 100644 (file)
@@ -50,7 +50,7 @@ package RT;
 use warnings;
 use strict;
 
-our $VERSION = '4.0.6';
+our $VERSION = '4.0.8';
 
 
 
index 99d10e3..03c262b 100644 (file)
@@ -858,26 +858,28 @@ sub InsertData {
                 @queues = @{ delete $item->{'Queue'} };
             }
 
-            my ( $return, $msg ) = $new_entry->Create(%$item);
-            unless( $return ) {
-                $RT::Logger->error( $msg );
-                next;
-            }
-
             if ( $item->{'BasedOn'} ) {
-                my $basedon = RT::CustomField->new($RT::SystemUser);
-                my ($ok, $msg ) = $basedon->LoadByCols( Name => $item->{'BasedOn'},
-                                                        LookupType => $new_entry->LookupType );
-                if ($ok) {
-                    ($ok, $msg) = $new_entry->SetBasedOn( $basedon );
+                if ( $item->{'LookupType'} ) {
+                    my $basedon = RT::CustomField->new($RT::SystemUser);
+                    my ($ok, $msg ) = $basedon->LoadByCols( Name => $item->{'BasedOn'},
+                                                            LookupType => $item->{'LookupType'} );
                     if ($ok) {
-                        $RT::Logger->debug("Added BasedOn $item->{BasedOn}: $msg");
+                        $item->{'BasedOn'} = $basedon->Id;
                     } else {
-                        $RT::Logger->error("Failed to add basedOn $item->{BasedOn}: $msg");
+                        $RT::Logger->error("Unable to load $item->{BasedOn} as a $item->{LookupType} CF.  Skipping BasedOn: $msg");
+                        delete $item->{'BasedOn'};
                     }
                 } else {
-                    $RT::Logger->error("Unable to load $item->{BasedOn} as a $item->{LookupType} CF.  Skipping BasedOn");
+                    $RT::Logger->error("Unable to load CF $item->{BasedOn} because no LookupType was specified.  Skipping BasedOn");
+                    delete $item->{'BasedOn'};
                 }
+
+            } 
+
+            my ( $return, $msg ) = $new_entry->Create(%$item);
+            unless( $return ) {
+                $RT::Logger->error( $msg );
+                next;
             }
 
             foreach my $value ( @{$values} ) {
index cadf7cc..e453cfa 100644 (file)
@@ -227,7 +227,7 @@ sub SetMIMEEntityToEncoding {
 
     my $body = $entity->bodyhandle;
 
-    if ( $enc ne $charset && $body ) {
+    if ( $body && ($enc ne $charset || $enc =~ /^utf-?8(?:-strict)?$/i) ) {
         my $string = $body->as_string or return;
 
         $RT::Logger->debug( "Converting '$charset' to '$enc' for "
@@ -335,7 +335,7 @@ sub DecodeMIMEWordsToEncoding {
             }
 
             # now we have got a decoded subject, try to convert into the encoding
-            unless ( $charset eq $to_charset ) {
+            if ( $charset ne $to_charset || $charset =~ /^utf-?8(?:-strict)?$/i ) {
                 Encode::from_to( $enc_str, $charset, $to_charset );
             }
 
@@ -537,7 +537,7 @@ sub SetMIMEHeadToEncoding {
         my @values = $head->get_all($tag);
         $head->delete($tag);
         foreach my $value (@values) {
-            if ( $charset ne $enc ) {
+            if ( $charset ne $enc || $enc =~ /^utf-?8(?:-strict)?$/i ) {
                 Encode::_utf8_off($value);
                 Encode::from_to( $value, $charset => $enc );
             }
index 385ba72..38157c2 100644 (file)
@@ -318,6 +318,35 @@ header field then it's value is used
 
 =cut
 
+sub WillSignEncrypt {
+    my %args = @_;
+    my $attachment = delete $args{Attachment};
+    my $ticket     = delete $args{Ticket};
+
+    if ( not RT->Config->Get('GnuPG')->{'Enable'} ) {
+        $args{Sign} = $args{Encrypt} = 0;
+        return wantarray ? %args : 0;
+    }
+
+    for my $argument ( qw(Sign Encrypt) ) {
+        next if defined $args{ $argument };
+
+        if ( $attachment and defined $attachment->GetHeader("X-RT-$argument") ) {
+            $args{$argument} = $attachment->GetHeader("X-RT-$argument");
+        } elsif ( $ticket and $argument eq "Encrypt" ) {
+            $args{Encrypt} = $ticket->QueueObj->Encrypt();
+        } elsif ( $ticket and $argument eq "Sign" ) {
+            # Note that $queue->Sign is UI-only, and that all
+            # UI-generated messages explicitly set the X-RT-Crypt header
+            # to 0 or 1; thus this path is only taken for messages
+            # generated _not_ via the web UI.
+            $args{Sign} = $ticket->QueueObj->SignAuto();
+        }
+    }
+
+    return wantarray ? %args : ($args{Sign} || $args{Encrypt});
+}
+
 sub SendEmail {
     my (%args) = (
         Entity => undef,
@@ -366,23 +395,12 @@ sub SendEmail {
     }
 
     if ( RT->Config->Get('GnuPG')->{'Enable'} ) {
-        my %crypt;
-
-        my $attachment;
-        $attachment = $TransactionObj->Attachments->First
-            if $TransactionObj;
-
-        foreach my $argument ( qw(Sign Encrypt) ) {
-            next if defined $args{ $argument };
-
-            if ( $attachment && defined $attachment->GetHeader("X-RT-$argument") ) {
-                $crypt{$argument} = $attachment->GetHeader("X-RT-$argument");
-            } elsif ( $TicketObj ) {
-                $crypt{$argument} = $TicketObj->QueueObj->$argument();
-            }
-        }
-
-        my $res = SignEncrypt( %args, %crypt );
+        %args = WillSignEncrypt(
+            %args,
+            Attachment => $TransactionObj ? $TransactionObj->Attachments->First : undef,
+            Ticket     => $TicketObj,
+        );
+        my $res = SignEncrypt( %args );
         return $res unless $res > 0;
     }
 
@@ -787,7 +805,7 @@ sub GetForwardFrom {
     my $ticket = $args{Ticket} || $txn->Object;
 
     if ( RT->Config->Get('ForwardFromUser') ) {
-        return ( $txn || $ticket )->CurrentUser->UserObj->EmailAddress;
+        return ( $txn || $ticket )->CurrentUser->EmailAddress;
     }
     else {
         return $ticket->QueueObj->CorrespondAddress
@@ -1206,8 +1224,16 @@ sub SetInReplyTo {
         if @references > 10;
 
     my $mail = $args{'Message'};
-    $mail->head->set( 'In-Reply-To' => join ' ', @rtid? (@rtid) : (@id) ) if @id || @rtid;
-    $mail->head->set( 'References' => join ' ', @references );
+    $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) );
+}
+
+sub ExtractTicketId {
+    my $entity = shift;
+
+    my $subject = $entity->head->get('Subject') || '';
+    chomp $subject;
+    return ParseTicketId( $subject );
 }
 
 sub ParseTicketId {
@@ -1433,7 +1459,7 @@ sub Gateway {
     }
     # }}}
 
-    $args{'ticket'} ||= ParseTicketId( $Subject );
+    $args{'ticket'} ||= ExtractTicketId( $Message );
 
     $SystemTicket = RT::Ticket->new( RT->SystemUser );
     $SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
@@ -1689,17 +1715,20 @@ sub _RunUnsafeAction {
             return ( 0, "Ticket not taken" );
         }
     } elsif ( $args{'Action'} =~ /^resolve$/i ) {
-        my ( $status, $msg ) = $args{'Ticket'}->SetStatus('resolved');
-        unless ($status) {
+        my $new_status = $args{'Ticket'}->FirstInactiveStatus;
+        if ($new_status) {
+            my ( $status, $msg ) = $args{'Ticket'}->SetStatus($new_status);
+            unless ($status) {
 
-            #Warn the sender that we couldn't actually submit the comment.
-            MailError(
-                To          => $args{'ErrorsTo'},
-                Subject     => "Ticket not resolved",
-                Explanation => $msg,
-                MIMEObj     => $args{'Message'}
-            );
-            return ( 0, "Ticket not resolved" );
+                #Warn the sender that we couldn't actually submit the comment.
+                MailError(
+                    To          => $args{'ErrorsTo'},
+                    Subject     => "Ticket not resolved",
+                    Explanation => $msg,
+                    MIMEObj     => $args{'Message'}
+                );
+                return ( 0, "Ticket not resolved" );
+            }
         }
     } else {
         return ( 0, "Not supported unsafe action $args{'Action'}", $args{'Ticket'} );
index e508908..87a523d 100644 (file)
@@ -77,8 +77,9 @@ sub GetCurrentUser {
 
     foreach my $p ( $args{'Message'}->parts_DFS ) {
         $p->head->delete($_) for qw(
-            X-RT-GnuPG-Status X-RT-Incoming-Encrypton
+            X-RT-GnuPG-Status X-RT-Incoming-Encryption
             X-RT-Incoming-Signature X-RT-Privacy
+            X-RT-Sign X-RT-Encrypt
         );
     }
 
index c8b258f..0bb7a83 100644 (file)
@@ -261,7 +261,15 @@ sub HandleRequest {
 
     $HTML::Mason::Commands::m->comp( '/Elements/SetupSessionCookie', %$ARGS );
     SendSessionCookie();
-    $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new() unless _UserLoggedIn();
+
+    if ( _UserLoggedIn() ) {
+        # make user info up to date
+        $HTML::Mason::Commands::session{'CurrentUser'}
+          ->Load( $HTML::Mason::Commands::session{'CurrentUser'}->id );
+    }
+    else {
+        $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
+    }
 
     # Process session-related callbacks before any auth attempts
     $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Session', CallbackPage => '/autohandler' );
@@ -287,7 +295,7 @@ sub HandleRequest {
             my $m = $HTML::Mason::Commands::m;
 
             # REST urls get a special 401 response
-            if ($m->request_comp->path =~ '^/REST/\d+\.\d+/') {
+            if ($m->request_comp->path =~ m{^/REST/\d+\.\d+/}) {
                 $HTML::Mason::Commands::r->content_type("text/plain");
                 $m->error_format("text");
                 $m->out("RT/$RT::VERSION 401 Credentials required\n");
@@ -296,12 +304,12 @@ sub HandleRequest {
             }
             # Specially handle /index.html so that we get a nicer URL
             elsif ( $m->request_comp->path eq '/index.html' ) {
-                my $next = SetNextPage(RT->Config->Get('WebURL'));
+                my $next = SetNextPage($ARGS);
                 $m->comp('/NoAuth/Login.html', next => $next, actions => [$msg]);
                 $m->abort;
             }
             else {
-                TangentForLogin(results => ($msg ? LoginError($msg) : undef));
+                TangentForLogin($ARGS, results => ($msg ? LoginError($msg) : undef));
             }
         }
     }
@@ -354,7 +362,7 @@ sub LoginError {
     return $key;
 }
 
-=head2 SetNextPage [PATH]
+=head2 SetNextPage ARGSRef [PATH]
 
 Intuits and stashes the next page in the sesssion hash.  If PATH is
 specified, uses that instead of the value of L<IntuitNextPage()>.  Returns
@@ -363,24 +371,68 @@ the hash value.
 =cut
 
 sub SetNextPage {
-    my $next = shift || IntuitNextPage();
+    my $ARGS = shift;
+    my $next = $_[0] ? $_[0] : IntuitNextPage();
     my $hash = Digest::MD5::md5_hex($next . $$ . rand(1024));
+    my $page = { url => $next };
+
+    # If an explicit URL was passed and we didn't IntuitNextPage, then
+    # IsPossibleCSRF below is almost certainly unrelated to the actual
+    # destination.  Currently explicit next pages aren't used in RT, but the
+    # API is available.
+    if (not $_[0] and RT->Config->Get("RestrictReferrer")) {
+        # This isn't really CSRF, but the CSRF heuristics are useful for catching
+        # requests which may have unintended side-effects.
+        my ($is_csrf, $msg, @loc) = IsPossibleCSRF($ARGS);
+        if ($is_csrf) {
+            RT->Logger->notice(
+                "Marking original destination as having side-effects before redirecting for login.\n"
+               ."Request: $next\n"
+               ."Reason: " . HTML::Mason::Commands::loc($msg, @loc)
+            );
+            $page->{'HasSideEffects'} = [$msg, @loc];
+        }
+    }
 
-    $HTML::Mason::Commands::session{'NextPage'}->{$hash} = $next;
+    $HTML::Mason::Commands::session{'NextPage'}->{$hash} = $page;
     $HTML::Mason::Commands::session{'i'}++;
     return $hash;
 }
 
+=head2 FetchNextPage HASHKEY
+
+Returns the stashed next page hashref for the given hash.
+
+=cut
+
+sub FetchNextPage {
+    my $hash = shift || "";
+    return $HTML::Mason::Commands::session{'NextPage'}->{$hash};
+}
+
+=head2 RemoveNextPage HASHKEY
+
+Removes the stashed next page for the given hash and returns it.
+
+=cut
+
+sub RemoveNextPage {
+    my $hash = shift || "";
+    return delete $HTML::Mason::Commands::session{'NextPage'}->{$hash};
+}
 
-=head2 TangentForLogin [HASH]
+=head2 TangentForLogin ARGSRef [HASH]
 
 Redirects to C</NoAuth/Login.html>, setting the value of L<IntuitNextPage> as
-the next page.  Optionally takes a hash which is dumped into query params.
+the next page.  Takes a hashref of request %ARGS as the first parameter.
+Optionally takes all other parameters as a hash which is dumped into query
+params.
 
 =cut
 
 sub TangentForLogin {
-    my $hash  = SetNextPage();
+    my $ARGS  = shift;
+    my $hash  = SetNextPage($ARGS);
     my %query = (@_, next => $hash);
     my $login = RT->Config->Get('WebURL') . 'NoAuth/Login.html?';
     $login .= $HTML::Mason::Commands::m->comp('/Elements/QueryString', %query);
@@ -395,8 +447,9 @@ calls L<TangentForLogin> with the appropriate results key.
 =cut
 
 sub TangentForLoginWithError {
-    my $key = LoginError(HTML::Mason::Commands::loc(@_));
-    TangentForLogin( results => $key );
+    my $ARGS = shift;
+    my $key  = LoginError(HTML::Mason::Commands::loc(@_));
+    TangentForLogin( $ARGS, results => $key );
 }
 
 =head2 IntuitNextPage
@@ -455,7 +508,7 @@ sub MaybeShowInstallModePage {
     my $m = $HTML::Mason::Commands::m;
     if ( $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex') ) {
         $m->call_next();
-    } elsif ( $m->request_comp->path !~ '^(/+)Install/' ) {
+    } elsif ( $m->request_comp->path !~ m{^(/+)Install/} ) {
         RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "Install/index.html" );
     } else {
         $m->call_next();
@@ -551,7 +604,7 @@ sub ShowRequestedPage {
     unless ( $HTML::Mason::Commands::session{'CurrentUser'}->Privileged ) {
 
         # if the user is trying to access a ticket, redirect them
-        if ( $m->request_comp->path =~ '^(/+)Ticket/Display.html' && $ARGS->{'id'} ) {
+        if ( $m->request_comp->path =~ m{^(/+)Ticket/Display.html} && $ARGS->{'id'} ) {
             RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/Display.html?id=" . $ARGS->{'id'} );
         }
 
@@ -592,7 +645,8 @@ sub AttemptExternalAuth {
             $user =~ s/^\Q$NodeName\E\\//i;
         }
 
-        my $next = delete $HTML::Mason::Commands::session{'NextPage'}->{$ARGS->{'next'} || ''};
+        my $next = RemoveNextPage($ARGS->{'next'});
+           $next = $next->{'url'} if ref $next;
         InstantiateNewSession() unless _UserLoggedIn;
         $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
         $HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user);
@@ -631,7 +685,7 @@ sub AttemptExternalAuth {
                 delete $HTML::Mason::Commands::session{'CurrentUser'};
 
                 if (RT->Config->Get('WebFallbackToInternalAuth')) {
-                    TangentForLoginWithError('Cannot create user: [_1]', $msg);
+                    TangentForLoginWithError($ARGS, 'Cannot create user: [_1]', $msg);
                 } else {
                     $m->abort();
                 }
@@ -653,14 +707,14 @@ sub AttemptExternalAuth {
             delete $HTML::Mason::Commands::session{'CurrentUser'};
             $user = $orig_user;
 
-            if ( RT->Config->Get('WebExternalOnly') ) {
-                TangentForLoginWithError('You are not an authorized user');
+            unless ( RT->Config->Get('WebFallbackToInternalAuth') ) {
+                TangentForLoginWithError($ARGS, 'You are not an authorized user');
             }
         }
     } elsif ( RT->Config->Get('WebFallbackToInternalAuth') ) {
         unless ( defined $HTML::Mason::Commands::session{'CurrentUser'} ) {
             # XXX unreachable due to prior defaulting in HandleRequest (check c34d108)
-            TangentForLoginWithError('You are not an authorized user');
+            TangentForLoginWithError($ARGS, 'You are not an authorized user');
         }
     } else {
 
@@ -691,7 +745,8 @@ sub AttemptPasswordAuthentication {
 
         # It's important to nab the next page from the session before we blow
         # the session away
-        my $next = delete $HTML::Mason::Commands::session{'NextPage'}->{$ARGS->{'next'} || ''};
+        my $next = RemoveNextPage($ARGS->{'next'});
+           $next = $next->{'url'} if ref $next;
 
         InstantiateNewSession();
         $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
@@ -964,7 +1019,7 @@ sub MobileClient {
     my $self = shift;
 
 
-if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60)/io && !$HTML::Mason::Commands::session{'NotMobile'})  {
+if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60|Mobile)/io && !$HTML::Mason::Commands::session{'NotMobile'})  {
     return 1;
 } else {
     return undef;
@@ -1171,6 +1226,21 @@ our %is_whitelisted_component = (
     # information for the search.  Because it's a straight-up read, in
     # addition to embedding its own auth, it's fine.
     '/NoAuth/rss/dhandler' => 1,
+
+    # While these can be used for denial-of-service against RT
+    # (construct a very inefficient query and trick lots of users into
+    # running them against RT) it's incredibly useful to be able to link
+    # to a search result or bookmark a result page.
+    '/Search/Results.html' => 1,
+    '/Search/Simple.html'  => 1,
+    '/m/tickets/search'     => 1,
+);
+
+# Components which are blacklisted from automatic, argument-based whitelisting.
+# These pages are not idempotent when called with just an id.
+our %is_blacklisted_component = (
+    # Takes only id and toggles bookmark state
+    '/Helpers/Toggle/TicketBookmark' => 1,
 );
 
 sub IsCompCSRFWhitelisted {
@@ -1195,6 +1265,10 @@ sub IsCompCSRFWhitelisted {
         delete $args{pass};
     }
 
+    # Some pages aren't idempotent even with safe args like id; blacklist
+    # them from the automatic whitelisting below.
+    return 0 if $is_blacklisted_component{$comp};
+
     # Eliminate arguments that do not indicate an effectful request.
     # For example, "id" is acceptable because that is how RT retrieves a
     # record.
@@ -1225,7 +1299,19 @@ sub IsRefererCSRFWhitelisted {
     my $configs;
     for my $config ( $base_url, RT->Config->Get('ReferrerWhitelist') ) {
         push @$configs,$config;
-        return 1 if $referer->host_port eq $config;
+
+        my $host_port = $referer->host_port;
+        if ($config =~ /\*/) {
+            # Turn a literal * into a domain component or partial component match.
+            # Refer to http://tools.ietf.org/html/rfc2818#page-5
+            my $regex = join "[a-zA-Z0-9\-]*",
+                         map { quotemeta($_) }
+                       split /\*/, $config;
+
+            return 1 if $host_port =~ /^$regex$/i;
+        } else {
+            return 1 if $host_port eq $config;
+        }
     }
 
     return (0,$referer,$configs);
@@ -1378,6 +1464,30 @@ sub MaybeShowInterstitialCSRFPage {
     # Calls abort, never gets here
 }
 
+our @POTENTIAL_PAGE_ACTIONS = (
+    qr'/Ticket/Create.html' => "create a ticket",              # loc
+    qr'/Ticket/'            => "update a ticket",              # loc
+    qr'/Admin/'             => "modify RT's configuration",    # loc
+    qr'/Approval/'          => "update an approval",           # loc
+    qr'/Articles/'          => "update an article",            # loc
+    qr'/Dashboards/'        => "modify a dashboard",           # loc
+    qr'/m/ticket/'          => "update a ticket",              # loc
+    qr'Prefs'               => "modify your preferences",      # loc
+    qr'/Search/'            => "modify or access a search",    # loc
+    qr'/SelfService/Create' => "create a ticket",              # loc
+    qr'/SelfService/'       => "update a ticket",              # loc
+);
+
+sub PotentialPageAction {
+    my $page = shift;
+    my @potentials = @POTENTIAL_PAGE_ACTIONS;
+    while (my ($pattern, $result) = splice @potentials, 0, 2) {
+        return HTML::Mason::Commands::loc($result)
+            if $page =~ $pattern;
+    }
+    return "";
+}
+
 package HTML::Mason::Commands;
 
 use vars qw/$r $m %session/;
@@ -1604,9 +1714,8 @@ sub CreateTicket {
         }
     }
 
-    foreach my $argument (qw(Encrypt Sign)) {
-        $MIMEObj->head->replace( "X-RT-$argument" => $ARGS{$argument} ? 1 : 0 )
-          if defined $ARGS{$argument};
+    for my $argument (qw(Encrypt Sign)) {
+        $MIMEObj->head->replace( "X-RT-$argument" => $ARGS{$argument} ? 1 : 0 );
     }
 
     my %create_args = (
@@ -1941,7 +2050,7 @@ sub MakeMIMEEntity {
     );
     my $Message = MIME::Entity->build(
         Type    => 'multipart/mixed',
-        "Message-Id" => RT::Interface::Email::GenMessageId,
+        "Message-Id" => Encode::encode_utf8( RT::Interface::Email::GenMessageId ),
         map { $_ => Encode::encode_utf8( $args{ $_} ) }
             grep defined $args{$_}, qw(Subject From Cc)
     );
index 6b351e9..045df1f 100644 (file)
@@ -150,10 +150,12 @@ treated as relative to it's parent's path, and made absolute.
 sub path {
     my $self = shift;
     if (@_) {
-        $self->{path} = shift;
-        $self->{path} = URI->new_abs($self->{path}, $self->parent->path . "/")->as_string
-            if defined $self->{path} and $self->parent and $self->parent->path;
-        $self->{path} =~ s!///!/! if $self->{path};
+        if (defined($self->{path} = shift)) {
+            my $base  = ($self->parent and $self->parent->path) ? $self->parent->path : "";
+               $base .= "/" unless $base =~ m{/$};
+            my $uri = URI->new_abs($self->{path}, $base);
+            $self->{path} = $uri->as_string;
+        }
     }
     return $self->{path};
 }
@@ -230,6 +232,7 @@ sub child {
         if ( defined $path and length $path ) {
             my $base_path = $HTML::Mason::Commands::r->path_info;
             my $query     = $HTML::Mason::Commands::m->cgi_object->query_string;
+            $base_path =~ s!/+!/!g;
             $base_path .= "?$query" if defined $query and length $query;
 
             $base_path =~ s/index\.html$//;
diff --git a/lib/RT/Pod/HTML.pm b/lib/RT/Pod/HTML.pm
new file mode 100644 (file)
index 0000000..8ddce42
--- /dev/null
@@ -0,0 +1,66 @@
+use strict;
+use warnings;
+
+package RT::Pod::HTML;
+use base 'Pod::Simple::XHTML';
+
+sub new {
+    my $self = shift->SUPER::new(@_);
+    $self->index(1);
+    $self->anchor_items(1);
+    return $self;
+}
+
+sub perldoc_url_prefix { "http://metacpan.org/module/" }
+
+sub html_header { '' }
+sub html_footer {
+    my $self = shift;
+    my $toc  = "../" x ($self->batch_mode_current_level - 1);
+    return '<a href="./' . $toc . '">&larr; Back to index</a>';
+}
+
+sub start_Verbatim { $_[0]{'scratch'} = "<pre>" }
+sub end_Verbatim   { $_[0]{'scratch'} .= "</pre>"; $_[0]->emit; }
+
+sub _end_head {
+    my $self = shift;
+    $self->{scratch} = '<a href="#___top">' . $self->{scratch} . '</a>';
+    return $self->SUPER::_end_head(@_);
+}
+
+sub resolve_pod_page_link {
+    my $self = shift;
+    my ($name, $section) = @_;
+
+    # Only try to resolve local links if we're in batch mode and are linking
+    # outside the current document.
+    return $self->SUPER::resolve_pod_page_link(@_)
+        unless $self->batch_mode and $name;
+
+    $section = defined $section
+        ? '#' . $self->idify($section, 1)
+        : '';
+
+    my $local;
+    if ($name =~ /^RT::/) {
+        $local = join "/",
+                  map { $self->encode_entities($_) }
+                split /::/, $name;
+    }
+    elsif ($name =~ /^rt-/) {
+        $local = $self->encode_entities($name);
+    }
+
+    if ($local) {
+        # Resolve links correctly by going up
+        my $depth = $self->batch_mode_current_level - 1;
+        return join "/",
+                    ($depth ? ".." x $depth : ()),
+                    "$local.html$section";
+    } else {
+        return $self->SUPER::resolve_pod_page_link(@_)
+    }
+}
+
+1;
diff --git a/lib/RT/Pod/HTMLBatch.pm b/lib/RT/Pod/HTMLBatch.pm
new file mode 100644 (file)
index 0000000..8d1b67f
--- /dev/null
@@ -0,0 +1,131 @@
+use strict;
+use warnings;
+
+package RT::Pod::HTMLBatch;
+use base 'Pod::Simple::HTMLBatch';
+
+use List::MoreUtils qw/all/;
+
+use RT::Pod::Search;
+use RT::Pod::HTML;
+
+sub new {
+    my $self = shift->SUPER::new(@_);
+    $self->verbose(0);
+
+    # Per-page output options
+    $self->css_flurry(0);          # No CSS
+    $self->javascript_flurry(0);   # No JS
+    $self->no_contents_links(1);   # No header/footer "Back to contents" links
+
+    # TOC options
+    $self->index(1);                    # Write a per-page TOC
+    $self->contents_file("index.html"); # Write a global TOC
+
+    $self->html_render_class('RT::Pod::HTML');
+    $self->search_class('RT::Pod::Search');
+
+    return $self;
+}
+
+sub classify {
+    my $self = shift;
+    my %info = (@_);
+
+    my $is_install_doc = sub {
+        my %page = @_;
+        local $_ = $page{name};
+        return 1 if /^(README|UPGRADING)/;
+        return 1 if $_ eq "RT_Config";
+        return 1 if $_ eq "web_deployment";
+        return 1 if $page{infile} =~ m{^configure(\.ac)?$};
+        return 0;
+    };
+
+    my $section = $info{infile} =~ m{/plugins/([^/]+)}      ? "05 Extension: $1"           :
+                  $info{infile} =~ m{/local/}               ? '04 Local Documenation'      :
+                  $is_install_doc->(%info)                  ? '00 Install and Upgrade '.
+                                                                 'Documentation'           :
+                  $info{infile} =~ m{/(docs|etc)/}          ? '01 User Documentation'      :
+                  $info{infile} =~ m{/bin/}                 ? '02 Utilities (bin)'         :
+                  $info{infile} =~ m{/sbin/}                ? '03 Utilities (sbin)'        :
+                  $info{name}   =~ /^RT::Action/            ? '08 Actions'                 :
+                  $info{name}   =~ /^RT::Condition/         ? '09 Conditions'              :
+                  $info{name}   =~ /^RT(::|$)/              ? '07 Developer Documentation' :
+                  $info{infile} =~ m{/devel/tools/}         ? '20 Utilities (devel/tools)' :
+                                                              '06 Miscellaneous'           ;
+
+    if ($info{infile} =~ m{/(docs|etc)/}) {
+        $info{name} =~ s/_/ /g;
+        $info{name} = join "/", map { ucfirst } split /::/, $info{name};
+    }
+
+    return ($info{name}, $section);
+}
+
+sub write_contents_file {
+    my ($self, $to) = @_;
+    return unless $self->contents_file;
+
+    my $file = join "/", $to, $self->contents_file;
+    open my $index, ">", $file
+        or warn "Unable to open index file '$file': $!\n", return;
+
+    my $pages = $self->_contents;
+    return unless @$pages;
+
+    # Classify
+    my %toc;
+    for my $page (@$pages) {
+        my ($name, $infile, $outfile, $pieces) = @$page;
+
+        my ($title, $section) = $self->classify(
+            name    => $name,
+            infile  => $infile,
+        );
+
+        (my $path = $outfile) =~ s{^\Q$to\E/?}{};
+
+        push @{ $toc{$section} }, {
+            name => $title,
+            path => $path,
+        };
+    }
+
+    # Write out index
+    print $index "<dl class='superindex'>\n";
+
+    for my $key (sort keys %toc) {
+        next unless @{ $toc{$key} };
+
+        (my $section = $key) =~ s/^\d+ //;
+        print $index "<dt>", esc($section), "</dt>\n";
+        print $index "<dd>\n";
+
+        my @sorted = sort {
+            my @names = map { $_->{name} } $a, $b;
+
+            # Sort just the upgrading docs descending within everything else
+            @names = reverse @names
+                if all { /^UPGRADING-/ } @names;
+
+            $names[0] cmp $names[1]
+        } @{ $toc{$key} };
+
+        for my $page (@sorted) {
+            print $index "  <a href='", esc($page->{path}), "'>",
+                                esc($page->{name}),
+                           "</a><br>\n";
+        }
+        print $index "</dd>\n";
+    }
+    print $index '</dl>';
+
+    close $index;
+}
+
+sub esc {
+    Pod::Simple::HTMLBatch::esc(@_);
+}
+
+1;
diff --git a/lib/RT/Pod/Search.pm b/lib/RT/Pod/Search.pm
new file mode 100644 (file)
index 0000000..d6ddd2d
--- /dev/null
@@ -0,0 +1,15 @@
+use strict;
+use warnings;
+
+package RT::Pod::Search;
+use base 'Pod::Simple::Search';
+
+sub new {
+    my $self = shift->SUPER::new(@_);
+       $self->laborious(1)              # Find scripts too
+            ->limit_re(qr/(?<!\.in)$/)  # Filter out .in files
+            ->inc(0);                   # Don't look in @INC
+    return $self;
+}
+
+1;
index 406df92..a942bb6 100644 (file)
@@ -394,6 +394,7 @@ sub Create {
         FinalPriority     => 0,
         DefaultDueIn      => 0,
         Sign              => undef,
+        SignAuto          => undef,
         Encrypt           => undef,
         _RecordTransaction => 1,
         @_
@@ -436,14 +437,11 @@ sub Create {
     }
     $RT::Handle->Commit;
 
-    if ( defined $args{'Sign'} ) {
-        my ($status, $msg) = $self->SetSign( $args{'Sign'} );
-        $RT::Logger->error("Couldn't set attribute 'Sign': $msg")
-            unless $status;
-    }
-    if ( defined $args{'Encrypt'} ) {
-        my ($status, $msg) = $self->SetEncrypt( $args{'Encrypt'} );
-        $RT::Logger->error("Couldn't set attribute 'Encrypt': $msg")
+    for my $attr (qw/Sign SignAuto Encrypt/) {
+        next unless defined $args{$attr};
+        my $set = "Set" . $attr;
+        my ($status, $msg) = $self->$set( $args{$attr} );
+        $RT::Logger->error("Couldn't set attribute '$attr': $msg")
             unless $status;
     }
 
@@ -595,6 +593,32 @@ sub SetSign {
     return ($status, $self->loc('Signing disabled'));
 }
 
+sub SignAuto {
+    my $self = shift;
+    my $value = shift;
+
+    return undef unless $self->CurrentUserHasRight('SeeQueue');
+    my $attr = $self->FirstAttribute('SignAuto') or return 0;
+    return $attr->Content;
+}
+
+sub SetSignAuto {
+    my $self = shift;
+    my $value = shift;
+
+    return ( 0, $self->loc('Permission Denied') )
+        unless $self->CurrentUserHasRight('AdminQueue');
+
+    my ($status, $msg) = $self->SetAttribute(
+        Name        => 'SignAuto',
+        Description => 'Sign auto-generated outgoing messages',
+        Content     => $value,
+    );
+    return ($status, $msg) unless $status;
+    return ($status, $self->loc('Signing enabled')) if $value;
+    return ($status, $self->loc('Signing disabled'));
+}
+
 sub Encrypt {
     my $self = shift;
     my $value = shift;
index 29cff47..8e4ce0e 100644 (file)
@@ -638,6 +638,8 @@ sub __Value {
 
     my $value = $self->SUPER::__Value($field);
 
+    return undef if (!defined $value);
+
     if ( $args{'decode_utf8'} ) {
         if ( !utf8::is_utf8($value) ) {
             utf8::decode($value);
@@ -1413,8 +1415,35 @@ sub _DeleteLink {
 }
 
 
+=head1 LockForUpdate
+
+In a database transaction, gains an exclusive lock on the row, to
+prevent race conditions.  On SQLite, this is a "RESERVED" lock on the
+entire database.
 
+=cut
 
+sub LockForUpdate {
+    my $self = shift;
+
+    my $pk = $self->_PrimaryKey;
+    my $id = @_ ? $_[0] : $self->$pk;
+    $self->_expire if $self->isa("DBIx::SearchBuilder::Record::Cachable");
+    if (RT->Config->Get('DatabaseType') eq "SQLite") {
+        # SQLite does DB-level locking, upgrading the transaction to
+        # "RESERVED" on the first UPDATE/INSERT/DELETE.  Do a no-op
+        # UPDATE to force the upgade.
+        return RT->DatabaseHandle->dbh->do(
+            "UPDATE " .$self->Table.
+                " SET $pk = $pk WHERE 1 = 0");
+    } else {
+        return $self->_LoadFromSQL(
+            "SELECT * FROM ".$self->Table
+                ." WHERE $pk = ? FOR UPDATE",
+            $id,
+        );
+    }
+}
 
 =head2 _NewTransaction  PARAMHASH
 
@@ -1441,6 +1470,11 @@ sub _NewTransaction {
         @_
     );
 
+    my $in_txn = RT->DatabaseHandle->TransactionDepth;
+    RT->DatabaseHandle->BeginTransaction unless $in_txn;
+
+    $self->LockForUpdate;
+
     my $old_ref = $args{'OldReference'};
     my $new_ref = $args{'NewReference'};
     my $ref_type = $args{'ReferenceType'};
@@ -1487,6 +1521,9 @@ sub _NewTransaction {
     if ( RT->Config->Get('UseTransactionBatch') and $transaction ) {
            push @{$self->{_TransactionBatch}}, $trans if $args{'CommitScrips'};
     }
+
+    RT->DatabaseHandle->Commit unless $in_txn;
+
     return ( $transaction, $msg, $trans );
 }
 
@@ -1605,7 +1642,7 @@ sub _AddCustomFieldValue {
             0,
             $self->loc(
                 "Custom field [_1] does not apply to this object",
-                $args{'Field'}
+                ref $args{'Field'} ? $args{'Field'}->id : $args{'Field'}
             )
         );
     }
index 0e0c7a0..40b030c 100644 (file)
@@ -541,7 +541,7 @@ sub _Set {
         }
     }
 
-    return $self->__Set(@_);
+    return $self->SUPER::_Set(@_);
 }
 
 
index 13a4b7d..fa33f7e 100644 (file)
@@ -178,16 +178,6 @@ Commit all of this object's prepared scrips
 sub Commit {
     my $self = shift;
 
-    # RT::Scrips->_SetupSourceObjects will clobber
-    # the CurrentUser, but we need to keep this ticket
-    # so that the _TransactionBatch cache is maintained
-    # and doesn't run twice.  sigh.
-    $self->_StashCurrentUser( TicketObj => $self->{TicketObj} ) if $self->{TicketObj};
-
-    #We're really going to need a non-acled ticket for the scrips to work
-    $self->_SetupSourceObjects( TicketObj      => $self->{'TicketObj'},
-                                TransactionObj => $self->{'TransactionObj'} );
-    
     foreach my $scrip (@{$self->Prepared}) {
         $RT::Logger->debug(
             "Committing scrip #". $scrip->id
@@ -199,8 +189,6 @@ sub Commit {
                         TransactionObj => $self->{'TransactionObj'} );
     }
 
-    # Apply the bandaid.
-    $self->_RestoreCurrentUser( TicketObj => $self->{TicketObj} ) if $self->{TicketObj};
 }
 
 
@@ -221,12 +209,6 @@ sub Prepare {
                  Type           => undef,
                  @_ );
 
-    # RT::Scrips->_SetupSourceObjects will clobber
-    # the CurrentUser, but we need to keep this ticket
-    # so that the _TransactionBatch cache is maintained
-    # and doesn't run twice.  sigh.
-    $self->_StashCurrentUser( TicketObj => $args{TicketObj} ) if $args{TicketObj};
-
     #We're really going to need a non-acled ticket for the scrips to work
     $self->_SetupSourceObjects( TicketObj      => $args{'TicketObj'},
                                 Ticket         => $args{'Ticket'},
@@ -259,10 +241,6 @@ sub Prepare {
 
     }
 
-    # Apply the bandaid.
-    $self->_RestoreCurrentUser( TicketObj => $args{TicketObj} ) if $args{TicketObj};
-
-
     return (@{$self->Prepared});
 
 };
@@ -279,40 +257,6 @@ sub Prepared {
     return ($self->{'prepared_scrips'} || []);
 }
 
-=head2 _StashCurrentUser TicketObj => RT::Ticket
-
-Saves aside the current user of the original ticket that was passed to these scrips.
-This is used to make sure that we don't accidentally leak the RT_System current user
-back to the calling code.
-
-=cut
-
-sub _StashCurrentUser {
-    my $self = shift;
-    my %args = @_;
-
-    $self->{_TicketCurrentUser} = $args{TicketObj}->CurrentUser;
-}
-
-=head2 _RestoreCurrentUser TicketObj => RT::Ticket
-
-Uses the current user saved by _StashCurrentUser to reset a Ticket object
-back to the caller's current user and avoid leaking an RT_System ticket to
-calling code.
-
-=cut
-
-sub _RestoreCurrentUser {
-    my $self = shift;
-    my %args = @_;
-    unless ( $self->{_TicketCurrentUser} ) {
-        RT->Logger->debug("Called _RestoreCurrentUser without a stashed current user object");
-        return;
-    }
-    $args{TicketObj}->CurrentUser($self->{_TicketCurrentUser});
-
-}
-
 =head2  _SetupSourceObjects { TicketObj , Ticket, Transaction, TransactionObj }
 
 Setup a ticket and transaction for this Scrip collection to work with as it runs through the 
@@ -334,14 +278,22 @@ sub _SetupSourceObjects {
             @_ );
 
 
-    if ( $self->{'TicketObj'} = $args{'TicketObj'} ) {
-        # This clobbers the passed in TicketObj by turning it into one
-        # whose current user is RT_System.  Anywhere in the Web UI
-        # currently calling into this is thus susceptable to a privilege
-        # leak; the only current call site is ->Apply, which bandaids
-        # over the top of this by re-asserting the CurrentUser
-        # afterwards.
-        $self->{'TicketObj'}->CurrentUser( $self->CurrentUser );
+    if ( $args{'TicketObj'} ) {
+        # This loads a clean copy of the Ticket object to ensure that we
+        # don't accidentally escalate the privileges of the passed in
+        # ticket (this function can be invoked from the UI).
+        # We copy the TransactionBatch transactions so that Scrips
+        # running against the new Ticket will have access to them. We
+        # use RanTransactionBatch to guard against running
+        # TransactionBatch Scrips more than once.
+        $self->{'TicketObj'} = RT::Ticket->new( $self->CurrentUser );
+        $self->{'TicketObj'}->Load( $args{'TicketObj'}->Id );
+        if ( $args{'TicketObj'}->TransactionBatch ) {
+            # try to ensure that we won't infinite loop if something dies, triggering DESTROY while 
+            # we have the _TransactionBatch objects;
+            $self->{'TicketObj'}->RanTransactionBatch(1);
+            $self->{'TicketObj'}->{'_TransactionBatch'} = $args{'TicketObj'}->{'_TransactionBatch'};
+        }
     }
     else {
         $self->{'TicketObj'} = RT::Ticket->new( $self->CurrentUser );
index a125483..1b4071f 100644 (file)
@@ -110,7 +110,7 @@ sub QueryToSQL {
                             (\w+)  # A straight word
                             (?:\.  # With an optional .foo
                                 ($RE{delimited}{-delim=>q['"]}
-                                |\w+
+                                |[\w-]+  # Allow \w + dashes
                                 ) # Which could be ."foo bar", too
                             )?
                         )
@@ -225,6 +225,11 @@ sub GuessType {
     return "default";
 }
 
+# $_[0] is $self
+# $_[1] is escaped value without surrounding single quotes
+# $_[2] is a boolean of "was quoted by the user?"
+#       ensure this is false before you do smart matching like $_[1] eq "me"
+# $_[3] is escaped subkey, if any (see HandleCf)
 sub HandleDefault   { return subject   => "Subject LIKE '$_[1]'"; }
 sub HandleSubject   { return subject   => "Subject LIKE '$_[1]'"; }
 sub HandleFulltext  { return content   => "Content LIKE '$_[1]'"; }
@@ -242,7 +247,14 @@ sub HandleStatus    {
     }
 }
 sub HandleOwner     {
-    return owner  => (!$_[2] and $_[1] eq "me") ? "Owner.id = '__CurrentUser__'" : "Owner = '$_[1]'";
+    if (!$_[2] and $_[1] eq "me") {
+        return owner => "Owner.id = '__CurrentUser__'";
+    }
+    elsif (!$_[2] and $_[1] =~ /\w+@\w+/) {
+        return owner => "Owner.EmailAddress = '$_[1]'";
+    } else {
+        return owner => "Owner = '$_[1]'";
+    }
 }
 sub HandleWatcher     {
     return watcher => (!$_[2] and $_[1] eq "me") ? "Watcher.id = '__CurrentUser__'" : "Watcher = '$_[1]'";
index 5ee7ecb..47c09a6 100644 (file)
@@ -211,29 +211,35 @@ sub LimitCustomField {
                  @_ );
 
     my $alias = $self->Join(
-       TYPE       => 'left',
-       ALIAS1     => 'main',
-       FIELD1     => 'id',
-       TABLE2     => 'ObjectCustomFieldValues',
-       FIELD2     => 'ObjectId'
+        TYPE       => 'left',
+        ALIAS1     => 'main',
+        FIELD1     => 'id',
+        TABLE2     => 'ObjectCustomFieldValues',
+        FIELD2     => 'ObjectId'
     );
     $self->Limit(
-       ALIAS      => $alias,
-       FIELD      => 'CustomField',
-       OPERATOR   => '=',
-       VALUE      => $args{'CUSTOMFIELD'},
+        ALIAS      => $alias,
+        FIELD      => 'CustomField',
+        OPERATOR   => '=',
+        VALUE      => $args{'CUSTOMFIELD'},
     ) if ($args{'CUSTOMFIELD'});
     $self->Limit(
-       ALIAS      => $alias,
-       FIELD      => 'ObjectType',
-       OPERATOR   => '=',
-       VALUE      => $self->_SingularClass,
+        ALIAS      => $alias,
+        FIELD      => 'ObjectType',
+        OPERATOR   => '=',
+        VALUE      => $self->_SingularClass,
     );
     $self->Limit(
-       ALIAS      => $alias,
-       FIELD      => 'Content',
-       OPERATOR   => $args{'OPERATOR'},
-       VALUE      => $args{'VALUE'},
+        ALIAS      => $alias,
+        FIELD      => 'Content',
+        OPERATOR   => $args{'OPERATOR'},
+        VALUE      => $args{'VALUE'},
+    );
+    $self->Limit(
+        ALIAS => $alias,
+        FIELD => 'Disabled',
+        OPERATOR => '=',
+        VALUE => 0,
     );
 }
 
index 40c73b3..4f96e16 100644 (file)
@@ -539,9 +539,9 @@ sub WipeoutAll
 {
     my $self = $_[0];
 
-    while ( my ($k, $v) = each %{ $self->{'cache'} } ) {
-        next if $v->{'State'} & (WIPED | IN_WIPING);
-        $self->Wipeout( Object => $v->{'Object'} );
+    foreach my $cache_val ( values %{ $self->{'cache'} } ) {
+        next if $cache_val->{'State'} & (WIPED | IN_WIPING);
+        $self->Wipeout( Object => $cache_val->{'Object'} );
     }
 }
 
index 117cc3f..e509454 100644 (file)
@@ -390,6 +390,7 @@ sub _Parse {
 
     # Unfold all headers
     $self->{'MIMEObj'}->head->unfold;
+    $self->{'MIMEObj'}->head->modify(1);
 
     return ( 1, $self->loc("Template parsed") );
 
index 7d69dd6..3e7c910 100644 (file)
@@ -131,14 +131,14 @@ sub import {
 
     if (RT->Config->Get('DevelMode')) { require Module::Refresh; }
 
-    $class->bootstrap_db( %args );
-
     RT::InitPluginPaths();
+    RT::InitClasses();
+
+    $class->bootstrap_db( %args );
 
     __reconnect_rt()
         unless $args{nodb};
 
-    RT::InitClasses();
     RT::InitLogging();
 
     RT->Plugins;
index 76b2e19..bddb48a 100644 (file)
@@ -1058,7 +1058,7 @@ sub AddWatcher {
         return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
             unless $addr;
 
-        if ( lc $self->CurrentUser->UserObj->EmailAddress
+        if ( lc $self->CurrentUser->EmailAddress
             eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
         {
             $args{'PrincipalId'} = $self->CurrentUser->id;
@@ -1239,7 +1239,7 @@ sub DeleteWatcher {
             }
         }
         else {
-            $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
+            $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
             return ( 0,
                      $self->loc('Error in parameters to Ticket->DeleteWatcher') );
         }
@@ -1910,6 +1910,31 @@ sub FirstActiveStatus {
     return $next;
 }
 
+=head2 FirstInactiveStatus
+
+Returns the first inactive status that the ticket could transition to,
+according to its current Queue's lifecycle.  May return undef if there
+is no such possible status to transition to, or we are already in it.
+This is used in resolve action in UnsafeEmailCommands, for instance.
+
+=cut
+
+sub FirstInactiveStatus {
+    my $self = shift;
+
+    my $lifecycle = $self->QueueObj->Lifecycle;
+    my $status = $self->Status;
+    my @inactive = $lifecycle->Inactive;
+    # no change if no inactive statuses in the lifecycle
+    return undef unless @inactive;
+
+    # no change if the ticket is already has first status from the list of inactive
+    return undef if lc $status eq lc $inactive[0];
+
+    my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
+    return $next;
+}
+
 =head2 SetStarted
 
 Takes a date in ISO format or undef
@@ -2095,14 +2120,16 @@ sub Comment {
     }
     $args{'NoteType'} = 'Comment';
 
+    $RT::Handle->BeginTransaction();
     if ($args{'DryRun'}) {
-        $RT::Handle->BeginTransaction();
         $args{'CommitScrips'} = 0;
     }
 
     my @results = $self->_RecordNote(%args);
     if ($args{'DryRun'}) {
         $RT::Handle->Rollback();
+    } else {
+        $RT::Handle->Commit();
     }
 
     return(@results);
@@ -2141,10 +2168,10 @@ sub Correspond {
              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
         return ( 0, $self->loc("Permission Denied"), undef );
     }
+    $args{'NoteType'} = 'Correspond';
 
-    $args{'NoteType'} = 'Correspond'; 
+    $RT::Handle->BeginTransaction();
     if ($args{'DryRun'}) {
-        $RT::Handle->BeginTransaction();
         $args{'CommitScrips'} = 0;
     }
 
@@ -2161,6 +2188,8 @@ sub Correspond {
 
     if ($args{'DryRun'}) {
         $RT::Handle->Rollback();
+    } else {
+        $RT::Handle->Commit();
     }
 
     return (@results);
@@ -2235,7 +2264,9 @@ sub _RecordNote {
     my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
     unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
         $args{'MIMEObj'}->head->set(
-            'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
+            'RT-Message-ID' => Encode::encode_utf8(
+                RT::Interface::Email::GenMessageId( Ticket => $self )
+            )
         );
     }
 
@@ -3257,6 +3288,28 @@ sub SeenUpTo {
     return $txns->First;
 }
 
+=head2 RanTransactionBatch
+
+Acts as a guard around running TransactionBatch scrips.
+
+Should be false until you enter the code that runs TransactionBatch scrips
+
+Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
+
+=cut
+
+sub RanTransactionBatch {
+    my $self = shift;
+    my $val = shift;
+
+    if ( defined $val ) {
+        return $self->{_RanTransactionBatch} = $val;
+    } else {
+        return $self->{_RanTransactionBatch};
+    }
+
+}
+
 
 =head2 TransactionBatch
 
@@ -3293,6 +3346,22 @@ sub ApplyTransactionBatch {
 
 sub _ApplyTransactionBatch {
     my $self = shift;
+
+    return if $self->RanTransactionBatch;
+    $self->RanTransactionBatch(1);
+
+    my $still_exists = RT::Ticket->new( RT->SystemUser );
+    $still_exists->Load( $self->Id );
+    if (not $still_exists->Id) {
+        # The ticket has been removed from the database, but we still
+        # have pending TransactionBatch txns for it.  Unfortunately,
+        # because it isn't in the DB anymore, attempting to run scrips
+        # on it may produce unpredictable results; simply drop the
+        # batched transactions.
+        $RT::Logger->warning("TransactionBatch was fired on a ticket that no longer exists; unable to run scrips!  Call ->ApplyTransactionBatch before shredding the ticket, for consistent results.");
+        return;
+    }
+
     my $batch = $self->TransactionBatch;
 
     my %seen;
@@ -3340,10 +3409,7 @@ sub DESTROY {
         return;
     }
 
-    my $batch = $self->TransactionBatch;
-    return unless $batch && @$batch;
-
-    return $self->_ApplyTransactionBatch;
+    return $self->ApplyTransactionBatch;
 }
 
 
index a5fa74e..6dd23e0 100644 (file)
@@ -431,6 +431,10 @@ sub _LinkLimit {
     my $is_null = 0;
     $is_null = 1 if !$value || $value =~ /^null$/io;
 
+    unless ($is_null) {
+        $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
+    }
+
     my $direction = $meta->[1] || '';
     my ($matchfield, $linkfield) = ('', '');
     if ( $direction eq 'To' ) {
@@ -1617,6 +1621,7 @@ sub _CustomFieldLimit {
                 FIELD      => $column,
                 OPERATOR   => $op,
                 VALUE      => $value,
+                CASESENSITIVE => 0,
                 %rest
             ) );
             $self->_CloseParen;
@@ -1679,6 +1684,7 @@ sub _CustomFieldLimit {
                         FIELD    => 'Content',
                         OPERATOR => $op,
                         VALUE    => $value,
+                        CASESENSITIVE => 0,
                         %rest
                     );
                 }
@@ -1705,6 +1711,7 @@ sub _CustomFieldLimit {
                         OPERATOR        => $op,
                         VALUE           => $value,
                         ENTRYAGGREGATOR => 'AND',
+                        CASESENSITIVE => 0,
                     ) );
                 }
             }
@@ -1714,6 +1721,7 @@ sub _CustomFieldLimit {
                     FIELD    => 'Content',
                     OPERATOR => $op,
                     VALUE    => $value,
+                    CASESENSITIVE => 0,
                     %rest
                 );
 
@@ -1740,6 +1748,7 @@ sub _CustomFieldLimit {
                     OPERATOR        => $op,
                     VALUE           => $value,
                     ENTRYAGGREGATOR => 'AND',
+                    CASESENSITIVE => 0,
                 ) );
                 $self->_CloseParen;
             }
@@ -1796,6 +1805,7 @@ sub _CustomFieldLimit {
                 FIELD      => $column,
                 OPERATOR   => $op,
                 VALUE      => $value,
+                CASESENSITIVE => 0,
             ) );
         }
         else {
@@ -1805,6 +1815,7 @@ sub _CustomFieldLimit {
                 FIELD      => 'Content',
                 OPERATOR   => $op,
                 VALUE      => $value,
+                CASESENSITIVE => 0,
             );
         }
         $self->_SQLLimit(
index fce0459..284a75e 100644 (file)
@@ -91,7 +91,26 @@ sub new {
     return ($self);
 }
 
+=head2 CanonicalizeURI <URI>
 
+Returns the canonical form of the given URI by calling L</FromURI> and then L</URI>.
+
+If the URI is unparseable by FromURI the passed in URI is simply returned untouched.
+
+=cut
+
+sub CanonicalizeURI {
+    my $self = shift;
+    my $uri  = shift;
+    if ($self->FromURI($uri)) {
+        my $canonical = $self->URI;
+        if ($canonical and $uri ne $canonical) {
+            RT->Logger->debug("Canonicalizing URI '$uri' to '$canonical'");
+            $uri = $canonical;
+        }
+    }
+    return $uri;
+}
 
 
 =head2 FromObject <Object>
index 00b230f..72d00f3 100644 (file)
@@ -102,6 +102,7 @@ sub _OverlayAccessible {
           AuthSystem            => { public => 1,  admin => 1 },
           Gecos                 => { public => 1,  admin => 1 },
           PGPKey                => { public => 1,  admin => 1 },
+          PrivateKey            => {               admin => 1 },
 
     }
 }
@@ -932,7 +933,7 @@ sub IsPassword {
         # crypt() output
         return 0 unless crypt(encode_utf8($value), $stored) eq $stored;
     } else {
-        $RT::Logger->warn("Unknown password form");
+        $RT::Logger->warning("Unknown password form");
         return 0;
     }
 
index 6b24643..10dea46 100644 (file)
@@ -30,12 +30,12 @@ if ($Ticket->CurrentUserHasRight('DeleteTicket') &&
     ($Ticket->Status ne 'deleted')) {
         $actions->child(
             isSpam => title => 'IsSpam',
-            path => "Display.html?Status=deleted&Queue=$spamqueuename&id=".$id,
+            path => "Ticket/Display.html?Status=deleted&Queue=$spamqueuename&id=".$id,
        );
 } else {
     $actions->child(
         isSpam => title => 'IsSpam',
-        path => "Display.html?Queue=$spamqueuename&id=".$id,
+        path => "Ticket/Display.html?Queue=$spamqueuename&id=".$id,
     );
 }
         
@@ -44,14 +44,14 @@ if ($Ticket->Queue eq $spamqueue->id) {
     ($Ticket->Status ne 'deleted')) {
         $actions->child(
                 isSpam => title => 'IsSpam',
-                path => "Display.html?Status=deleted&Queue=$spamqueuename&id=".$id,
+                path => "Ticket/Display.html?Status=deleted&Queue=$spamqueuename&id=".$id,
         );
                 }
         }
 
          $actions->child(
              Export => title => 'Export',
-             path => "Export.html?id=".$id,
+             path => "Ticket/Export.html?id=".$id,
          );
 
 
index e8ef014..e4020ba 100644 (file)
@@ -7,6 +7,7 @@ if (   $m->request_comp->path eq '/NoAuth/Login.html'
     && $ARGS{next} )
 {
     my $next = delete $session{'NextPage'}->{ $ARGS{'next'} };
+       $next = $next->{'url'} if ref $next;
     RT::Interface::Web::Redirect( $next || RT->Config->Get('WebURL') );
 }
 </%init>
index 88c4495..613bdb2 100755 (executable)
 use strict;
 use warnings;
 
-use lib '/www/data/rt/rt-perl/current-perl10/share/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib64/perl5';
-
 # fix lib paths, some may be relative
 BEGIN {
     require File::Spec;
index f124187..820af01 100755 (executable)
 use strict;
 use warnings;
 
-use lib '/www/data/rt/rt-perl/current-perl10/share/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib64/perl5';
-
 # fix lib paths, some may be relative
 BEGIN {
     require File::Spec;
index a2aae5e..b2f4af0 100755 (executable)
@@ -217,6 +217,11 @@ sub attachments {
         VALUE => 'deleted'
     );
 
+    # On newer DBIx::SearchBuilder's, indicate that making the query DISTINCT
+    # is unnecessary because the joins won't produce duplicates.  This
+    # drastically improves performance when fetching attachments.
+    $res->{joins_are_distinct} = 1;
+
     return goto_specific(
         suffix => $type,
         error => "Don't know how to find $type attachments",
index 7dc100d..bd2e463 100755 (executable)
@@ -172,7 +172,7 @@ if (caller) {
 require Plack::Runner;
 
 my $is_fastcgi = $0 =~ m/fcgi$/;
-my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) :
+my $r = Plack::Runner->new( $0 =~ /standalone/ ? ( server => 'Standalone' ) :
                             $is_fastcgi        ? ( server => 'FCGI' )
                                                : (),
                             env => 'deployment' );
index 7dc100d..bd2e463 100755 (executable)
@@ -172,7 +172,7 @@ if (caller) {
 require Plack::Runner;
 
 my $is_fastcgi = $0 =~ m/fcgi$/;
-my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) :
+my $r = Plack::Runner->new( $0 =~ /standalone/ ? ( server => 'Standalone' ) :
                             $is_fastcgi        ? ( server => 'FCGI' )
                                                : (),
                             env => 'deployment' );
index 64addfd..38d37c7 100755 (executable)
@@ -74,7 +74,7 @@ should wipeout.
 
 =head2 --sqldump <filename>
 
-Outputs INSERT queries into file. This dump can be used to restore data
+Outputs INSERT queiries into file. This dump can be used to restore data
 after wiping out.
 
 By default creates files
@@ -107,13 +107,13 @@ L<RT::Shredder>
 
 =cut
 
+use strict;
+use warnings FATAL => 'all';
+
 use lib '/www/data/rt/rt-perl/current-perl10/share/perl5';
 use lib '/www/data/rt/rt-perl/current-perl10/lib/perl5';
 use lib '/www/data/rt/rt-perl/current-perl10/lib64/perl5';
 
-use strict;
-use warnings FATAL => 'all';
-
 # fix lib paths, some may be relative
 BEGIN {
     require File::Spec;
index fd754aa..769cc27 100755 (executable)
@@ -56,9 +56,10 @@ no warnings qw(numeric redefine);
 use Getopt::Long;
 my %args;
 my %deps;
+my @orig_argv = @ARGV;
 GetOptions(
     \%args,                               'v|verbose',
-    'install',                            'with-MYSQL',
+    'install!',                           'with-MYSQL',
     'with-POSTGRESQL|with-pg|with-pgsql', 'with-SQLITE',
     'with-ORACLE',                        'with-FASTCGI',
     'with-MODPERL1',                      'with-MODPERL2',
@@ -74,6 +75,7 @@ GetOptions(
     'with-DASHBOARDS',
     'with-USERLOGO',
     'with-SSL-MAILGATE',
+    'with-HTML-DOC',
 
     'download=s',
     'repository=s',
@@ -103,6 +105,7 @@ my %default = (
     'with-DASHBOARDS' => 1,
     'with-USERLOGO' => 1,
     'with-SSL-MAILGATE' => 0,
+    'with-HTML-DOC' => 0,
 );
 $args{$_} = $default{$_} foreach grep !exists $args{$_}, keys %default;
 
@@ -293,7 +296,7 @@ Test::LongString
 .
 
 $deps{'FASTCGI'} = [ text_to_hash( << '.') ];
-FCGI
+FCGI 0.74
 FCGI::ProcManager
 .
 
@@ -344,7 +347,7 @@ URI 1.59
 
 $deps{'GRAPHVIZ'} = [ text_to_hash( << '.') ];
 GraphViz
-IPC::Run
+IPC::Run 0.90
 .
 
 $deps{'GD'} = [ text_to_hash( << '.') ];
@@ -357,8 +360,14 @@ $deps{'USERLOGO'} = [ text_to_hash( << '.') ];
 Convert::Color
 .
 
+$deps{'HTML-DOC'} = [ text_to_hash( <<'.') ];
+Pod::Simple 3.17
+HTML::Entities
+.
+
 my %AVOID = (
     'DBD::Oracle' => [qw(1.23)],
+    'Email::Address' => [qw(1.893 1.894)],
 );
 
 if ($args{'download'}) {
@@ -403,7 +412,12 @@ foreach my $type (sort grep $args{$_}, keys %args) {
     $Missing_By_Type{$type} = \%missing if keys %missing;
 }
 
-conclude(%Missing_By_Type);
+if ( $args{'install'} && keys %Missing_By_Type ) {
+    exec($0, @orig_argv, '--no-install');
+}
+else {
+    conclude(%Missing_By_Type);
+}
 
 sub test_deps {
     my @deps = @_;
diff --git a/sbin/rt-validate-aliases b/sbin/rt-validate-aliases
new file mode 100755 (executable)
index 0000000..2484b41
--- /dev/null
@@ -0,0 +1,343 @@
+#!/usr/bin/perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+#                                          <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+use Text::ParseWords qw//;
+use Getopt::Long;
+
+BEGIN { # BEGIN RT CMD BOILERPLATE
+    require File::Spec;
+    require Cwd;
+    my @libs = ("lib", "local/lib");
+    my $bin_path;
+
+    for my $lib (@libs) {
+        unless ( File::Spec->file_name_is_absolute($lib) ) {
+            $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1];
+            $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
+        }
+        unshift @INC, $lib;
+    }
+}
+
+require RT;
+RT::LoadConfig();
+RT::Init();
+
+my ($PREFIX, $URL, $HOST) = ("");
+GetOptions(
+    "prefix|p=s" => \$PREFIX,
+    "url|u=s"    => \$URL,
+    "host|h=s"   => \$HOST,
+);
+
+unless (@ARGV) {
+    @ARGV = grep {-f} ("/etc/aliases",
+                       "/etc/mail/aliases",
+                       "/etc/postfix/aliases");
+    die "Can't determine aliases file to parse!"
+        unless @ARGV;
+}
+
+my %aliases = parse_lines();
+unless (%aliases) {
+    warn "No mailgate aliases found in @ARGV";
+    exit;
+}
+
+my %seen;
+my $global_mailgate;
+for my $address (sort keys %aliases) {
+    my ($mailgate, $opts, $extra) = @{$aliases{$address}};
+    my %opts = %{$opts};
+
+    next if $opts{url} and $URL and $opts{url} !~ /\Q$URL\E/;
+
+    if ($mailgate !~ /^\|/) {
+        warn "Missing the leading | on alias $address\n";
+        $mailgate = "|$mailgate";
+    }
+    if (($global_mailgate ||= $mailgate) ne $mailgate) {
+        warn "Unexpected mailgate for alias $address -- expected $global_mailgate, got $mailgate\n";
+    }
+
+    if (not defined $opts{action}) {
+        warn "Missing --action parameter for alias $address\n";
+    } elsif ($opts{action} !~ /^(correspond|comment)$/) {
+        warn "Invalid --action parameter for alias $address: $opts{action}\n"
+    }
+
+    my $queue = RT::Queue->new( RT->SystemUser );
+    if (not defined $opts{queue}) {
+        warn "Missing --queue parameter for alias $address\n";
+    } else {
+        $queue->Load( $opts{queue} );
+        if (not $queue->id) {
+            warn "Invalid --queue parameter for alias $address: $opts{queue}\n";
+        } elsif ($queue->Disabled) {
+            warn "Disabled --queue given for alias $address: $opts{queue}\n";
+        }
+    }
+
+    if (not defined $opts{url}) {
+        warn "Missing --url parameter for alias $address\n";
+    } #XXX: Test connectivity and/or https certs?
+
+    if ($queue->id and $opts{action} =~ /^(correspond|comment)$/) {
+        push @{$seen{lc $queue->Name}{$opts{action}}}, $address;
+    }
+
+    warn "Unknown extra arguments for alias $address: @{$extra}\n"
+        if @{$extra};
+}
+
+# Check the global settings
+my %global;
+for my $action (qw/correspond comment/) {
+    my $setting = ucfirst($action) . "Address";
+    my $value = RT->Config->Get($setting);
+    if (not defined $value) {
+        warn "$setting is not set!\n";
+        next;
+    }
+    my ($local,$host) = lc($value) =~ /(.*?)\@(.*)/;
+    next if $HOST and $host !~ /\Q$HOST\E/;
+    $local = "$PREFIX$local" unless exists $aliases{$local};
+
+    $global{$setting} = $local;
+    if (not exists $aliases{$local}) {
+        warn "$setting $value does not exist in aliases!\n"
+    } elsif ($aliases{$local}[1]{action} ne $action) {
+        warn "$setting $value is a $aliases{$local}[1]{action} in aliases!"
+    }
+}
+warn "CorrespondAddress and CommentAddress are the same!\n"
+    if RT->Config->Get("CorrespondAddress") eq RT->Config->Get("CommentAddress");
+
+
+# Go through the queues, one at a time
+my $queues = RT::Queues->new( RT->SystemUser );
+$queues->UnLimit;
+while (my $q = $queues->Next) {
+    my $qname = $q->Name;
+    for my $action (qw/correspond comment/) {
+        my $setting = ucfirst($action) . "Address";
+        my $value = $q->$setting;
+
+        if (not $value) {
+            my @other = grep {$_ ne $global{$setting}} @{$seen{lc $q->Name}{$action} || []};
+            warn "CorrespondAddress not set on $qname, but in aliases as "
+                .join(" and ", @other) . "\n" if @other;
+            next;
+        }
+
+        if ($action eq "comment" and $q->CorrespondAddress
+                and $q->CorrespondAddress eq $q->CommentAddress) {
+            warn "CorrespondAddress and CommentAddress are set the same on $qname\n";
+            next;
+        }
+
+        my ($local, $host) = lc($value) =~ /(.*?)\@(.*)/;
+        next if $HOST and $host !~ /\Q$HOST\E/;
+        $local = "$PREFIX$local" unless exists $aliases{$local};
+
+        my @other = @{$seen{lc $q->Name}{$action} || []};
+        if (not exists $aliases{$local}) {
+            if (@other) {
+                warn "$setting $value on $qname does not exist in aliases -- typo'd as "
+                    .join(" or ", @other) . "?\n";
+            } else {
+                warn "$setting $value on $qname does not exist in aliases!\n"
+            }
+            next;
+        }
+
+        my %opt = %{$aliases{$local}[1]};
+        if ($opt{action} ne $action) {
+            warn "$setting address $value on $qname is a $opt{action} in aliases!\n"
+        }
+        if (lc $opt{queue} ne lc $q->Name and $action ne "comment") {
+            warn "$setting address $value on $qname points to queue $opt{queue} in aliases!\n";
+        }
+
+        @other = grep {$_ ne $local} @other;
+        warn "Extra aliases for queue $qname: ".join(",",@other)."\n"
+            if @other;
+    }
+}
+
+
+sub parse_lines {
+    local @ARGV = @ARGV;
+
+    my %aliases;
+    my $line = "";
+    for (<>) {
+        next unless /\S/;
+        next if /^#/;
+        chomp;
+        if (/^\s+/) {
+            $line .= $_;
+        } else {
+            add_line($line, \%aliases);
+            $line = $_;
+        }
+    }
+    add_line($line, \%aliases);
+
+    expand(\%aliases);
+    filter_mailgate(\%aliases);
+
+    return %aliases;
+}
+
+sub expand {
+    my ($data) = @_;
+
+    for (1..100) {
+        my $expanded = 0;
+        for my $address (sort keys %{$data}) {
+            my @new;
+            for my $part (@{$data->{$address}}) {
+                if (m!^[|/]! or not $data->{$part}) {
+                    push @new, $part;
+                } else {
+                    $expanded++;
+                    push @new, @{$data->{$part}};
+                }
+            }
+            $data->{$address} = \@new;
+        }
+        return unless $expanded;
+    }
+    warn "Recursion limit exceeded -- cycle in aliases?\n";
+}
+
+sub filter_mailgate {
+    my ($data) = @_;
+
+    for my $address (sort keys %{$data}) {
+        my @parts = @{delete $data->{$address}};
+
+        my @pipes = grep {m!^\|?.*?/rt-mailgate\b!} @parts;
+        next unless @pipes;
+
+        my $pipe = shift @pipes;
+        warn "More than one rt-mailgate pipe for alias: $address\n"
+            if @pipes;
+
+        my @args = Text::ParseWords::shellwords($pipe);
+
+        # We allow "|/random-other-command /opt/rt4/bin/rt-mailgate ...",
+        # we just need to strip off enough
+        my $index = 0;
+        $index++ while $args[$index] !~ m!/rt-mailgate!;
+        my $mailgate = join(' ', splice(@args,0,$index+1));
+
+        my %opts;
+        local @ARGV = @args;
+        Getopt::Long::Configure( "pass_through" ); # Allow unknown options
+        my $ret = eval {
+            GetOptions( \%opts, "queue=s", "action=s", "url=s",
+                        "jar=s", "debug", "extension=s",
+                        "timeout=i", "verify-ssl!", "ca-file=s",
+                    );
+            1;
+        };
+        warn "Failed to parse options for $address: $@" unless $ret;
+        next unless %opts;
+
+        $data->{lc $address} = [$mailgate, \%opts, [@ARGV]];
+    }
+}
+
+sub add_line {
+    my ($line, $data) = @_;
+    return unless $line =~ /\S/;
+
+    my ($name, $parts) = parse_line($line);
+    return unless defined $name;
+
+    if (defined $data->{$name}) {
+        warn "Duplicate definition for alias $name\n";
+        return;
+    }
+
+    $data->{lc $name} = $parts;
+}
+
+sub parse_line {
+    my $re_name      = qr/\S+/;
+    # Intentionally accept pipe-like aliases with a missing | -- we deal with them later
+    my $re_quoted_pipe    = qr/"\|?[^\\"]*(?:\\[\\"][^\\"]*)*"/;
+    my $re_nonquoted_pipe = qr/\|[^\s,]+/;
+    my $re_pipe      = qr/(?:$re_quoted_pipe|$re_nonquoted_pipe)/;
+    my $re_path      = qr!/[^,\s]+!;
+    my $re_address   = qr![^|/,\s][^,\s]*!;
+    my $re_value     = qr/(?:$re_pipe|$re_path|$re_address)/;
+    my $re_values    = qr/(?:$re_value(?:\s*,\s*$re_value)*)/;
+
+    my ($line) = @_;
+    if ($line =~ /^($re_name):\s*($re_values)/) {
+        my ($name, $all_parts) = ($1, $2);
+        my @parts;
+        while ($all_parts =~ s/^(?:\s*,\s*)?($re_value)//) {
+            my $part = $1;
+            if ($part =~ /^"/) {
+                $part =~ s/^"//; $part =~ s/"$//;
+                $part =~ s/\\(.)/$1/g;
+            }
+            push @parts, $part;
+        }
+        return $name, [@parts];
+    } else {
+        warn "Parse failure, line $. of $ARGV: $line\n";
+        return ();
+    }
+}
index 7dc100d..bd2e463 100755 (executable)
@@ -172,7 +172,7 @@ if (caller) {
 require Plack::Runner;
 
 my $is_fastcgi = $0 =~ m/fcgi$/;
-my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) :
+my $r = Plack::Runner->new( $0 =~ /standalone/ ? ( server => 'Standalone' ) :
                             $is_fastcgi        ? ( server => 'FCGI' )
                                                : (),
                             env => 'deployment' );
index 148c98e..4491a71 100644 (file)
@@ -162,10 +162,7 @@ MaybeRedirectForResults(
 
 push @results, @warnings;
 
-unless ($Group->Disabled()) {
-    $EnabledChecked ='checked="checked"';
-}
-
+$EnabledChecked = ( $Group->Disabled() ? '' : 'checked="checked"' );
 
 </%INIT>
 
index 5682eee..c2cf094 100644 (file)
@@ -51,7 +51,7 @@
 
 
 
-<form action="<%RT->Config->Get('WebPath')%>/Admin/Queues/Modify.html" name="ModifyQueue" method="post">
+<form action="<%RT->Config->Get('WebPath')%>/Admin/Queues/Modify.html" name="ModifyQueue" method="post" enctype="multipart/form-data">
 <input type="hidden" class="hidden" name="SetEnabled" value="1" />
 <input type="hidden" class="hidden" name="id" value="<% $Create? 'new': $QueueObj->Id %>" />
 
 <td align="right"><input type="checkbox" class="checkbox" name="Encrypt" value="1" <% $QueueObj->Encrypt? 'checked="checked"': '' |n%> /></td>
 <td><&|/l&>Encrypt by default</&></td>
 </tr>
+<tr><td align="right"><input type="checkbox" class="checkbox" name="SignAuto" value="1" <% $QueueObj->SignAuto? 'checked="checked"': '' |n%> /></td>
+<td colspan="3"><&|/l_unsafe, "<b>","</b>","<i>","</i>"&>Sign all auto-generated mail.  [_1]Caution[_2]: Enabling this option alters the signature from providing [_3]authentication[_4] to providing [_3]integrity[_4].</&></td></tr>
 % }
 
 <tr><td align="right"><input type="checkbox" class="checkbox" name="Enabled" value="1" <%$EnabledChecked|n%> /></td>
@@ -181,13 +183,13 @@ unless ($Create) {
 if ( $QueueObj->Id ) {
     $title = loc('Configuration for queue [_1]', $QueueObj->Name );
     my @attribs= qw(Description CorrespondAddress CommentAddress Name
-        InitialPriority FinalPriority DefaultDueIn Sign Encrypt Lifecycle SubjectTag Disabled);
+        InitialPriority FinalPriority DefaultDueIn Sign SignAuto Encrypt Lifecycle SubjectTag Disabled);
 
     # we're asking about enabled on the web page but really care about disabled
     if ( $SetEnabled ) {
         $Disabled = $ARGS{'Disabled'} = $Enabled? 0: 1;
         $ARGS{$_} = 0 foreach grep !defined $ARGS{$_} || !length $ARGS{$_},
-            qw(Sign Encrypt Disabled);
+            qw(Sign SignAuto Encrypt Disabled);
     }
 
     $m->callback(
index 90408e4..ee58c44 100644 (file)
@@ -64,7 +64,7 @@
 <& /Widgets/Form/Select,
     Name         => 'PrivateKey',
     Description  => loc('Private Key'),
-    Values       => [ map $_->{'Key'}, @{ $keys_meta{'info'} } ],
+    Values       => \@potential_keys,
     CurrentValue => $UserObj->PrivateKey,
     DefaultLabel => loc('No private key'),
 &>
@@ -91,7 +91,8 @@ unless ( $UserObj->id ) {
 $id = $ARGS{'id'} = $UserObj->id;
 
 my $email = $UserObj->EmailAddress;
-my %keys_meta = RT::Crypt::GnuPG::GetKeysForSigning( $email, 'force' );
+my %keys_meta = RT::Crypt::GnuPG::GetKeysForSigning( $email );
+my @potential_keys = map $_->{'Key'}, @{ $keys_meta{'info'} || [] };
 
 $ARGS{'PrivateKey'} = $m->comp('/Widgets/Form/Select:Process',
     Name      => 'PrivateKey',
@@ -100,8 +101,14 @@ $ARGS{'PrivateKey'} = $m->comp('/Widgets/Form/Select:Process',
 );
 
 if ( $Update ) {
-    my ($status, $msg) = $UserObj->SetPrivateKey( $ARGS{'PrivateKey'} );
-    push @results, $msg;
+    if (not $ARGS{'PrivateKey'} or grep {$_ eq $ARGS{'PrivateKey'}} @potential_keys) {
+        if (($ARGS{'PrivateKey'}||'') ne ($UserObj->PrivateKey||'')) {
+            my ($status, $msg) = $UserObj->SetPrivateKey( $ARGS{'PrivateKey'} );
+            push @results, $msg;
+        }
+    } else {
+        push @results, loc("Invalid key [_1] for address '[_2]'", $ARGS{'PrivateKey'}, $email);
+    }
 }
 
 my $title = loc("[_1]'s GnuPG keys",$UserObj->Name);
index d2061da..169c25c 100644 (file)
@@ -74,7 +74,7 @@ $tickets->LimitOwner( VALUE => $session{'CurrentUser'}->Id );
 
 # also consider AdminCcs as potential approvers.
 my $group_tickets = RT::Tickets->new( $session{'CurrentUser'} );
-$group_tickets->LimitWatcher( VALUE => $session{'CurrentUser'}->UserObj->EmailAddress, TYPE => 'AdminCc' );
+$group_tickets->LimitWatcher( VALUE => $session{'CurrentUser'}->EmailAddress, TYPE => 'AdminCc' );
 
 my $created_before = RT::Date->new( $session{'CurrentUser'} );
 my $created_after = RT::Date->new( $session{'CurrentUser'} );
index a057706..3e0f2c6 100644 (file)
 %#
 %# END BPS TAGGED BLOCK }}}
 <%init>
-$m->call_next(%ARGS) if $session{'CurrentUser'}->UserObj->HasRight(
+if ( $session{'CurrentUser'}->UserObj->HasRight(
     Right => 'ShowApprovalsTab',
     Object => $RT::System,
-);
+) ) {
+    $m->call_next(%ARGS);
+}
+else {
+    Abort("No permission to view approval");
+}
 </%init>
index 3669e46..3a57102 100644 (file)
 <&|/l&>Recipient</&>:
 </td><td class="value">
 <input name="Recipient" id="Recipient" size="30" value="<%$fields{Recipient} ? $fields{Recipient} : ''%>" />
-<div class="hints"><% loc("Leave blank to send to your current email address ([_1])", $session{'CurrentUser'}->UserObj->EmailAddress) %></div>
+<div class="hints"><% loc("Leave blank to send to your current email address ([_1])", $session{'CurrentUser'}->EmailAddress) %></div>
 </td></tr>
 </table>
 </&>
index 4893c12..a3c1943 100644 (file)
 
 % my $strong_start = "<strong>";
 % my $strong_end   = "</strong>";
-<p><&|/l_unsafe, $strong_start, $strong_end, $Reason &>RT has detected a possible [_1]cross-site request forgery[_2] for this request, because [_3].  This is possibly caused by a malicious attacker trying to perform actions against RT on your behalf. If you did not initiate this request, then you should alert your security team.</&></p>
+<p><&|/l_unsafe, $strong_start, $strong_end, $Reason, $action &>RT has detected a possible [_1]cross-site request forgery[_2] for this request, because [_3].  A malicious attacker may be trying to [_1][_4][_2] on your behalf. If you did not initiate this request, then you should alert your security team.</&></p>
 
 % my $start = qq|<strong><a href="$url_with_token">|;
 % my $end   = qq|</a></strong>|;
-<p><&|/l_unsafe, $escaped_path, $start, $end &>If you really intended to visit [_1], then [_2]click here to resume your request[_3].</&></p>
+<p><&|/l_unsafe, $escaped_path, $action, $start, $end &>If you really intended to visit [_1] and [_2], then [_3]click here to resume your request[_4].</&></p>
 
 <& /Elements/Footer, %ARGS &>
 % $m->abort;
@@ -71,4 +71,6 @@ $escaped_path = "<tt>$escaped_path</tt>";
 
 my $url_with_token = URI->new($OriginalURL);
 $url_with_token->query_form([CSRF_Token => $Token]);
+
+my $action = RT::Interface::Web::PotentialPageAction($OriginalURL) || loc("perform actions");
 </%INIT>
index 87fd61b..7295e3f 100644 (file)
@@ -116,7 +116,7 @@ my $COLUMN_MAP = {
     CheckBox => {
         title => sub {
             my $name = $_[1] || 'SelectedTickets';
-            my $checked = $m->request_args->{ $name .'All' }? 'checked="checked"': '';
+            my $checked = $DECODED_ARGS->{ $name .'All' }? 'checked="checked"': '';
 
             return \qq{<input type="checkbox" name="}, $name, \qq{All" value="1" $checked
                               onclick="setCheckbox(this.form, },
@@ -128,9 +128,9 @@ my $COLUMN_MAP = {
 
             my $name = $_[2] || 'SelectedTickets';
             return \qq{<input type="checkbox" name="}, $name, \qq{" value="$id" checked="checked" />}
-                if $m->request_args->{ $name . 'All'};
+                if $DECODED_ARGS->{ $name . 'All'};
 
-            my $arg = $m->request_args->{ $name };
+            my $arg = $DECODED_ARGS->{ $name };
             my $checked = '';
             if ( $arg && ref $arg ) {
                 $checked = 'checked="checked"' if grep $_ == $id, @$arg;
@@ -147,7 +147,7 @@ my $COLUMN_MAP = {
             my $id = $_[0]->id;
 
             my $name = $_[2] || 'SelectedTicket';
-            my $arg = $m->request_args->{ $name };
+            my $arg = $DECODED_ARGS->{ $name };
             my $checked = '';
             $checked = 'checked="checked"' if $arg && $arg == $id;
             return \qq{<input type="radio" name="}, $name, \qq{" value="$id" $checked />};
index b74c484..8b87fd4 100644 (file)
@@ -71,7 +71,7 @@ if ( $Object && $Object->id ) {
 
 # Always fill $Default with submited values if it's empty
 if ( ( !defined $Default || !length $Default ) && $DefaultsFromTopArguments ) {
-    my %TOP = $m->request_args;
+    my %TOP = %$DECODED_ARGS;
     $Default = $TOP{ $NamePrefix .$CustomField->Id . '-Values' }
             || $TOP{ $NamePrefix .$CustomField->Id . '-Value' };
 }
index 0ae0f84..2f3f103 100644 (file)
@@ -129,12 +129,16 @@ if ( $self->{'Sign'} ) {
     $QueueObj ||= $TicketObj->QueueObj
         if $TicketObj;
 
-    my $address = $self->{'SignUsing'};
-    $address ||= ($self->{'UpdateType'} && $self->{'UpdateType'} eq "private")
+    my $private = $session{'CurrentUser'}->UserObj->PrivateKey || '';
+    my $queue = ($self->{'UpdateType'} && $self->{'UpdateType'} eq "private")
         ? ( $QueueObj->CommentAddress || RT->Config->Get('CommentAddress') )
         : ( $QueueObj->CorrespondAddress || RT->Config->Get('CorrespondAddress') );
 
-    unless ( RT::Crypt::GnuPG::DrySign( $address ) ) {
+    my $address = $self->{'SignUsing'} || $queue;
+    if ($address ne $private and $address ne $queue) {
+        push @{ $self->{'GnuPGCanNotSignAs'} ||= [] }, $address;
+        $checks_failure = 1;
+    } elsif ( not RT::Crypt::GnuPG::DrySign( $address ) ) {
         push @{ $self->{'GnuPGCanNotSignAs'} ||= [] }, $address;
         $checks_failure = 1;
     } else {
index 4a6ac26..ccdf0f2 100644 (file)
@@ -96,7 +96,7 @@
 % $m->callback( %ARGS, CallbackName => 'Head' );
 
 </head>
-  <body class="<% lc $style %>" <% $id && qq[id="comp-$id"] |n %>>
+  <body class="<% lc $style %><% RT->Config->Get("UseSideBySideLayout", $session{'CurrentUser'}) ? ' sidebyside' : '' %>" <% $id && qq[id="comp-$id"] |n %>>
 
 % if ($ShowBar) {
 <& /Elements/Logo, %ARGS &>
index 28788db..d5741f4 100644 (file)
@@ -67,7 +67,7 @@ $onload => undef
 % }
 
 % if ( $RichText and RT->Config->Get('MessageBoxRichText',  $session{'CurrentUser'})) {
-    jQuery().ready(function ()  { ReplaceAllTextareas(<%$m->request_args->{'CKeditorEncoded'} || 0 |n,j%>) });
+    jQuery().ready(function ()  { ReplaceAllTextareas(<%$DECODED_ARGS->{'CKeditorEncoded'} || 0 |n,j%>) });
 % }
 --></script>
 <%ARGS>
index 999d3fe..8929ff7 100644 (file)
@@ -65,7 +65,7 @@ if ( ref( $session{'Actions'}{''} ) eq 'ARRAY' ) {
     unshift @actions, @{ delete $session{'Actions'}{''} };
 }
 
-my $actions_pointer = $m->request_args->{'results'};
+my $actions_pointer = $DECODED_ARGS->{'results'};
 
 if ($actions_pointer &&  ref( $session{'Actions'}->{$actions_pointer} ) eq 'ARRAY' ) {
     unshift @actions, @{ delete $session{'Actions'}->{$actions_pointer} };
index b86bfef..b3f1a24 100644 (file)
@@ -61,6 +61,8 @@
 <div id="login-box">
 <&| /Widgets/TitleBox, title => loc('Login'), titleright => $RT::VERSION, hideable => 0 &>
 
+<& LoginRedirectWarning, %ARGS &>
+
 % unless (RT->Config->Get('WebExternalAuth') and !RT->Config->Get('WebFallbackToInternalAuth')) {
 <form id="login" name="login" method="post" action="<% RT->Config->Get('WebPath') %>/NoAuth/Login.html">
 
diff --git a/share/html/Elements/LoginRedirectWarning b/share/html/Elements/LoginRedirectWarning
new file mode 100644 (file)
index 0000000..891e381
--- /dev/null
@@ -0,0 +1,20 @@
+<%args>
+$next => undef
+</%args>
+<%init>
+return unless $next;
+
+my $destination = RT::Interface::Web::FetchNextPage($next);
+return unless ref $destination and $destination->{'HasSideEffects'};
+
+my $consequence = RT::Interface::Web::PotentialPageAction($destination->{'url'}) || loc("perform actions");
+   $consequence = $m->interp->apply_escapes($consequence => "h");
+</%init>
+<div class="redirect-warning">
+  <p>
+    <&|/l&>After logging in you'll be sent to your original destination:</&>
+    <tt title="<% $destination->{'url'} %>"><% $destination->{'url'} %></tt>
+    <&|/l_unsafe, "<strong>$consequence</strong>" &>which may [_1] on your behalf.</&>
+  </p>
+  <p><&|/l&>If this is not what you expect, leave this page now without logging in.</&></p>
+</div>
index 61995e0..69227bf 100644 (file)
@@ -46,7 +46,7 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 <textarea autocomplete="off" class="messagebox" <% $width_attr %>="<% $Width %>" rows="<% $Height %>" <% $wrap_type |n %> name="<% $Name %>" id="<% $Name %>">\
-% $m->comp('/Articles/Elements/IncludeArticle', %ARGS);
+% $m->comp('/Articles/Elements/IncludeArticle', %ARGS) if $IncludeArticle;
 % $m->callback( %ARGS, SignatureRef => \$signature );
 <% $Default || '' %><% $message %><% $signature %></textarea>
 % $m->callback( %ARGS, CallbackName => 'AfterTextArea' );
@@ -89,4 +89,5 @@ $Width            => RT->Config->Get('MessageBoxWidth', $session{'CurrentUser'}
 $Height           => RT->Config->Get('MessageBoxHeight', $session{'CurrentUser'} ) || 15
 $Wrap             => RT->Config->Get('MessageBoxWrap', $session{'CurrentUser'} ) || 'SOFT'
 $IncludeSignature => RT->Config->Get('MessageBoxIncludeSignature');
+$IncludeArticle   => 1;
 </%ARGS>
index 09f274f..f649d28 100644 (file)
@@ -122,9 +122,13 @@ my $statuses = {};
 
 use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( RT->SystemUser );
-my $query = @queues
-    ? join(' OR ', map "Queue = ".$_->{id}, @queues)
-    : 'id < 0';
+my $query =
+    "(".
+    join(" OR ", map {s{(['\\])}{\\$1}g; "Status = '$_'"} @statuses) #'
+    .") AND (".
+    join(' OR ', map "Queue = ".$_->{id}, @queues)
+    .")";
+$query = 'id < 0' unless @queues;
 $report->SetupGroupings( Query => $query, GroupBy => [qw(Status Queue)] );
 
 while ( my $entry = $report->Next ) {
index ecb219d..b043984 100644 (file)
@@ -118,7 +118,7 @@ my $COLUMN_MAP = {
     RemoveCheckBox => {
         title => sub {
             my $name = 'RemoveCustomField';
-            my $checked = $m->request_args->{ $name .'All' }? 'checked="checked"': '';
+            my $checked = $DECODED_ARGS->{ $name .'All' }? 'checked="checked"': '';
 
             return \qq{<input type="checkbox" name="}, $name, \qq{All" value="1" $checked
                               onclick="setCheckbox(this.form, },
@@ -130,7 +130,7 @@ my $COLUMN_MAP = {
             return '' if $_[0]->IsApplied;
 
             my $name = 'RemoveCustomField';
-            my $arg = $m->request_args->{ $name };
+            my $arg = $DECODED_ARGS->{ $name };
 
             my $checked = '';
             if ( $arg && ref $arg ) {
index 44beee0..4f1df60 100644 (file)
@@ -56,7 +56,7 @@
 
 <%INIT>
 my @types;
-if ($Scope =~ 'queue') {
+if ($Scope =~ /queue/) {
    @types = RT::Queue->ManageableRoleGroupTypes;
 }
 else { 
index 95cc21a..184316e 100644 (file)
@@ -50,6 +50,7 @@
 <%INIT>
 
 my $request_path = $HTML::Mason::Commands::r->path_info;
+$request_path =~ s!/{2,}!/!g;
 
 my $query_string = sub {
     my %args = @_;
@@ -836,7 +837,7 @@ my $build_selfservice_nav = sub {
     } elsif ( $queue_id ) {
         Menu->child( new => title => loc('New ticket'), path => '/SelfService/Create.html?Queue=' . $queue_id );
     }
-    my $tickets = Menu->child( tickets => title => loc('Tickets'));
+    my $tickets = Menu->child( tickets => title => loc('Tickets'), path => '/SelfService/' );
     $tickets->child( open   => title => loc('Open tickets'),   path => '/SelfService/' );
     $tickets->child( closed => title => loc('Closed tickets'), path => '/SelfService/Closed.html' );
 
index dbc2d88..c2b92c1 100644 (file)
@@ -116,6 +116,9 @@ foreach (split /\s*,\s*/, $exclude) {
 
 my @suggestions;
 
+$users->Limit( FIELD => $return, OPERATOR => '!=', VALUE => '' );
+$users->Limit( FIELD => $return, OPERATOR => 'IS NOT', VALUE => 'NULL', ENTRYAGGREGATOR => 'AND'  );
+
 while ( my $user = $users->Next ) {
     next if $user->id == RT->SystemUser->id
          or $user->id == RT->Nobody->id;
index ed6623c..f90ac9f 100644 (file)
 
 .titlebox .titlebox-title {
  position: relative;
- /* This is for [rt3 #19044]. Move it to an IE-specific file if it causes
-  * problems. If we remove CSS3PIE, it can also probably go away, although it
-  * probably won't hurt. */
- z-index: 1;
 }
 
 .titlebox .titlebox-title a {
index 4d069d9..7b573f7 100644 (file)
@@ -87,8 +87,7 @@ div#ticket-history {
  float: left;
  margin: 0.25em 0.70em 0.25em 0.25em;
  width: 1em;
- height: 1.25em;
- padding: 0.75em 0 0 0;
+ padding: 0;
  border-right: 1px solid #999;
  border-bottom: 1px solid #999;
  -moz-border-radius-bottomright: 0.25em;
@@ -100,6 +99,16 @@ div#ticket-history {
 
 div#ticket-history span.type a {
  color: #fff;
+ padding-top: 0.75em;
+ display: block;
+}
+
+#ticket-history a#lasttrans {
+    display: inline;
+    height: 0;
+    width: 0;
+    padding: 0;
+    margin: 0;
 }
 
 
index 912ac55..9610cd5 100644 (file)
@@ -54,6 +54,7 @@
  margin-left: 1em;
  -moz-border-radius: 0.5em;
  -webkit-border-radius: 0.5em;
+ border-radius: 0.5em;
  margin-bottom: 2em;
  border-bottom: 2px solid #aaa;
  border-right: 2px solid #aaa;
@@ -71,6 +72,7 @@
  margin-top: 1em;
  -moz-border-radius: 0.5em;
  -webkit-border-radius: 0.5em;
+ border-radius: 0.5em;
  margin-right: 0.25em;
  
 }
     padding-right: 0.75em;
     -moz-border-radius: 0.5em;
     -webkit-border-radius: 0.5em;
+    border-radius: 0.5em;
     border-bottom: 2px solid #aaa;
     border-right: 2px solid #aaa;
 
  padding-top: 0.5em;
  -moz-border-radius-bottomleft: 0.25em;
  -webkit-border-bottom-left-radius: 0.25em;
+ border-bottom-left-radius: 0.25em;
  
  
  -moz-border-radius-topright: 0.25em;
  -webkit-border-top-right-radius: 0.25em;
+ border-top-right-radius: 0.25em;
 
 }
 
index 8dc0cc1..8b600b8 100644 (file)
@@ -60,8 +60,10 @@ div#body {
     padding: 1.8em 1em 1em 1em;
     -moz-border-radius-topleft: 0.5em;
     -webkit-border-top-left-radius: 0.5em;
+    border-top-left-radius: 0.5em;
     -moz-border-radius-bottomleft: 0.5em;
     -webkit-border-bottom-left-radius: 0.5em;
+    border-bottom-left-radius: 0.5em;
     margin-left: 10em;
     margin-top: 3em;
     margin-right: 0;
@@ -89,8 +91,10 @@ div#footer {
  border-left: 2px solid #aaa;
  -moz-border-radius-topleft: 0.5em;
  -webkit-border-top-left-radius: 0.5em;
+ border-top-left-radius: 0.5em;
  -moz-border-radius-bottomleft: 0.5em;
  -webkit-border-bottom-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
 }
 
 div#footer #time {
index 196f0e6..dc29818 100644 (file)
     background-color: #fff;
     -moz-border-radius-bottomright: 0.5em;
     -webkit-border-bottom-right-radius: 0.5em;
+    border-bottom-right-radius: 0.5em;
     -moz-border-radius-topright: 0.5em;
     -webkit-border-top-right-radius: 0.5em;
+    border-top-right-radius: 0.5em;
     width: 10em;
     font-size: 0.85em;
     position: absolute;
     border: 1px solid #ccc;
     -moz-border-radius-bottomleft: 0.5em;
     -webkit-border-bottom-left-radius: 0.5em;
+    border-bottom-left-radius: 0.5em;
     padding: 0;
     padding-top: 0.5em;
     padding-right: 0.5em;
index 19ee847..fb252b5 100644 (file)
  border-bottom: 1px solid #999;
  -moz-border-radius-bottomleft: 0.5em;
  -webkit-border-bottom-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
 }
 
 
index 06b6678..4d416e1 100644 (file)
@@ -77,6 +77,7 @@ div#ticket-history {
  color: #ccc;
  -moz-border-radius-bottomleft: 0.5em;
  -webkit-border-bottom-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
  white-space: nowrap;
 }
 
@@ -91,6 +92,7 @@ div#ticket-history {
  border-bottom: 1px solid #999;
  -moz-border-radius: 0.25em;
  -webkit-border-bottom-right-radius: 0.25em;
+ border-bottom-right-radius: 0.25em;
 }
 
 div#ticket-history span.type a {
@@ -150,6 +152,7 @@ border-bottom: 2px solid #aaa;
 margin-top: 0.5em;
 -moz-border-radius: 0.5em;
 -webkit-border-radius: 0.5em;
+border-radius: 0.5em;
 
 }
 
index eab97b1..19af1b2 100644 (file)
@@ -87,6 +87,7 @@ input[type=reset], input[type=submit], input[class=button], button {
    padding-right: 0.5em;
    -moz-border-radius: 0.5em;
    -webkit-border-radius: 0.5em;
+   border-radius: 0.5em;
 }
 
 input.button:hover, button:hover, input[type=reset]:hover, input[type=submit]:hover, input[class=button]:hover {
diff --git a/share/html/NoAuth/css/base/jquery-ui-timepicker-addon.css b/share/html/NoAuth/css/base/jquery-ui-timepicker-addon.css
new file mode 100644 (file)
index 0000000..7eb8715
--- /dev/null
@@ -0,0 +1,7 @@
+.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
+.ui-timepicker-div dl { text-align: left; }
+.ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; }
+.ui-timepicker-div dl dd { margin: 0 10px 10px 65px; }
+.ui-timepicker-div td { font-size: 90%; }
+.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
+.ui-datepicker-buttonpane button.ui-datepicker-current { opacity: 1.0; }
index 820996e..8fe4f15 100644 (file)
@@ -46,5 +46,3 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 @import "jquery-ui.custom.modified.css";
-@import "ui.timepickr.css";
-@import "ui.timepickr.custom.css";
index 7a32322..3b1e1a0 100644 (file)
     width: 200px; /*must have*/
     height: 200px; /*must have*/
 }
+/*
+ * jQuery UI Slider 1.8.4
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider#theming
+ */
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }
index bd05a28..608ebf8 100644 (file)
@@ -100,3 +100,11 @@ margin-right:auto;margin-left:auto;
     padding-left: 1em;
 }
 
+.redirect-warning tt {
+    display: block;
+    margin: 0.5em 0 0.5em 1em;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 90%;
+}
index 9f77c8a..dac733d 100644 (file)
@@ -49,6 +49,7 @@
 
 @import "yui-fonts.css";
 @import "jquery-ui.css";
+@import "jquery-ui-timepicker-addon.css";
 @import "superfish.css";
 @import "superfish-navbar.css";
 @import "superfish-vertical.css";
index 9a3f24c..459156e 100644 (file)
@@ -90,4 +90,6 @@ ul.sf-navbar .current ul ul {
        -moz-border-radius-topright: 0;
        -webkit-border-top-right-radius: 0;
        -webkit-border-bottom-left-radius: 0;
+       border-top-right-radius: 0;
+       border-bottom-left-radius: 0;
 }
index 31198e4..7cb3b56 100644 (file)
@@ -130,6 +130,8 @@ li.sfHover > a > .sf-sub-indicator {
        -moz-border-radius-topright: 17px;
        -webkit-border-top-right-radius: 17px;
        -webkit-border-bottom-left-radius: 17px;
+       border-top-right-radius: 17px;
+       border-bottom-left-radius: 17px;
 }
 .sf-shadow ul.sf-shadow-off {
        background: transparent;
index daab263..869eba7 100644 (file)
@@ -82,21 +82,17 @@ iframe.richtext-editor {
 .messagebox-container.action-response iframe
 {
     background-color: #fcc !important;
-} 
-
-/*
-% if ( RT->Config->Get("UseSideBySideLayout", $session{'CurrentUser'}) ) {
-*/
+}
 
-#ticket-create-metadata,
-#ticket-update-metadata {
+.sidebyside #ticket-create-metadata,
+.sidebyside #ticket-update-metadata {
     float: right;
     width: 40%;
     clear: right;
 }
 
-#ticket-create-message,
-#ticket-update-message {
+.sidebyside #ticket-create-message,
+.sidebyside #ticket-update-message {
     float: left;
     width: 58%;
     clear: left;
@@ -104,10 +100,10 @@ iframe.richtext-editor {
 
 @media (max-width: 950px) {
     /* Revert to a single column when we're less than 1000px wide */
-    #ticket-create-metadata,
-    #ticket-update-metadata,
-    #ticket-create-message,
-    #ticket-update-message
+    .sidebyside #ticket-create-metadata,
+    .sidebyside #ticket-update-metadata,
+    .sidebyside #ticket-create-message,
+    .sidebyside #ticket-update-message
     {
         float: none;
         width: auto;
@@ -115,15 +111,12 @@ iframe.richtext-editor {
     }
 }
 
-#comp-Ticket-Update #body {
+.sidebyside #comp-Ticket-Update #body {
     padding-top: 3em;
 }
 
-#ticket-create-message .button[name="AddMoreAttach"],
-#ticket-update-message .button[name="AddMoreAttach"] {
+.sidebyside #ticket-create-message .button[name="AddMoreAttach"],
+.sidebyside #ticket-update-message .button[name="AddMoreAttach"] {
     float: right;
 }
 
-/*
-% }
-*/
index be63c59..e404b61 100644 (file)
     border: 1px solid #ccc;
     -moz-border-radius-bottomleft: 0.5em;
     -webkit-border-bottom-left-radius: 0.5em;
+    border-bottom-left-radius: 0.5em;
     border-right: none;
     border-top: none;
     list-style-type: none;
index c86f4cf..0e9e812 100644 (file)
@@ -94,7 +94,7 @@ while (my $t = $tickets->Next) {
     my $start = Data::ICal::Entry::Event->new;
     my $end   = Data::ICal::Entry::Event->new;
     $_->add_properties(
-        url       => RT->Config->Get('WebURL') . "?q=".$t->id,
+        url       => RT->Config->Get('WebURL') . "Ticket/Display.html?id=".$t->id,
         organizer => $t->OwnerObj->Name,
         dtstamp   => $now->iCal,
         created   => $t->CreatedObj->iCal,
index e90b4fe..0466005 100644 (file)
@@ -222,3 +222,53 @@ c=this._daylightSavingAdjust(new Date(c,e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(t
 function(a){if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));
 return this.each(function(){typeof a=="string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new L;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.4";window["DP_jQuery_"+y]=d})(jQuery);
 ;
+/*!
+ * jQuery UI Mouse 1.8.4
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Mouse
+ *
+ * Depends:
+ *     jquery.ui.widget.js
+ */
+(function(c){c.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var a=this;this.element.bind("mousedown."+this.widgetName,function(b){return a._mouseDown(b)}).bind("click."+this.widgetName,function(b){if(a._preventClickEvent){a._preventClickEvent=false;b.stopImmediatePropagation();return false}});this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName)},_mouseDown:function(a){a.originalEvent=a.originalEvent||{};if(!a.originalEvent.mouseHandled){this._mouseStarted&&
+this._mouseUp(a);this._mouseDownEvent=a;var b=this,e=a.which==1,f=typeof this.options.cancel=="string"?c(a.target).parents().add(a.target).filter(this.options.cancel).length:false;if(!e||f||!this._mouseCapture(a))return true;this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet)this._mouseDelayTimer=setTimeout(function(){b.mouseDelayMet=true},this.options.delay);if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a)){this._mouseStarted=this._mouseStart(a)!==false;if(!this._mouseStarted){a.preventDefault();
+return true}}this._mouseMoveDelegate=function(d){return b._mouseMove(d)};this._mouseUpDelegate=function(d){return b._mouseUp(d)};c(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);c.browser.safari||a.preventDefault();return a.originalEvent.mouseHandled=true}},_mouseMove:function(a){if(c.browser.msie&&!(document.documentMode>=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&
+this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;this._preventClickEvent=a.target==this._mouseDownEvent.target;this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-
+a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery);
+/*
+ * jQuery UI Slider 1.8.4
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider
+ *
+ * Depends:
+ *  jquery.ui.core.js
+ *  jquery.ui.mouse.js
+ *  jquery.ui.widget.js
+ */
+(function(d){d.widget("ui.slider",d.ui.mouse,{widgetEventPrefix:"slide",options:{animate:false,distance:0,max:100,min:0,orientation:"horizontal",range:false,step:1,value:0,values:null},_create:function(){var a=this,b=this.options;this._mouseSliding=this._keySliding=false;this._animateOff=true;this._handleIndex=null;this._detectOrientation();this._mouseInit();this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget ui-widget-content ui-corner-all");b.disabled&&this.element.addClass("ui-slider-disabled ui-disabled");
+this.range=d([]);if(b.range){if(b.range===true){this.range=d("<div></div>");if(!b.values)b.values=[this._valueMin(),this._valueMin()];if(b.values.length&&b.values.length!==2)b.values=[b.values[0],b.values[0]]}else this.range=d("<div></div>");this.range.appendTo(this.element).addClass("ui-slider-range");if(b.range==="min"||b.range==="max")this.range.addClass("ui-slider-range-"+b.range);this.range.addClass("ui-widget-header")}d(".ui-slider-handle",this.element).length===0&&d("<a href='#'></a>").appendTo(this.element).addClass("ui-slider-handle");
+if(b.values&&b.values.length)for(;d(".ui-slider-handle",this.element).length<b.values.length;)d("<a href='#'></a>").appendTo(this.element).addClass("ui-slider-handle");this.handles=d(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(c){c.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur();
+else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(c){d(this).data("index.ui-slider-handle",c)});this.handles.keydown(function(c){var e=true,f=d(this).data("index.ui-slider-handle"),h,g,i;if(!a.options.disabled){switch(c.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:e=
+false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");h=a._start(c,f);if(h===false)return}break}i=a.options.step;h=a.options.values&&a.options.values.length?(g=a.values(f)):(g=a.value());switch(c.keyCode){case d.ui.keyCode.HOME:g=a._valueMin();break;case d.ui.keyCode.END:g=a._valueMax();break;case d.ui.keyCode.PAGE_UP:g=a._trimAlignValue(h+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:g=a._trimAlignValue(h-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(h===
+a._valueMax())return;g=a._trimAlignValue(h+i);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(h===a._valueMin())return;g=a._trimAlignValue(h-i);break}a._slide(c,f,g);return e}}).keyup(function(c){var e=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(c,e);a._change(c,e);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider");
+this._mouseDestroy();return this},_mouseCapture:function(a){var b=this.options,c,e,f,h,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});e=this._valueMax()-this._valueMin()+1;h=this;this.handles.each(function(i){var j=Math.abs(c-h.values(i));if(e>j){e=j;f=d(this);g=i}});if(b.range===true&&this.values(1)===b.min){g+=1;f=d(this.handles[g])}if(this._start(a,
+g)===false)return false;this._mouseSliding=true;h._handleIndex=g;f.addClass("ui-state-active").focus();b=f.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-f.width()/2,top:a.pageY-b.top-f.height()/2-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b=
+this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b=
+this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b);
+c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var e;if(this.options.values&&this.options.values.length){e=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>e||b===1&&c<e))c=e;if(c!==this.values(b)){e=this.values();e[b]=c;a=this._trigger("slide",a,{handle:this.handles[b],value:c,values:e});this.values(b?0:1);a!==false&&this.values(b,c,true)}}else if(c!==this.value()){a=this._trigger("slide",a,{handle:this.handles[b],value:c});
+a!==false&&this.value(c)}},_stop:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b);c.values=this.values()}this._trigger("stop",a,c)},_change:function(a,b){if(!this._keySliding&&!this._mouseSliding){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b);c.values=this.values()}this._trigger("change",a,c)}},value:function(a){if(arguments.length){this.options.value=
+this._trimAlignValue(a);this._refreshValue();this._change(null,0)}return this._value()},values:function(a,b){var c,e,f;if(arguments.length>1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;e=arguments[0];for(f=0;f<c.length;f+=1){c[f]=this._trimAlignValue(e[f]);this._change(null,f)}this._refreshValue()}else return this.options.values&&this.options.values.length?this._values(a):this.value();
+else return this._values()},_setOption:function(a,b){var c,e=0;if(d.isArray(this.options.values))e=this.options.values.length;d.Widget.prototype._setOption.apply(this,arguments);switch(a){case "disabled":if(b){this.handles.filter(".ui-state-focus").blur();this.handles.removeClass("ui-state-hover");this.handles.attr("disabled","disabled");this.element.addClass("ui-disabled")}else{this.handles.removeAttr("disabled");this.element.removeClass("ui-disabled")}break;case "orientation":this._detectOrientation();
+this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation);this._refreshValue();break;case "value":this._animateOff=true;this._refreshValue();this._change(null,0);this._animateOff=false;break;case "values":this._animateOff=true;this._refreshValue();for(c=0;c<e;c+=1)this._change(null,c);this._animateOff=false;break}},_value:function(){var a=this.options.value;return a=this._trimAlignValue(a)},_values:function(a){var b,c;if(arguments.length){b=this.options.values[a];
+return b=this._trimAlignValue(b)}else{b=this.options.values.slice();for(c=0;c<b.length;c+=1)b[c]=this._trimAlignValue(b[c]);return b}},_trimAlignValue:function(a){if(a<this._valueMin())return this._valueMin();if(a>this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=a%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a=
+this.options.range,b=this.options,c=this,e=!this._animateOff?b.animate:false,f,h={},g,i,j,l;if(this.options.values&&this.options.values.length)this.handles.each(function(k){f=(c.values(k)-c._valueMin())/(c._valueMax()-c._valueMin())*100;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";d(this).stop(1,1)[e?"animate":"css"](h,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(k===0)c.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({width:f-
+g+"%"},{queue:false,duration:b.animate})}else{if(k===0)c.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({height:f-g+"%"},{queue:false,duration:b.animate})}g=f});else{i=this.value();j=this._valueMin();l=this._valueMax();f=l!==j?(i-j)/(l-j)*100:0;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";this.handle.stop(1,1)[e?"animate":"css"](h,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"},
+b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[e?"animate":"css"]({width:100-f+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[e?"animate":"css"]({height:100-f+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.4"})})(jQuery);
index 40cc0db..2ac101f 100644 (file)
 
         return data;
     };
+
+    $.datepicker._checkOffset_orig = $.datepicker._checkOffset;
+    $.datepicker._checkOffset = function(inst, offset, isFixed) {
+        // copied from the original
+        var dpHeight    = inst.dpDiv.outerHeight();
+        var inputHeight = inst.input ? inst.input.outerHeight() : 0;
+        var viewHeight  = document.documentElement.clientHeight + $(document).scrollTop();
+
+        // save the original offset rather than the new offset because the
+        // original function modifies the passed arg as a side-effect
+        var old_offset = { top: offset.top, left: offset.left };
+        offset = $.datepicker._checkOffset_orig(inst, offset, isFixed);
+
+        // Negate any up or down positioning by adding instead of subtracting
+        offset.top += Math.min(old_offset.top, (old_offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
+            Math.abs(dpHeight + inputHeight) : 0);
+
+        return offset;
+    };
+
+
+    $.timepicker._newInst_orig = $.timepicker._newInst;
+    $.timepicker._newInst = function($input, o) {
+        var tp_inst = $.timepicker._newInst_orig($input, o);
+        tp_inst._defaults.onClose = function(dateText, dp_inst) {
+           if ($.isFunction(o.onClose))
+               o.onClose.call($input[0], dateText, dp_inst, tp_inst);
+        };
+        return tp_inst;
+    };
+
 })(jQuery);
diff --git a/share/html/NoAuth/js/jquery-ui-timepicker-addon.js b/share/html/NoAuth/js/jquery-ui-timepicker-addon.js
new file mode 100644 (file)
index 0000000..0a4ff02
--- /dev/null
@@ -0,0 +1,1326 @@
+/*
+* jQuery timepicker addon
+* By: Trent Richardson [http://trentrichardson.com]
+* Version 1.0.0
+* Last Modified: 02/05/2012
+*
+* Copyright 2012 Trent Richardson
+* Dual licensed under the MIT and GPL licenses.
+* http://trentrichardson.com/Impromptu/GPL-LICENSE.txt
+* http://trentrichardson.com/Impromptu/MIT-LICENSE.txt
+*
+* HERES THE CSS:
+* .ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
+* .ui-timepicker-div dl { text-align: left; }
+* .ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; }
+* .ui-timepicker-div dl dd { margin: 0 10px 10px 65px; }
+* .ui-timepicker-div td { font-size: 90%; }
+* .ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
+*/
+
+(function($) {
+
+// Prevent "Uncaught RangeError: Maximum call stack size exceeded"
+$.ui.timepicker = $.ui.timepicker || {};
+if ($.ui.timepicker.version) {
+       return;
+}
+
+$.extend($.ui, { timepicker: { version: "1.0.0" } });
+
+/* Time picker manager.
+   Use the singleton instance of this class, $.timepicker, to interact with the time picker.
+   Settings for (groups of) time pickers are maintained in an instance object,
+   allowing multiple different settings on the same page. */
+
+function Timepicker() {
+       this.regional = []; // Available regional settings, indexed by language code
+       this.regional[''] = { // Default regional settings
+               currentText: 'Now',
+               closeText: 'Done',
+               ampm: false,
+               amNames: ['AM', 'A'],
+               pmNames: ['PM', 'P'],
+               timeFormat: 'hh:mm tt',
+               timeSuffix: '',
+               timeOnlyTitle: 'Choose Time',
+               timeText: 'Time',
+               hourText: 'Hour',
+               minuteText: 'Minute',
+               secondText: 'Second',
+               millisecText: 'Millisecond',
+               timezoneText: 'Time Zone'
+       };
+       this._defaults = { // Global defaults for all the datetime picker instances
+               showButtonPanel: true,
+               timeOnly: false,
+               showHour: true,
+               showMinute: true,
+               showSecond: false,
+               showMillisec: false,
+               showTimezone: false,
+               showTime: true,
+               stepHour: 1,
+               stepMinute: 1,
+               stepSecond: 1,
+               stepMillisec: 1,
+               hour: 0,
+               minute: 0,
+               second: 0,
+               millisec: 0,
+               timezone: '+0000',
+               hourMin: 0,
+               minuteMin: 0,
+               secondMin: 0,
+               millisecMin: 0,
+               hourMax: 23,
+               minuteMax: 59,
+               secondMax: 59,
+               millisecMax: 999,
+               minDateTime: null,
+               maxDateTime: null,
+               onSelect: null,
+               hourGrid: 0,
+               minuteGrid: 0,
+               secondGrid: 0,
+               millisecGrid: 0,
+               alwaysSetTime: true,
+               separator: ' ',
+               altFieldTimeOnly: true,
+               showTimepicker: true,
+               timezoneIso8609: false,
+               timezoneList: null,
+               addSliderAccess: false,
+               sliderAccessArgs: null
+       };
+       $.extend(this._defaults, this.regional['']);
+};
+
+$.extend(Timepicker.prototype, {
+       $input: null,
+       $altInput: null,
+       $timeObj: null,
+       inst: null,
+       hour_slider: null,
+       minute_slider: null,
+       second_slider: null,
+       millisec_slider: null,
+       timezone_select: null,
+       hour: 0,
+       minute: 0,
+       second: 0,
+       millisec: 0,
+       timezone: '+0000',
+       hourMinOriginal: null,
+       minuteMinOriginal: null,
+       secondMinOriginal: null,
+       millisecMinOriginal: null,
+       hourMaxOriginal: null,
+       minuteMaxOriginal: null,
+       secondMaxOriginal: null,
+       millisecMaxOriginal: null,
+       ampm: '',
+       formattedDate: '',
+       formattedTime: '',
+       formattedDateTime: '',
+       timezoneList: null,
+
+       /* Override the default settings for all instances of the time picker.
+          @param  settings  object - the new settings to use as defaults (anonymous object)
+          @return the manager object */
+       setDefaults: function(settings) {
+               extendRemove(this._defaults, settings || {});
+               return this;
+       },
+
+       //########################################################################
+       // Create a new Timepicker instance
+       //########################################################################
+       _newInst: function($input, o) {
+               var tp_inst = new Timepicker(),
+                       inlineSettings = {};
+
+               for (var attrName in this._defaults) {
+                       var attrValue = $input.attr('time:' + attrName);
+                       if (attrValue) {
+                               try {
+                                       inlineSettings[attrName] = eval(attrValue);
+                               } catch (err) {
+                                       inlineSettings[attrName] = attrValue;
+                               }
+                       }
+               }
+               tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, o, {
+                       beforeShow: function(input, dp_inst) {
+                               if ($.isFunction(o.beforeShow))
+                                       return o.beforeShow(input, dp_inst, tp_inst);
+                       },
+                       onChangeMonthYear: function(year, month, dp_inst) {
+                               // Update the time as well : this prevents the time from disappearing from the $input field.
+                               tp_inst._updateDateTime(dp_inst);
+                               if ($.isFunction(o.onChangeMonthYear))
+                                       o.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst);
+                       },
+                       onClose: function(dateText, dp_inst) {
+                               if (tp_inst.timeDefined === true && $input.val() != '')
+                                       tp_inst._updateDateTime(dp_inst);
+                               if ($.isFunction(o.onClose))
+                                       o.onClose.call($input[0], dateText, dp_inst, tp_inst);
+                       },
+                       timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker');
+               });
+               tp_inst.amNames = $.map(tp_inst._defaults.amNames, function(val) { return val.toUpperCase(); });
+               tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function(val) { return val.toUpperCase(); });
+
+               if (tp_inst._defaults.timezoneList === null) {
+                       var timezoneList = [];
+                       for (var i = -11; i <= 12; i++)
+                               timezoneList.push((i >= 0 ? '+' : '-') + ('0' + Math.abs(i).toString()).slice(-2) + '00');
+                       if (tp_inst._defaults.timezoneIso8609)
+                               timezoneList = $.map(timezoneList, function(val) {
+                                       return val == '+0000' ? 'Z' : (val.substring(0, 3) + ':' + val.substring(3));
+                               });
+                       tp_inst._defaults.timezoneList = timezoneList;
+               }
+
+               tp_inst.hour = tp_inst._defaults.hour;
+               tp_inst.minute = tp_inst._defaults.minute;
+               tp_inst.second = tp_inst._defaults.second;
+               tp_inst.millisec = tp_inst._defaults.millisec;
+               tp_inst.ampm = '';
+               tp_inst.$input = $input;
+
+               if (o.altField)
+                       tp_inst.$altInput = $(o.altField)
+                               .css({ cursor: 'pointer' })
+                               .focus(function(){ $input.trigger("focus"); });
+
+               if(tp_inst._defaults.minDate==0 || tp_inst._defaults.minDateTime==0)
+               {
+                       tp_inst._defaults.minDate=new Date();
+               }
+               if(tp_inst._defaults.maxDate==0 || tp_inst._defaults.maxDateTime==0)
+               {
+                       tp_inst._defaults.maxDate=new Date();
+               }
+
+               // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime..
+               if(tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date)
+                       tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime());
+               if(tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date)
+                       tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime());
+               if(tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date)
+                       tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime());
+               if(tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date)
+                       tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime());
+               return tp_inst;
+       },
+
+       //########################################################################
+       // add our sliders to the calendar
+       //########################################################################
+       _addTimePicker: function(dp_inst) {
+               var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ?
+                               this.$input.val() + ' ' + this.$altInput.val() :
+                               this.$input.val();
+
+               this.timeDefined = this._parseTime(currDT);
+               this._limitMinMaxDateTime(dp_inst, false);
+               this._injectTimePicker();
+       },
+
+       //########################################################################
+       // parse the time string from input value or _setTime
+       //########################################################################
+       _parseTime: function(timeString, withDate) {
+               var regstr = this._defaults.timeFormat.toString()
+                               .replace(/h{1,2}/ig, '(\\d?\\d)')
+                               .replace(/m{1,2}/ig, '(\\d?\\d)')
+                               .replace(/s{1,2}/ig, '(\\d?\\d)')
+                               .replace(/l{1}/ig, '(\\d?\\d?\\d)')
+                               .replace(/t{1,2}/ig, this._getPatternAmpm())
+                               .replace(/z{1}/ig, '(z|[-+]\\d\\d:?\\d\\d)?')
+                               .replace(/\s/g, '\\s?') + this._defaults.timeSuffix + '$',
+                       order = this._getFormatPositions(),
+                       ampm = '',
+                       treg;
+
+               if (!this.inst) this.inst = $.datepicker._getInst(this.$input[0]);
+
+               if (withDate || !this._defaults.timeOnly) {
+                       // the time should come after x number of characters and a space.
+                       // x = at least the length of text specified by the date format
+                       var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat');
+                       // escape special regex characters in the seperator
+                       var specials = new RegExp("[.*+?|()\\[\\]{}\\\\]", "g");
+                       regstr = '^.{' + dp_dateFormat.length + ',}?' + this._defaults.separator.replace(specials, "\\$&") + regstr;
+               }
+
+               treg = timeString.match(new RegExp(regstr, 'i'));
+
+               if (treg) {
+                       if (order.t !== -1) {
+                               if (treg[order.t] === undefined || treg[order.t].length === 0) {
+                                       ampm = '';
+                                       this.ampm = '';
+                               } else {
+                                       ampm = $.inArray(treg[order.t].toUpperCase(), this.amNames) !== -1 ? 'AM' : 'PM';
+                                       this.ampm = this._defaults[ampm == 'AM' ? 'amNames' : 'pmNames'][0];
+                               }
+                       }
+
+                       if (order.h !== -1) {
+                               if (ampm == 'AM' && treg[order.h] == '12')
+                                       this.hour = 0; // 12am = 0 hour
+                               else if (ampm == 'PM' && treg[order.h] != '12')
+                                       this.hour = (parseFloat(treg[order.h]) + 12).toFixed(0); // 12pm = 12 hour, any other pm = hour + 12
+                               else this.hour = Number(treg[order.h]);
+                       }
+
+                       if (order.m !== -1) this.minute = Number(treg[order.m]);
+                       if (order.s !== -1) this.second = Number(treg[order.s]);
+                       if (order.l !== -1) this.millisec = Number(treg[order.l]);
+                       if (order.z !== -1 && treg[order.z] !== undefined) {
+                               var tz = treg[order.z].toUpperCase();
+                               switch (tz.length) {
+                               case 1: // Z
+                                       tz = this._defaults.timezoneIso8609 ? 'Z' : '+0000';
+                                       break;
+                               case 5: // +hhmm
+                                       if (this._defaults.timezoneIso8609)
+                                               tz = tz.substring(1) == '0000'
+                                                  ? 'Z'
+                                                  : tz.substring(0, 3) + ':' + tz.substring(3);
+                                       break;
+                               case 6: // +hh:mm
+                                       if (!this._defaults.timezoneIso8609)
+                                               tz = tz == 'Z' || tz.substring(1) == '00:00'
+                                                  ? '+0000'
+                                                  : tz.replace(/:/, '');
+                                       else if (tz.substring(1) == '00:00')
+                                               tz = 'Z';
+                                       break;
+                               }
+                               this.timezone = tz;
+                       }
+
+                       return true;
+
+               }
+               return false;
+       },
+
+       //########################################################################
+       // pattern for standard and localized AM/PM markers
+       //########################################################################
+       _getPatternAmpm: function() {
+               var markers = [],
+                       o = this._defaults;
+               if (o.amNames)
+                       $.merge(markers, o.amNames);
+               if (o.pmNames)
+                       $.merge(markers, o.pmNames);
+               markers = $.map(markers, function(val) { return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&'); });
+               return '(' + markers.join('|') + ')?';
+       },
+
+       //########################################################################
+       // figure out position of time elements.. cause js cant do named captures
+       //########################################################################
+       _getFormatPositions: function() {
+               var finds = this._defaults.timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|t{1,2}|z)/g),
+                       orders = { h: -1, m: -1, s: -1, l: -1, t: -1, z: -1 };
+
+               if (finds)
+                       for (var i = 0; i < finds.length; i++)
+                               if (orders[finds[i].toString().charAt(0)] == -1)
+                                       orders[finds[i].toString().charAt(0)] = i + 1;
+
+               return orders;
+       },
+
+       //########################################################################
+       // generate and inject html for timepicker into ui datepicker
+       //########################################################################
+       _injectTimePicker: function() {
+               var $dp = this.inst.dpDiv,
+                       o = this._defaults,
+                       tp_inst = this,
+                       // Added by Peter Medeiros:
+                       // - Figure out what the hour/minute/second max should be based on the step values.
+                       // - Example: if stepMinute is 15, then minMax is 45.
+                       hourMax = parseInt((o.hourMax - ((o.hourMax - o.hourMin) % o.stepHour)) ,10),
+                       minMax  = parseInt((o.minuteMax - ((o.minuteMax - o.minuteMin) % o.stepMinute)) ,10),
+                       secMax  = parseInt((o.secondMax - ((o.secondMax - o.secondMin) % o.stepSecond)) ,10),
+                       millisecMax  = parseInt((o.millisecMax - ((o.millisecMax - o.millisecMin) % o.stepMillisec)) ,10),
+                       dp_id = this.inst.id.toString().replace(/([^A-Za-z0-9_])/g, '');
+
+               // Prevent displaying twice
+               //if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0) {
+               if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0 && o.showTimepicker) {
+                       var noDisplay = ' style="display:none;"',
+                               html =  '<div class="ui-timepicker-div" id="ui-timepicker-div-' + dp_id + '"><dl>' +
+                                               '<dt class="ui_tpicker_time_label" id="ui_tpicker_time_label_' + dp_id + '"' +
+                                               ((o.showTime) ? '' : noDisplay) + '>' + o.timeText + '</dt>' +
+                                               '<dd class="ui_tpicker_time" id="ui_tpicker_time_' + dp_id + '"' +
+                                               ((o.showTime) ? '' : noDisplay) + '></dd>' +
+                                               '<dt class="ui_tpicker_hour_label" id="ui_tpicker_hour_label_' + dp_id + '"' +
+                                               ((o.showHour) ? '' : noDisplay) + '>' + o.hourText + '</dt>',
+                               hourGridSize = 0,
+                               minuteGridSize = 0,
+                               secondGridSize = 0,
+                               millisecGridSize = 0,
+                               size = null;
+
+                       // Hours
+                       html += '<dd class="ui_tpicker_hour"><div id="ui_tpicker_hour_' + dp_id + '"' +
+                                               ((o.showHour) ? '' : noDisplay) + '></div>';
+                       if (o.showHour && o.hourGrid > 0) {
+                               html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>';
+
+                               for (var h = o.hourMin; h <= hourMax; h += parseInt(o.hourGrid,10)) {
+                                       hourGridSize++;
+                                       var tmph = (o.ampm && h > 12) ? h-12 : h;
+                                       if (tmph < 10) tmph = '0' + tmph;
+                                       if (o.ampm) {
+                                               if (h == 0) tmph = 12 +'a';
+                                               else if (h < 12) tmph += 'a';
+                                               else tmph += 'p';
+                                       }
+                                       html += '<td>' + tmph + '</td>';
+                               }
+
+                               html += '</tr></table></div>';
+                       }
+                       html += '</dd>';
+
+                       // Minutes
+                       html += '<dt class="ui_tpicker_minute_label" id="ui_tpicker_minute_label_' + dp_id + '"' +
+                                       ((o.showMinute) ? '' : noDisplay) + '>' + o.minuteText + '</dt>'+
+                                       '<dd class="ui_tpicker_minute"><div id="ui_tpicker_minute_' + dp_id + '"' +
+                                                       ((o.showMinute) ? '' : noDisplay) + '></div>';
+
+                       if (o.showMinute && o.minuteGrid > 0) {
+                               html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>';
+
+                               for (var m = o.minuteMin; m <= minMax; m += parseInt(o.minuteGrid,10)) {
+                                       minuteGridSize++;
+                                       html += '<td>' + ((m < 10) ? '0' : '') + m + '</td>';
+                               }
+
+                               html += '</tr></table></div>';
+                       }
+                       html += '</dd>';
+
+                       // Seconds
+                       html += '<dt class="ui_tpicker_second_label" id="ui_tpicker_second_label_' + dp_id + '"' +
+                                       ((o.showSecond) ? '' : noDisplay) + '>' + o.secondText + '</dt>'+
+                                       '<dd class="ui_tpicker_second"><div id="ui_tpicker_second_' + dp_id + '"'+
+                                                       ((o.showSecond) ? '' : noDisplay) + '></div>';
+
+                       if (o.showSecond && o.secondGrid > 0) {
+                               html += '<div style="padding-left: 1px"><table><tr>';
+
+                               for (var s = o.secondMin; s <= secMax; s += parseInt(o.secondGrid,10)) {
+                                       secondGridSize++;
+                                       html += '<td>' + ((s < 10) ? '0' : '') + s + '</td>';
+                               }
+
+                               html += '</tr></table></div>';
+                       }
+                       html += '</dd>';
+
+                       // Milliseconds
+                       html += '<dt class="ui_tpicker_millisec_label" id="ui_tpicker_millisec_label_' + dp_id + '"' +
+                                       ((o.showMillisec) ? '' : noDisplay) + '>' + o.millisecText + '</dt>'+
+                                       '<dd class="ui_tpicker_millisec"><div id="ui_tpicker_millisec_' + dp_id + '"'+
+                                                       ((o.showMillisec) ? '' : noDisplay) + '></div>';
+
+                       if (o.showMillisec && o.millisecGrid > 0) {
+                               html += '<div style="padding-left: 1px"><table><tr>';
+
+                               for (var l = o.millisecMin; l <= millisecMax; l += parseInt(o.millisecGrid,10)) {
+                                       millisecGridSize++;
+                                       html += '<td>' + ((l < 10) ? '0' : '') + l + '</td>';
+                               }
+
+                               html += '</tr></table></div>';
+                       }
+                       html += '</dd>';
+
+                       // Timezone
+                       html += '<dt class="ui_tpicker_timezone_label" id="ui_tpicker_timezone_label_' + dp_id + '"' +
+                                       ((o.showTimezone) ? '' : noDisplay) + '>' + o.timezoneText + '</dt>';
+                       html += '<dd class="ui_tpicker_timezone" id="ui_tpicker_timezone_' + dp_id + '"'        +
+                                                       ((o.showTimezone) ? '' : noDisplay) + '></dd>';
+
+                       html += '</dl></div>';
+                       $tp = $(html);
+
+                               // if we only want time picker...
+                       if (o.timeOnly === true) {
+                               $tp.prepend(
+                                       '<div class="ui-widget-header ui-helper-clearfix ui-corner-all">' +
+                                               '<div class="ui-datepicker-title">' + o.timeOnlyTitle + '</div>' +
+                                       '</div>');
+                               $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide();
+                       }
+
+                       this.hour_slider = $tp.find('#ui_tpicker_hour_'+ dp_id).slider({
+                               orientation: "horizontal",
+                               value: this.hour,
+                               min: o.hourMin,
+                               max: hourMax,
+                               step: o.stepHour,
+                               slide: function(event, ui) {
+                                       tp_inst.hour_slider.slider( "option", "value", ui.value);
+                                       tp_inst._onTimeChange();
+                               }
+                       });
+
+
+                       // Updated by Peter Medeiros:
+                       // - Pass in Event and UI instance into slide function
+                       this.minute_slider = $tp.find('#ui_tpicker_minute_'+ dp_id).slider({
+                               orientation: "horizontal",
+                               value: this.minute,
+                               min: o.minuteMin,
+                               max: minMax,
+                               step: o.stepMinute,
+                               slide: function(event, ui) {
+                                       tp_inst.minute_slider.slider( "option", "value", ui.value);
+                                       tp_inst._onTimeChange();
+                               }
+                       });
+
+                       this.second_slider = $tp.find('#ui_tpicker_second_'+ dp_id).slider({
+                               orientation: "horizontal",
+                               value: this.second,
+                               min: o.secondMin,
+                               max: secMax,
+                               step: o.stepSecond,
+                               slide: function(event, ui) {
+                                       tp_inst.second_slider.slider( "option", "value", ui.value);
+                                       tp_inst._onTimeChange();
+                               }
+                       });
+
+                       this.millisec_slider = $tp.find('#ui_tpicker_millisec_'+ dp_id).slider({
+                               orientation: "horizontal",
+                               value: this.millisec,
+                               min: o.millisecMin,
+                               max: millisecMax,
+                               step: o.stepMillisec,
+                               slide: function(event, ui) {
+                                       tp_inst.millisec_slider.slider( "option", "value", ui.value);
+                                       tp_inst._onTimeChange();
+                               }
+                       });
+
+                       this.timezone_select = $tp.find('#ui_tpicker_timezone_'+ dp_id).append('<select></select>').find("select");
+                       $.fn.append.apply(this.timezone_select,
+                               $.map(o.timezoneList, function(val, idx) {
+                                       return $("<option />")
+                                               .val(typeof val == "object" ? val.value : val)
+                                               .text(typeof val == "object" ? val.label : val);
+                               })
+                       );
+                       this.timezone_select.val((typeof this.timezone != "undefined" && this.timezone != null && this.timezone != "") ? this.timezone : o.timezone);
+                       this.timezone_select.change(function() {
+                               tp_inst._onTimeChange();
+                       });
+
+                       // Add grid functionality
+                       if (o.showHour && o.hourGrid > 0) {
+                               size = 100 * hourGridSize * o.hourGrid / (hourMax - o.hourMin);
+
+                               $tp.find(".ui_tpicker_hour table").css({
+                                       width: size + "%",
+                                       marginLeft: (size / (-2 * hourGridSize)) + "%",
+                                       borderCollapse: 'collapse'
+                               }).find("td").each( function(index) {
+                                       $(this).click(function() {
+                                               var h = $(this).html();
+                                               if(o.ampm)      {
+                                                       var ap = h.substring(2).toLowerCase(),
+                                                               aph = parseInt(h.substring(0,2), 10);
+                                                       if (ap == 'a') {
+                                                               if (aph == 12) h = 0;
+                                                               else h = aph;
+                                                       } else if (aph == 12) h = 12;
+                                                       else h = aph + 12;
+                                               }
+                                               tp_inst.hour_slider.slider("option", "value", h);
+                                               tp_inst._onTimeChange();
+                                               tp_inst._onSelectHandler();
+                                       }).css({
+                                               cursor: 'pointer',
+                                               width: (100 / hourGridSize) + '%',
+                                               textAlign: 'center',
+                                               overflow: 'hidden'
+                                       });
+                               });
+                       }
+
+                       if (o.showMinute && o.minuteGrid > 0) {
+                               size = 100 * minuteGridSize * o.minuteGrid / (minMax - o.minuteMin);
+                               $tp.find(".ui_tpicker_minute table").css({
+                                       width: size + "%",
+                                       marginLeft: (size / (-2 * minuteGridSize)) + "%",
+                                       borderCollapse: 'collapse'
+                               }).find("td").each(function(index) {
+                                       $(this).click(function() {
+                                               tp_inst.minute_slider.slider("option", "value", $(this).html());
+                                               tp_inst._onTimeChange();
+                                               tp_inst._onSelectHandler();
+                                       }).css({
+                                               cursor: 'pointer',
+                                               width: (100 / minuteGridSize) + '%',
+                                               textAlign: 'center',
+                                               overflow: 'hidden'
+                                       });
+                               });
+                       }
+
+                       if (o.showSecond && o.secondGrid > 0) {
+                               $tp.find(".ui_tpicker_second table").css({
+                                       width: size + "%",
+                                       marginLeft: (size / (-2 * secondGridSize)) + "%",
+                                       borderCollapse: 'collapse'
+                               }).find("td").each(function(index) {
+                                       $(this).click(function() {
+                                               tp_inst.second_slider.slider("option", "value", $(this).html());
+                                               tp_inst._onTimeChange();
+                                               tp_inst._onSelectHandler();
+                                       }).css({
+                                               cursor: 'pointer',
+                                               width: (100 / secondGridSize) + '%',
+                                               textAlign: 'center',
+                                               overflow: 'hidden'
+                                       });
+                               });
+                       }
+
+                       if (o.showMillisec && o.millisecGrid > 0) {
+                               $tp.find(".ui_tpicker_millisec table").css({
+                                       width: size + "%",
+                                       marginLeft: (size / (-2 * millisecGridSize)) + "%",
+                                       borderCollapse: 'collapse'
+                               }).find("td").each(function(index) {
+                                       $(this).click(function() {
+                                               tp_inst.millisec_slider.slider("option", "value", $(this).html());
+                                               tp_inst._onTimeChange();
+                                               tp_inst._onSelectHandler();
+                                       }).css({
+                                               cursor: 'pointer',
+                                               width: (100 / millisecGridSize) + '%',
+                                               textAlign: 'center',
+                                               overflow: 'hidden'
+                                       });
+                               });
+                       }
+
+                       var $buttonPanel = $dp.find('.ui-datepicker-buttonpane');
+                       if ($buttonPanel.length) $buttonPanel.before($tp);
+                       else $dp.append($tp);
+
+                       this.$timeObj = $tp.find('#ui_tpicker_time_'+ dp_id);
+
+                       if (this.inst !== null) {
+                               var timeDefined = this.timeDefined;
+                               this._onTimeChange();
+                               this.timeDefined = timeDefined;
+                       }
+
+                       //Emulate datepicker onSelect behavior. Call on slidestop.
+                       var onSelectDelegate = function() {
+                               tp_inst._onSelectHandler();
+                       };
+                       this.hour_slider.bind('slidestop',onSelectDelegate);
+                       this.minute_slider.bind('slidestop',onSelectDelegate);
+                       this.second_slider.bind('slidestop',onSelectDelegate);
+                       this.millisec_slider.bind('slidestop',onSelectDelegate);
+
+                       // slideAccess integration: http://trentrichardson.com/2011/11/11/jquery-ui-sliders-and-touch-accessibility/
+                       if (this._defaults.addSliderAccess){
+                               var sliderAccessArgs = this._defaults.sliderAccessArgs;
+                               setTimeout(function(){ // fix for inline mode
+                                       if($tp.find('.ui-slider-access').length == 0){
+                                               $tp.find('.ui-slider:visible').sliderAccess(sliderAccessArgs);
+
+                                               // fix any grids since sliders are shorter
+                                               var sliderAccessWidth = $tp.find('.ui-slider-access:eq(0)').outerWidth(true);
+                                               if(sliderAccessWidth){
+                                                       $tp.find('table:visible').each(function(){
+                                                               var $g = $(this),
+                                                                       oldWidth = $g.outerWidth(),
+                                                                       oldMarginLeft = $g.css('marginLeft').toString().replace('%',''),
+                                                                       newWidth = oldWidth - sliderAccessWidth,
+                                                                       newMarginLeft = ((oldMarginLeft * newWidth)/oldWidth) + '%';
+
+                                                               $g.css({ width: newWidth, marginLeft: newMarginLeft });
+                                                       });
+                                               }
+                                       }
+                               },0);
+                       }
+                       // end slideAccess integration
+
+               }
+       },
+
+       //########################################################################
+       // This function tries to limit the ability to go outside the
+       // min/max date range
+       //########################################################################
+       _limitMinMaxDateTime: function(dp_inst, adjustSliders){
+               var o = this._defaults,
+                       dp_date = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay);
+
+               if(!this._defaults.showTimepicker) return; // No time so nothing to check here
+
+               if($.datepicker._get(dp_inst, 'minDateTime') !== null && $.datepicker._get(dp_inst, 'minDateTime') !== undefined && dp_date){
+                       var minDateTime = $.datepicker._get(dp_inst, 'minDateTime'),
+                               minDateTimeDate = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), 0, 0, 0, 0);
+
+                       if(this.hourMinOriginal === null || this.minuteMinOriginal === null || this.secondMinOriginal === null || this.millisecMinOriginal === null){
+                               this.hourMinOriginal = o.hourMin;
+                               this.minuteMinOriginal = o.minuteMin;
+                               this.secondMinOriginal = o.secondMin;
+                               this.millisecMinOriginal = o.millisecMin;
+                       }
+
+                       if(dp_inst.settings.timeOnly || minDateTimeDate.getTime() == dp_date.getTime()) {
+                               this._defaults.hourMin = minDateTime.getHours();
+                               if (this.hour <= this._defaults.hourMin) {
+                                       this.hour = this._defaults.hourMin;
+                                       this._defaults.minuteMin = minDateTime.getMinutes();
+                                       if (this.minute <= this._defaults.minuteMin) {
+                                               this.minute = this._defaults.minuteMin;
+                                               this._defaults.secondMin = minDateTime.getSeconds();
+                                       } else if (this.second <= this._defaults.secondMin){
+                                               this.second = this._defaults.secondMin;
+                                               this._defaults.millisecMin = minDateTime.getMilliseconds();
+                                       } else {
+                                               if(this.millisec < this._defaults.millisecMin)
+                                                       this.millisec = this._defaults.millisecMin;
+                                               this._defaults.millisecMin = this.millisecMinOriginal;
+                                       }
+                               } else {
+                                       this._defaults.minuteMin = this.minuteMinOriginal;
+                                       this._defaults.secondMin = this.secondMinOriginal;
+                                       this._defaults.millisecMin = this.millisecMinOriginal;
+                               }
+                       }else{
+                               this._defaults.hourMin = this.hourMinOriginal;
+                               this._defaults.minuteMin = this.minuteMinOriginal;
+                               this._defaults.secondMin = this.secondMinOriginal;
+                               this._defaults.millisecMin = this.millisecMinOriginal;
+                       }
+               }
+
+               if($.datepicker._get(dp_inst, 'maxDateTime') !== null && $.datepicker._get(dp_inst, 'maxDateTime') !== undefined && dp_date){
+                       var maxDateTime = $.datepicker._get(dp_inst, 'maxDateTime'),
+                               maxDateTimeDate = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), 0, 0, 0, 0);
+
+                       if(this.hourMaxOriginal === null || this.minuteMaxOriginal === null || this.secondMaxOriginal === null){
+                               this.hourMaxOriginal = o.hourMax;
+                               this.minuteMaxOriginal = o.minuteMax;
+                               this.secondMaxOriginal = o.secondMax;
+                               this.millisecMaxOriginal = o.millisecMax;
+                       }
+
+                       if(dp_inst.settings.timeOnly || maxDateTimeDate.getTime() == dp_date.getTime()){
+                               this._defaults.hourMax = maxDateTime.getHours();
+                               if (this.hour >= this._defaults.hourMax) {
+                                       this.hour = this._defaults.hourMax;
+                                       this._defaults.minuteMax = maxDateTime.getMinutes();
+                                       if (this.minute >= this._defaults.minuteMax) {
+                                               this.minute = this._defaults.minuteMax;
+                                               this._defaults.secondMax = maxDateTime.getSeconds();
+                                       } else if (this.second >= this._defaults.secondMax) {
+                                               this.second = this._defaults.secondMax;
+                                               this._defaults.millisecMax = maxDateTime.getMilliseconds();
+                                       } else {
+                                               if(this.millisec > this._defaults.millisecMax) this.millisec = this._defaults.millisecMax;
+                                               this._defaults.millisecMax = this.millisecMaxOriginal;
+                                       }
+                               } else {
+                                       this._defaults.minuteMax = this.minuteMaxOriginal;
+                                       this._defaults.secondMax = this.secondMaxOriginal;
+                                       this._defaults.millisecMax = this.millisecMaxOriginal;
+                               }
+                       }else{
+                               this._defaults.hourMax = this.hourMaxOriginal;
+                               this._defaults.minuteMax = this.minuteMaxOriginal;
+                               this._defaults.secondMax = this.secondMaxOriginal;
+                               this._defaults.millisecMax = this.millisecMaxOriginal;
+                       }
+               }
+
+               if(adjustSliders !== undefined && adjustSliders === true){
+                       var hourMax = parseInt((this._defaults.hourMax - ((this._defaults.hourMax - this._defaults.hourMin) % this._defaults.stepHour)) ,10),
+                minMax  = parseInt((this._defaults.minuteMax - ((this._defaults.minuteMax - this._defaults.minuteMin) % this._defaults.stepMinute)) ,10),
+                secMax  = parseInt((this._defaults.secondMax - ((this._defaults.secondMax - this._defaults.secondMin) % this._defaults.stepSecond)) ,10),
+                               millisecMax  = parseInt((this._defaults.millisecMax - ((this._defaults.millisecMax - this._defaults.millisecMin) % this._defaults.stepMillisec)) ,10);
+
+                       if(this.hour_slider)
+                               this.hour_slider.slider("option", { min: this._defaults.hourMin, max: hourMax }).slider('value', this.hour);
+                       if(this.minute_slider)
+                               this.minute_slider.slider("option", { min: this._defaults.minuteMin, max: minMax }).slider('value', this.minute);
+                       if(this.second_slider)
+                               this.second_slider.slider("option", { min: this._defaults.secondMin, max: secMax }).slider('value', this.second);
+                       if(this.millisec_slider)
+                               this.millisec_slider.slider("option", { min: this._defaults.millisecMin, max: millisecMax }).slider('value', this.millisec);
+               }
+
+       },
+
+
+       //########################################################################
+       // when a slider moves, set the internal time...
+       // on time change is also called when the time is updated in the text field
+       //########################################################################
+       _onTimeChange: function() {
+               var hour   = (this.hour_slider) ? this.hour_slider.slider('value') : false,
+                       minute = (this.minute_slider) ? this.minute_slider.slider('value') : false,
+                       second = (this.second_slider) ? this.second_slider.slider('value') : false,
+                       millisec = (this.millisec_slider) ? this.millisec_slider.slider('value') : false,
+                       timezone = (this.timezone_select) ? this.timezone_select.val() : false,
+                       o = this._defaults;
+
+               if (typeof(hour) == 'object') hour = false;
+               if (typeof(minute) == 'object') minute = false;
+               if (typeof(second) == 'object') second = false;
+               if (typeof(millisec) == 'object') millisec = false;
+               if (typeof(timezone) == 'object') timezone = false;
+
+               if (hour !== false) hour = parseInt(hour,10);
+               if (minute !== false) minute = parseInt(minute,10);
+               if (second !== false) second = parseInt(second,10);
+               if (millisec !== false) millisec = parseInt(millisec,10);
+
+               var ampm = o[hour < 12 ? 'amNames' : 'pmNames'][0];
+
+               // If the update was done in the input field, the input field should not be updated.
+               // If the update was done using the sliders, update the input field.
+               var hasChanged = (hour != this.hour || minute != this.minute
+                               || second != this.second || millisec != this.millisec
+                               || (this.ampm.length > 0
+                                   && (hour < 12) != ($.inArray(this.ampm.toUpperCase(), this.amNames) !== -1))
+                               || timezone != this.timezone);
+
+               if (hasChanged) {
+
+                       if (hour !== false)this.hour = hour;
+                       if (minute !== false) this.minute = minute;
+                       if (second !== false) this.second = second;
+                       if (millisec !== false) this.millisec = millisec;
+                       if (timezone !== false) this.timezone = timezone;
+
+                       if (!this.inst) this.inst = $.datepicker._getInst(this.$input[0]);
+
+                       this._limitMinMaxDateTime(this.inst, true);
+               }
+               if (o.ampm) this.ampm = ampm;
+
+               //this._formatTime();
+               this.formattedTime = $.datepicker.formatTime(this._defaults.timeFormat, this, this._defaults);
+               if (this.$timeObj) this.$timeObj.text(this.formattedTime + o.timeSuffix);
+               this.timeDefined = true;
+               if (hasChanged) this._updateDateTime();
+       },
+
+       //########################################################################
+       // call custom onSelect.
+       // bind to sliders slidestop, and grid click.
+       //########################################################################
+       _onSelectHandler: function() {
+               var onSelect = this._defaults.onSelect;
+               var inputEl = this.$input ? this.$input[0] : null;
+               if (onSelect && inputEl) {
+                       onSelect.apply(inputEl, [this.formattedDateTime, this]);
+               }
+       },
+
+       //########################################################################
+       // left for any backwards compatibility
+       //########################################################################
+       _formatTime: function(time, format) {
+               time = time || { hour: this.hour, minute: this.minute, second: this.second, millisec: this.millisec, ampm: this.ampm, timezone: this.timezone };
+               var tmptime = (format || this._defaults.timeFormat).toString();
+
+               tmptime = $.datepicker.formatTime(tmptime, time, this._defaults);
+
+               if (arguments.length) return tmptime;
+               else this.formattedTime = tmptime;
+       },
+
+       //########################################################################
+       // update our input with the new date time..
+       //########################################################################
+       _updateDateTime: function(dp_inst) {
+               dp_inst = this.inst || dp_inst;
+               var dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)),
+                       dateFmt = $.datepicker._get(dp_inst, 'dateFormat'),
+                       formatCfg = $.datepicker._getFormatConfig(dp_inst),
+                       timeAvailable = dt !== null && this.timeDefined;
+               this.formattedDate = $.datepicker.formatDate(dateFmt, (dt === null ? new Date() : dt), formatCfg);
+               var formattedDateTime = this.formattedDate;
+               if (dp_inst.lastVal !== undefined && (dp_inst.lastVal.length > 0 && this.$input.val().length === 0))
+                       return;
+
+               if (this._defaults.timeOnly === true) {
+                       formattedDateTime = this.formattedTime;
+               } else if (this._defaults.timeOnly !== true && (this._defaults.alwaysSetTime || timeAvailable)) {
+                       formattedDateTime += this._defaults.separator + this.formattedTime + this._defaults.timeSuffix;
+               }
+
+               this.formattedDateTime = formattedDateTime;
+
+               if(!this._defaults.showTimepicker) {
+                       this.$input.val(this.formattedDate);
+               } else if (this.$altInput && this._defaults.altFieldTimeOnly === true) {
+                       this.$altInput.val(this.formattedTime);
+                       this.$input.val(this.formattedDate);
+               } else if(this.$altInput) {
+                       this.$altInput.val(formattedDateTime);
+                       this.$input.val(formattedDateTime);
+               } else {
+                       this.$input.val(formattedDateTime);
+               }
+
+               this.$input.trigger("change");
+       }
+
+});
+
+$.fn.extend({
+       //########################################################################
+       // shorthand just to use timepicker..
+       //########################################################################
+       timepicker: function(o) {
+               o = o || {};
+               var tmp_args = arguments;
+
+               if (typeof o == 'object') tmp_args[0] = $.extend(o, { timeOnly: true });
+
+               return $(this).each(function() {
+                       $.fn.datetimepicker.apply($(this), tmp_args);
+               });
+       },
+
+       //########################################################################
+       // extend timepicker to datepicker
+       //########################################################################
+       datetimepicker: function(o) {
+               o = o || {};
+               tmp_args = arguments;
+
+               if (typeof(o) == 'string'){
+                       if(o == 'getDate')
+                               return $.fn.datepicker.apply($(this[0]), tmp_args);
+                       else
+                               return this.each(function() {
+                                       var $t = $(this);
+                                       $t.datepicker.apply($t, tmp_args);
+                               });
+               }
+               else
+                       return this.each(function() {
+                               var $t = $(this);
+                               $t.datepicker($.timepicker._newInst($t, o)._defaults);
+                       });
+       }
+});
+
+//########################################################################
+// format the time all pretty...
+// format = string format of the time
+// time = a {}, not a Date() for timezones
+// options = essentially the regional[].. amNames, pmNames, ampm
+//########################################################################
+$.datepicker.formatTime = function(format, time, options) {
+       options = options || {};
+       options = $.extend($.timepicker._defaults, options);
+       time = $.extend({hour:0, minute:0, second:0, millisec:0, timezone:'+0000'}, time);
+
+       var tmptime = format;
+       var ampmName = options['amNames'][0];
+
+       var hour = parseInt(time.hour, 10);
+       if (options.ampm) {
+               if (hour > 11){
+                       ampmName = options['pmNames'][0];
+                       if(hour > 12)
+                               hour = hour % 12;
+               }
+               if (hour === 0)
+                       hour = 12;
+       }
+       tmptime = tmptime.replace(/(?:hh?|mm?|ss?|[tT]{1,2}|[lz])/g, function(match) {
+               switch (match.toLowerCase()) {
+                       case 'hh': return ('0' + hour).slice(-2);
+                       case 'h':  return hour;
+                       case 'mm': return ('0' + time.minute).slice(-2);
+                       case 'm':  return time.minute;
+                       case 'ss': return ('0' + time.second).slice(-2);
+                       case 's':  return time.second;
+                       case 'l':  return ('00' + time.millisec).slice(-3);
+                       case 'z':  return time.timezone;
+                       case 't': case 'tt':
+                               if (options.ampm) {
+                                       if (match.length == 1)
+                                               ampmName = ampmName.charAt(0);
+                                       return match.charAt(0) == 'T' ? ampmName.toUpperCase() : ampmName.toLowerCase();
+                               }
+                               return '';
+               }
+       });
+
+       tmptime = $.trim(tmptime);
+       return tmptime;
+};
+
+//########################################################################
+// the bad hack :/ override datepicker so it doesnt close on select
+// inspired: http://stackoverflow.com/questions/1252512/jquery-datepicker-prevent-closing-picker-when-clicking-a-date/1762378#1762378
+//########################################################################
+$.datepicker._base_selectDate = $.datepicker._selectDate;
+$.datepicker._selectDate = function (id, dateStr) {
+       var inst = this._getInst($(id)[0]),
+               tp_inst = this._get(inst, 'timepicker');
+
+       if (tp_inst) {
+               tp_inst._limitMinMaxDateTime(inst, true);
+               inst.inline = inst.stay_open = true;
+               //This way the onSelect handler called from calendarpicker get the full dateTime
+               this._base_selectDate(id, dateStr);
+               inst.inline = inst.stay_open = false;
+               this._notifyChange(inst);
+               this._updateDatepicker(inst);
+       }
+       else this._base_selectDate(id, dateStr);
+};
+
+//#############################################################################################
+// second bad hack :/ override datepicker so it triggers an event when changing the input field
+// and does not redraw the datepicker on every selectDate event
+//#############################################################################################
+$.datepicker._base_updateDatepicker = $.datepicker._updateDatepicker;
+$.datepicker._updateDatepicker = function(inst) {
+
+       // don't popup the datepicker if there is another instance already opened
+       var input = inst.input[0];
+       if($.datepicker._curInst &&
+          $.datepicker._curInst != inst &&
+          $.datepicker._datepickerShowing &&
+          $.datepicker._lastInput != input) {
+               return;
+       }
+
+       if (typeof(inst.stay_open) !== 'boolean' || inst.stay_open === false) {
+
+               this._base_updateDatepicker(inst);
+
+               // Reload the time control when changing something in the input text field.
+               var tp_inst = this._get(inst, 'timepicker');
+               if(tp_inst) tp_inst._addTimePicker(inst);
+       }
+};
+
+//#######################################################################################
+// third bad hack :/ override datepicker so it allows spaces and colon in the input field
+//#######################################################################################
+$.datepicker._base_doKeyPress = $.datepicker._doKeyPress;
+$.datepicker._doKeyPress = function(event) {
+       var inst = $.datepicker._getInst(event.target),
+               tp_inst = $.datepicker._get(inst, 'timepicker');
+
+       if (tp_inst) {
+               if ($.datepicker._get(inst, 'constrainInput')) {
+                       var ampm = tp_inst._defaults.ampm,
+                               dateChars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')),
+                               datetimeChars = tp_inst._defaults.timeFormat.toString()
+                                                               .replace(/[hms]/g, '')
+                                                               .replace(/TT/g, ampm ? 'APM' : '')
+                                                               .replace(/Tt/g, ampm ? 'AaPpMm' : '')
+                                                               .replace(/tT/g, ampm ? 'AaPpMm' : '')
+                                                               .replace(/T/g, ampm ? 'AP' : '')
+                                                               .replace(/tt/g, ampm ? 'apm' : '')
+                                                               .replace(/t/g, ampm ? 'ap' : '') +
+                                                               " " +
+                                                               tp_inst._defaults.separator +
+                                                               tp_inst._defaults.timeSuffix +
+                                                               (tp_inst._defaults.showTimezone ? tp_inst._defaults.timezoneList.join('') : '') +
+                                                               (tp_inst._defaults.amNames.join('')) +
+                                                               (tp_inst._defaults.pmNames.join('')) +
+                                                               dateChars,
+                               chr = String.fromCharCode(event.charCode === undefined ? event.keyCode : event.charCode);
+                       return event.ctrlKey || (chr < ' ' || !dateChars || datetimeChars.indexOf(chr) > -1);
+               }
+       }
+
+       return $.datepicker._base_doKeyPress(event);
+};
+
+//#######################################################################################
+// Override key up event to sync manual input changes.
+//#######################################################################################
+$.datepicker._base_doKeyUp = $.datepicker._doKeyUp;
+$.datepicker._doKeyUp = function (event) {
+       var inst = $.datepicker._getInst(event.target),
+               tp_inst = $.datepicker._get(inst, 'timepicker');
+
+       if (tp_inst) {
+               if (tp_inst._defaults.timeOnly && (inst.input.val() != inst.lastVal)) {
+                       try {
+                               $.datepicker._updateDatepicker(inst);
+                       }
+                       catch (err) {
+                               $.datepicker.log(err);
+                       }
+               }
+       }
+
+       return $.datepicker._base_doKeyUp(event);
+};
+
+//#######################################################################################
+// override "Today" button to also grab the time.
+//#######################################################################################
+$.datepicker._base_gotoToday = $.datepicker._gotoToday;
+$.datepicker._gotoToday = function(id) {
+       var inst = this._getInst($(id)[0]),
+               $dp = inst.dpDiv;
+       this._base_gotoToday(id);
+       var now = new Date();
+       var tp_inst = this._get(inst, 'timepicker');
+       if (tp_inst && tp_inst._defaults.showTimezone && tp_inst.timezone_select) {
+               var tzoffset = now.getTimezoneOffset(); // If +0100, returns -60
+               var tzsign = tzoffset > 0 ? '-' : '+';
+               tzoffset = Math.abs(tzoffset);
+               var tzmin = tzoffset % 60;
+               tzoffset = tzsign + ('0' + (tzoffset - tzmin) / 60).slice(-2) + ('0' + tzmin).slice(-2);
+               if (tp_inst._defaults.timezoneIso8609)
+                       tzoffset = tzoffset.substring(0, 3) + ':' + tzoffset.substring(3);
+               tp_inst.timezone_select.val(tzoffset);
+       }
+       this._setTime(inst, now);
+       $( '.ui-datepicker-today', $dp).click();
+};
+
+//#######################################################################################
+// Disable & enable the Time in the datetimepicker
+//#######################################################################################
+$.datepicker._disableTimepickerDatepicker = function(target, date, withDate) {
+       var inst = this._getInst(target),
+       tp_inst = this._get(inst, 'timepicker');
+       $(target).datepicker('getDate'); // Init selected[Year|Month|Day]
+       if (tp_inst) {
+               tp_inst._defaults.showTimepicker = false;
+               tp_inst._updateDateTime(inst);
+       }
+};
+
+$.datepicker._enableTimepickerDatepicker = function(target, date, withDate) {
+       var inst = this._getInst(target),
+       tp_inst = this._get(inst, 'timepicker');
+       $(target).datepicker('getDate'); // Init selected[Year|Month|Day]
+       if (tp_inst) {
+               tp_inst._defaults.showTimepicker = true;
+               tp_inst._addTimePicker(inst); // Could be disabled on page load
+               tp_inst._updateDateTime(inst);
+       }
+};
+
+//#######################################################################################
+// Create our own set time function
+//#######################################################################################
+$.datepicker._setTime = function(inst, date) {
+       var tp_inst = this._get(inst, 'timepicker');
+       if (tp_inst) {
+               var defaults = tp_inst._defaults,
+                       // calling _setTime with no date sets time to defaults
+                       hour = date ? date.getHours() : defaults.hour,
+                       minute = date ? date.getMinutes() : defaults.minute,
+                       second = date ? date.getSeconds() : defaults.second,
+                       millisec = date ? date.getMilliseconds() : defaults.millisec;
+
+               //check if within min/max times..
+               if ((hour < defaults.hourMin || hour > defaults.hourMax) || (minute < defaults.minuteMin || minute > defaults.minuteMax) || (second < defaults.secondMin || second > defaults.secondMax) || (millisec < defaults.millisecMin || millisec > defaults.millisecMax)) {
+                       hour = defaults.hourMin;
+                       minute = defaults.minuteMin;
+                       second = defaults.secondMin;
+                       millisec = defaults.millisecMin;
+               }
+
+               tp_inst.hour = hour;
+               tp_inst.minute = minute;
+               tp_inst.second = second;
+               tp_inst.millisec = millisec;
+
+               if (tp_inst.hour_slider) tp_inst.hour_slider.slider('value', hour);
+               if (tp_inst.minute_slider) tp_inst.minute_slider.slider('value', minute);
+               if (tp_inst.second_slider) tp_inst.second_slider.slider('value', second);
+               if (tp_inst.millisec_slider) tp_inst.millisec_slider.slider('value', millisec);
+
+               tp_inst._onTimeChange();
+               tp_inst._updateDateTime(inst);
+       }
+};
+
+//#######################################################################################
+// Create new public method to set only time, callable as $().datepicker('setTime', date)
+//#######################################################################################
+$.datepicker._setTimeDatepicker = function(target, date, withDate) {
+       var inst = this._getInst(target),
+               tp_inst = this._get(inst, 'timepicker');
+
+       if (tp_inst) {
+               this._setDateFromField(inst);
+               var tp_date;
+               if (date) {
+                       if (typeof date == "string") {
+                               tp_inst._parseTime(date, withDate);
+                               tp_date = new Date();
+                               tp_date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
+                       }
+                       else tp_date = new Date(date.getTime());
+                       if (tp_date.toString() == 'Invalid Date') tp_date = undefined;
+                       this._setTime(inst, tp_date);
+               }
+       }
+
+};
+
+//#######################################################################################
+// override setDate() to allow setting time too within Date object
+//#######################################################################################
+$.datepicker._base_setDateDatepicker = $.datepicker._setDateDatepicker;
+$.datepicker._setDateDatepicker = function(target, date) {
+       var inst = this._getInst(target),
+       tp_date = (date instanceof Date) ? new Date(date.getTime()) : date;
+
+       this._updateDatepicker(inst);
+       this._base_setDateDatepicker.apply(this, arguments);
+       this._setTimeDatepicker(target, tp_date, true);
+};
+
+//#######################################################################################
+// override getDate() to allow getting time too within Date object
+//#######################################################################################
+$.datepicker._base_getDateDatepicker = $.datepicker._getDateDatepicker;
+$.datepicker._getDateDatepicker = function(target, noDefault) {
+       var inst = this._getInst(target),
+               tp_inst = this._get(inst, 'timepicker');
+
+       if (tp_inst) {
+               this._setDateFromField(inst, noDefault);
+               var date = this._getDate(inst);
+               if (date && tp_inst._parseTime($(target).val(), tp_inst.timeOnly)) date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
+               return date;
+       }
+       return this._base_getDateDatepicker(target, noDefault);
+};
+
+//#######################################################################################
+// override parseDate() because UI 1.8.14 throws an error about "Extra characters"
+// An option in datapicker to ignore extra format characters would be nicer.
+//#######################################################################################
+$.datepicker._base_parseDate = $.datepicker.parseDate;
+$.datepicker.parseDate = function(format, value, settings) {
+       var date;
+       try {
+               date = this._base_parseDate(format, value, settings);
+       } catch (err) {
+               if (err.indexOf(":") >= 0) {
+                       // Hack!  The error message ends with a colon, a space, and
+                       // the "extra" characters.  We rely on that instead of
+                       // attempting to perfectly reproduce the parsing algorithm.
+                       date = this._base_parseDate(format, value.substring(0,value.length-(err.length-err.indexOf(':')-2)), settings);
+               } else {
+                       // The underlying error was not related to the time
+                       throw err;
+               }
+       }
+       return date;
+};
+
+//#######################################################################################
+// override formatDate to set date with time to the input
+//#######################################################################################
+$.datepicker._base_formatDate = $.datepicker._formatDate;
+$.datepicker._formatDate = function(inst, day, month, year){
+       var tp_inst = this._get(inst, 'timepicker');
+       if(tp_inst) {
+               tp_inst._updateDateTime(inst);
+               return tp_inst.$input.val();
+       }
+       return this._base_formatDate(inst);
+};
+
+//#######################################################################################
+// override options setter to add time to maxDate(Time) and minDate(Time). MaxDate
+//#######################################################################################
+$.datepicker._base_optionDatepicker = $.datepicker._optionDatepicker;
+$.datepicker._optionDatepicker = function(target, name, value) {
+       var inst = this._getInst(target),
+               tp_inst = this._get(inst, 'timepicker');
+       if (tp_inst) {
+               var min = null, max = null, onselect = null;
+               if (typeof name == 'string') { // if min/max was set with the string
+                       if (name === 'minDate' || name === 'minDateTime' )
+                               min = value;
+                       else if (name === 'maxDate' || name === 'maxDateTime')
+                               max = value;
+                       else if (name === 'onSelect')
+                               onselect = value;
+               } else if (typeof name == 'object') { //if min/max was set with the JSON
+                       if (name.minDate)
+                               min = name.minDate;
+                       else if (name.minDateTime)
+                               min = name.minDateTime;
+                       else if (name.maxDate)
+                               max = name.maxDate;
+                       else if (name.maxDateTime)
+                               max = name.maxDateTime;
+               }
+               if(min) { //if min was set
+                       if (min == 0)
+                               min = new Date();
+                       else
+                               min = new Date(min);
+
+                       tp_inst._defaults.minDate = min;
+                       tp_inst._defaults.minDateTime = min;
+               } else if (max) { //if max was set
+                       if(max==0)
+                               max=new Date();
+                       else
+                               max= new Date(max);
+                       tp_inst._defaults.maxDate = max;
+                       tp_inst._defaults.maxDateTime = max;
+               } else if (onselect)
+                       tp_inst._defaults.onSelect = onselect;
+       }
+       if (value === undefined)
+               return this._base_optionDatepicker(target, name);
+       return this._base_optionDatepicker(target, name, value);
+};
+
+//#######################################################################################
+// jQuery extend now ignores nulls!
+//#######################################################################################
+function extendRemove(target, props) {
+       $.extend(target, props);
+       for (var name in props)
+               if (props[name] === null || props[name] === undefined)
+                       target[name] = props[name];
+       return target;
+};
+
+$.timepicker = new Timepicker(); // singleton instance
+$.timepicker.version = "1.0.0";
+
+})(jQuery);
index 5bfce41..fe5c0a3 100644 (file)
@@ -222,35 +222,47 @@ function doOnLoad( js ) {
 }
 
 jQuery(function() {
-    jQuery(".ui-datepicker:not(.withtime)").datepicker( {
-        dateFormat: 'yy-mm-dd',
-        constrainInput: false
-    } );
-
-    jQuery(".ui-datepicker.withtime").datepicker( {
+    var opts = {
         dateFormat: 'yy-mm-dd',
         constrainInput: false,
-        onSelect: function( dateText, inst ) {
-            // trigger timepicker to get time
-            var button = document.createElement('input');
-            button.setAttribute('type',  'button');
-            jQuery(button).width('5em');
-            jQuery(button).insertAfter(this);
-            jQuery(button).timepickr({val: '00:00'});
-            var date_input = this;
-
-            jQuery(button).blur( function() {
-                var time = jQuery(button).val();
-                if ( ! time.match(/\d\d:\d\d/) ) {
-                    time = '00:00';
-                }
-                jQuery(date_input).val(  dateText + ' ' + time + ':00' );
-                jQuery(button).remove();
-            } );
-
-            jQuery(button).focus();
-        }
-    } );
+        showButtonPanel: true,
+        changeMonth: true,
+        changeYear: true,
+        showOtherMonths: true,
+        selectOtherMonths: true
+    };
+    jQuery(".ui-datepicker:not(.withtime)").datepicker(opts);
+    jQuery(".ui-datepicker.withtime").datetimepicker( jQuery.extend({}, opts, {
+        stepHour: 1,
+        // We fake this by snapping below for the minute slider
+        //stepMinute: 5,
+        hourGrid: 6,
+        minuteGrid: 15,
+        showSecond: false,
+        timeFormat: 'hh:mm:ss'
+    }) ).each(function(index, el) {
+        var tp = jQuery.datepicker._get( jQuery.datepicker._getInst(el), 'timepicker');
+        if (!tp) return;
+
+        // Hook after _injectTimePicker so we can modify the minute_slider
+        // right after it's first created
+        tp._base_injectTimePicker = tp._injectTimePicker;
+        tp._injectTimePicker = function() {
+            this._base_injectTimePicker.apply(this, arguments);
+
+            // Now that we have minute_slider, modify it to be stepped for mouse movements
+            var slider = jQuery.data(this.minute_slider[0], "slider");
+            slider._base_normValueFromMouse = slider._normValueFromMouse;
+            slider._normValueFromMouse = function() {
+                var value           = this._base_normValueFromMouse.apply(this, arguments);
+                var old_step        = this.options.step;
+                this.options.step   = 5;
+                var aligned         = this._trimAlignValue( value );
+                this.options.step   = old_step;
+                return aligned;
+            };
+        };
+    });
 });
 
 function textToHTML(value) {
index 9f7e04a..b5d3edd 100644 (file)
@@ -53,6 +53,7 @@
 % foreach my $section( RT->Config->Sections ) {
 <&|/Widgets/TitleBox, title => loc( $section ) &>
 % foreach my $option( RT->Config->Options( Section => $section ) ) {
+% next if $option eq 'EmailFrequency' && !RT->Config->Get('RecordOutgoingEmail');
 % my $meta = RT->Config->Meta( $option );
 <& $meta->{'Widget'},
     Default      => 1,
index 9a2212b..016a50c 100644 (file)
@@ -167,6 +167,17 @@ else {
             elsif (lc $k eq 'text') {
                 $text = delete $data{$k};
             }
+            elsif ( lc $k ne 'id' ) {
+                $e = 1;
+                push @$o, $k;
+                push(@comments, "# $k: Unknown field");
+            }
+        }
+
+        if ( $e ) {
+            unshift @comments, "# Could not create ticket.";
+            $k = \%data;
+            goto DONE;
         }
 
         # people fields allow multiple values
@@ -292,8 +303,10 @@ else {
         elsif (exists $simple{$key}) {
             $key = $simple{$key};
             $set = "Set$key";
+            my $current = $ticket->$key;
+            $current = '' unless defined $current;
 
-            next if (($val eq ($ticket->$key||''))|| ($ticket->$key =~ /^\d+$/ && $val =~ /^\d+$/ && $val == $ticket->$key));
+            next if ($val eq $current) or ($current =~ /^\d+$/ && $val =~ /^\d+$/ && $val == $current);
             ($n, $s) = $ticket->$set("$val");
         }
         elsif (exists $dates{$key}) {
@@ -331,13 +344,6 @@ else {
                 }
             }
             foreach $p (keys %new) {
-                # XXX: This is a stupid test.
-                unless ($p =~ /^[\w.+-]+\@([\w.-]+\.)*\w+.?$/) {
-                    $s = 0;
-                    $n = "$p is not a valid email address.";
-                    push @msgs, [ $s, $n ];
-                    next;
-                }
                 unless ($ticket->IsWatcher(Type => $type, Email => $p)) {
                     ($s, $n) = $ticket->AddWatcher(Type => $type,
                                                    Email => $p);
index 070ce7c..571c3d3 100644 (file)
@@ -98,14 +98,14 @@ my %query;
 
     for(@session_fields) {
         $query{$_} = $current->{$_} unless defined $query{$_};
-        $query{$_} = $m->request_args->{$_} unless defined $query{$_};
+        $query{$_} = $DECODED_ARGS->{$_} unless defined $query{$_};
     }
 
-    if ($m->request_args->{'SavedSearchLoadSubmit'}) {
-        $query{'SavedChartSearchId'} = $m->request_args->{'SavedSearchLoad'};
+    if ($DECODED_ARGS->{'SavedSearchLoadSubmit'}) {
+        $query{'SavedChartSearchId'} = $DECODED_ARGS->{'SavedSearchLoad'};
     }
 
-    if ($m->request_args->{'SavedSearchSave'}) {
+    if ($DECODED_ARGS->{'SavedSearchSave'}) {
         $query{'SavedChartSearchId'} = $saved_search->{'SearchId'};
     }
 
index d07e49c..bc29111 100644 (file)
@@ -62,7 +62,7 @@
 
 <%INIT>
 my @types;
-if ($Scope =~ 'queue') {
+if ($Scope =~ /queue/) {
    @types = qw(Cc AdminCc);
 }
 elsif ($Suffix eq 'Group') {
index 171b38d..4fee865 100644 (file)
@@ -151,6 +151,7 @@ if ($ARGS{'TicketsRefreshInterval'}) {
 my $refresh = $session{'tickets_refresh_interval'}
     || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} );
 
+# Check $m->request_args, not $DECODED_ARGS, to avoid creating a new CSRF token on each refresh
 if (RT->Config->Get('RestrictReferrer') and $refresh and not $m->request_args->{CSRF_Token}) {
     my $token = RT::Interface::Web::StoreRequestToken( $session{'CurrentSearchHash'} );
     $m->notes->{RefreshURL} = RT->Config->Get('WebURL')
index ec08294..86c99e1 100644 (file)
@@ -48,7 +48,7 @@
 <%perl>
     my ($ticket, $trans,$attach, $filename);
     my $arg = $m->dhandler_arg;                # get rest of path
-    if ($arg =~ '^(\d+)/(\d+)') {
+    if ($arg =~ m{^(\d+)/(\d+)}) {
         $trans = $1;
         $attach = $2;
     }
      my $enc = $AttachmentObj->OriginalEncoding || 'utf-8';
      my $iana = Encode::find_encoding( $enc );
      $iana = $iana? $iana->mime_name : $enc;
-        $content_type .= ";charset=$iana";
+
+     require MIME::Types;
+     my $mimetype = MIME::Types->new->type($content_type);
+     unless ( $mimetype && $mimetype->isBinary ) {
+           $content_type .= ";charset=$iana";
+     }
 
      $r->content_type( $content_type );
      $m->clear_buffer();
index c17c6e7..1ffbda2 100644 (file)
@@ -48,8 +48,9 @@
 <ul>
 % while (my $link = $members->Next) {
 <li><& /Elements/ShowLink, URI => $link->BaseURI &><br />
+% next if $link->BaseObj and $checked->{$link->BaseObj->id};
 % if ($depth < 8) {
-<& /Ticket/Elements/ShowMembers, Ticket => $link->BaseObj, depth => ($depth+1) &> 
+<& /Ticket/Elements/ShowMembers, Ticket => $link->BaseObj, depth => ($depth+1), checked => $checked &> 
 % }
 </li>
 % }
@@ -61,9 +62,13 @@ return unless $Ticket;
 my $members = $Ticket->Members;
 return unless $members->Count;
 
+return if $checked->{$Ticket->id};
+
+$checked->{$Ticket->id} = 1;
 </%INIT>
 
 <%ARGS>
 $Ticket => undef
 $depth => 1
+$checked => {}
 </%ARGS>