[Rt-commit] rt branch, 4.4/include-article-queue, created. rt-4.4.2-67-g54242d489

Craig Kaiser craig at bestpractical.com
Fri Feb 16 13:26:46 EST 2018


The branch, 4.4/include-article-queue has been created
        at  54242d489829d4248fb26970c8e5b8ef634151ab (commit)

- Log -----------------------------------------------------------------
commit 2d2495159ea55b28a17b444864c8bee778a11036
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Fri Dec 15 12:58:39 2017 -0500

    Add callback to Bulk.html at start of Init block

diff --git a/share/html/Search/Bulk.html b/share/html/Search/Bulk.html
index bb06b2415..348d45e00 100644
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@ -226,6 +226,10 @@ $cfs->SetContextObject( values %$seen_queues ) if keys %$seen_queues == 1;
 </div>
 
 <%INIT>
+my (@results);
+
+$m->callback(CallbackName => 'Initial', ARGSRef => \%ARGS, results_ref => \@results, QueryRef => \$Query, UpdateTicketRef => \@UpdateTicket);
+
 unless ( defined $Rows ) {
     $Rows = $RowsPerPage;
     $ARGS{Rows} = $RowsPerPage;
@@ -235,8 +239,6 @@ my $title = loc("Update multiple tickets");
 # Iterate through the ARGS hash and remove anything with a null value.
 map ( $ARGS{$_} =~ /^$/ && ( delete $ARGS{$_} ), keys %ARGS );
 
-my (@results);
-
 ProcessAttachments(ARGSRef => \%ARGS);
 
 $Page ||= 1;

commit 39e6fc1a2afbf7e580249b27439047f9532bad7d
Author: Alex Vandiver <alex at chmrr.net>
Date:   Tue Dec 26 01:34:29 2017 -0500

    Don't fail externalauth/auth_config.t tests if Net::LDAP is missing
    
    Doing the configuration at compile-time is complicated, and
    additionally introduces an implicit dependency on Net::LDAP.  Since
    that dependency is technically optional, switch the configuration to
    runtime, and introduce the same run-time dependency checking that is
    used in other ExternalAuth tests.

diff --git a/t/externalauth/auth_config.t b/t/externalauth/auth_config.t
index dda38c6ec..e26709d87 100644
--- a/t/externalauth/auth_config.t
+++ b/t/externalauth/auth_config.t
@@ -1,44 +1,47 @@
 use strict;
 use warnings;
-use RT;
-my $config;
-BEGIN{
-    $config = <<'END';
-Set($ExternalSettings, {
-        'My_LDAP'       =>  {
-            'type'             =>  'ldap',
-            'server'           =>  'ldap.example.com',
-            # By not passing 'user' and 'pass' we are using an anonymous
-            # bind, which some servers to not allow
-            'base'             =>  'ou=Staff,dc=example,dc=com',
-            'filter'           =>  '(objectClass=inetOrgPerson)',
-            # Users are allowed to log in via email address or account
-            # name
-            'attr_match_list'  => [
-                'Name',
-                'EmailAddress',
-            ],
-            # Import the following properties of the user from LDAP upon
-            # login
-            'attr_map' => {
-                'Name'         => 'sAMAccountName',
-                'EmailAddress' => 'mail',
-                'RealName'     => 'cn',
-                'WorkPhone'    => 'telephoneNumber',
-                'Address1'     => 'streetAddress',
-                'City'         => 'l',
-                'State'        => 'st',
-                'Zip'          => 'postalCode',
-                'Country'      => 'co',
-            },
+
+use RT::Test nodb => 1, tests => undef;
+use Test::Warn;
+
+# Having an LDAP in ExternalSettings implicitly loads Net::LDAP, so
+# only run these tests if that loads.
+eval { require RT::Authen::ExternalAuth; require Net::LDAP::Server::Test; 1; } or do {
+    plan skip_all => 'Unable to test without Net::LDAP and Net::LDAP::Server::Test';
+};
+
+RT::Config->Set( ExternalSettings => {
+    'My_LDAP'       =>  {
+        'type'             =>  'ldap',
+        'server'           =>  'ldap.example.com',
+        # By not passing 'user' and 'pass' we are using an anonymous
+        # bind, which some servers to not allow
+        'base'             =>  'ou=Staff,dc=example,dc=com',
+        'filter'           =>  '(objectClass=inetOrgPerson)',
+        # Users are allowed to log in via email address or account
+        # name
+        'attr_match_list'  => [
+            'Name',
+            'EmailAddress',
+        ],
+        # Import the following properties of the user from LDAP upon
+        # login
+        'attr_map' => {
+            'Name'         => 'sAMAccountName',
+            'EmailAddress' => 'mail',
+            'RealName'     => 'cn',
+            'WorkPhone'    => 'telephoneNumber',
+            'Address1'     => 'streetAddress',
+            'City'         => 'l',
+            'State'        => 'st',
+            'Zip'          => 'postalCode',
+            'Country'      => 'co',
         },
-    } );
+    },
+} );
 
-END
 
-}
-use RT::Test nodb => 1, tests => undef, config => $config;
-use Test::Warn;
+warnings_are {RT::Config->PostLoadCheck} [], "No warnings loading config";
 
 diag "Test ExternalAuth configuration processing";
 my $auth_settings = RT::Config->Get('ExternalSettings');

commit eac112a027a468eda35ae740434db01722dfa9df
Author: Robert <rspier at pobox.com>
Date:   Sat Nov 4 22:47:03 2017 -0700

    Set proper HTTP Status codes on Abort
    
    Iniitally implemented because Google's crawler was complaining that "Ticket
    not found" pages did not have a 404 status code.

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 5d6b39eb4..aff0f4801 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -72,6 +72,7 @@ use Digest::MD5 ();
 use List::MoreUtils qw();
 use JSON qw();
 use Plack::Util;
+use HTTP::Status qw();
 
 =head2 SquishedCSS $style
 
@@ -2038,6 +2039,10 @@ sub Abort {
     my $why  = shift;
     my %args = @_;
 
+    $args{Code} //= HTTP::Status::HTTP_OK;
+
+    $r->headers_out->{'Status'} = $args{Code} . ' ' . HTTP::Status::status_message($args{Code});
+
     if (   $session{'ErrorDocument'}
         && $session{'ErrorDocumentType'} )
     {
@@ -2140,11 +2145,11 @@ sub CreateTicket {
 
     my $Queue = RT::Queue->new( $current_user );
     unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
-        Abort('Queue not found');
+        Abort('Queue not found', Code => HTTP::Status::HTTP_NOT_FOUND);
     }
 
     unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
-        Abort('You have no permission to create tickets in that queue.');
+        Abort('You have no permission to create tickets in that queue.', Code => HTTP::Status::HTTP_FORBIDDEN);
     }
 
     my $due;
@@ -2257,7 +2262,7 @@ sub CreateTicket {
 
     push( @Actions, split( "\n", $ErrMsg ) );
     unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
-        Abort( "No permission to view newly created ticket #" . $Ticket->id . "." );
+        Abort( "No permission to view newly created ticket #" . $Ticket->id . ".", Code => HTTP::Status::HTTP_FORBIDDEN );
     }
     return ( $Ticket, @Actions );
 
@@ -2282,13 +2287,13 @@ sub LoadTicket {
     }
 
     unless ($id) {
-        Abort("No ticket specified");
+        Abort("No ticket specified", Code => HTTP::Status::HTTP_BAD_REQUEST);
     }
 
     my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
     $Ticket->Load($id);
     unless ( $Ticket->id ) {
-        Abort("Could not load ticket $id");
+        Abort("Could not load ticket $id", Code => HTTP::Status::HTTP_NOT_FOUND);
     }
     return $Ticket;
 }
diff --git a/share/html/Dashboards/Modify.html b/share/html/Dashboards/Modify.html
index 09da8e06f..1998926d3 100644
--- a/share/html/Dashboards/Modify.html
+++ b/share/html/Dashboards/Modify.html
@@ -92,7 +92,7 @@ my $Dashboard = RT::Dashboard->new($session{'CurrentUser'});
 my $method = $Create ? 'ObjectsForCreating' : 'ObjectsForModifying';
 my @privacies = $Dashboard->$method;
 
-Abort(loc("Permission Denied")) if @privacies == 0;
+Abort(loc("Permission Denied"), Code => HTTP::Status::HTTP_FORBIDDEN) if @privacies == 0;
 
 if ($Create) {
     $title = loc("Create a new dashboard");
@@ -149,10 +149,13 @@ if (!$Create && !$tried_create && $id && $ARGS{'Save'}) {
 my $can_delete = $Dashboard->CurrentUserCanDelete;
 
 if (!$Create && !$tried_create && $id && $ARGS{'Delete'}) {
+    if (!$can_delete) {
+        Abort(loc("Couldn't delete dashboard [_1]: Permission Denied", $id), Code => HTTP::Status::HTTP_FORBIDDEN);
+    }
     my ($ok, $msg) = $Dashboard->Delete();
     if (!$ok) {
-             Abort(loc("Couldn't delete dashboard [_1]: [_2]", $id, $msg));
-             }
+        Abort(loc("Couldn't delete dashboard [_1]: [_2]", $id, $msg), Code => HTTP::Status::HTTP_BAD_REQUEST);
+    }
 
     push @results, $msg;
     $redirect_to = '/Dashboards/index.html';
diff --git a/share/html/Ticket/Attachment/WithHeaders/dhandler b/share/html/Ticket/Attachment/WithHeaders/dhandler
index cbe07511e..009287809 100644
--- a/share/html/Ticket/Attachment/WithHeaders/dhandler
+++ b/share/html/Ticket/Attachment/WithHeaders/dhandler
@@ -50,13 +50,13 @@
     my ($id) = $m->dhandler_arg =~ /^(\d+)/;
     unless ( $id ) {
         # wrong url format
-        Abort("Corrupted attachment URL");
+        Abort("Corrupted attachment URL", Code => HTTP::Status::HTTP_BAD_REQUEST);
     }
 
     my $AttachmentObj = RT::Attachment->new( $session{'CurrentUser'} );
     $AttachmentObj->Load( $id );
     unless ( $AttachmentObj->id ) {
-        Abort("Couldn't load attachment #$id");
+        Abort("Couldn't load attachment #$id", Code => HTTP::Status::HTTP_NOT_FOUND);
     }
 
     my $content_type = 'text/plain';
diff --git a/share/html/Ticket/Attachment/dhandler b/share/html/Ticket/Attachment/dhandler
index afa471c1d..67e0281de 100644
--- a/share/html/Ticket/Attachment/dhandler
+++ b/share/html/Ticket/Attachment/dhandler
@@ -53,16 +53,16 @@ if ( $arg =~ m{^(\d+)/(\d+)} ) {
     $attach = $2;
 }
 else {
-    Abort("Corrupted attachment URL.");
+    Abort("Corrupted attachment URL.", Code => HTTP::Status::HTTP_BAD_REQUEST);
 }
 my $AttachmentObj = RT::Attachment->new( $session{'CurrentUser'} );
-$AttachmentObj->Load($attach) || Abort("Attachment '$attach' could not be loaded");
+$AttachmentObj->Load($attach) || Abort("Attachment '$attach' could not be loaded", Code => HTTP::Status::HTTP_NOT_FOUND);
 
 unless ( $AttachmentObj->id ) {
-    Abort("Bad attachment id. Couldn't find attachment '$attach'\n");
+    Abort("Bad attachment id. Couldn't find attachment '$attach'\n", Code => HTTP::Status::HTTP_NOT_FOUND);
 }
 unless ( $AttachmentObj->TransactionId() == $trans ) {
-    Abort("Bad transaction number for attachment. $trans should be". $AttachmentObj->TransactionId() . "\n");
+    Abort("Bad transaction number for attachment. $trans should be". $AttachmentObj->TransactionId() . "\n", Code => HTTP::Status::HTTP_NOT_FOUND);
 }
 
 my $content = $AttachmentObj->OriginalContent;
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index 26dc216b0..e1b347d7a 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -366,7 +366,7 @@ my $current_user = $session{'CurrentUser'};
 if ($CloneTicket) {
     my $CloneTicketObj = RT::Ticket->new( $session{CurrentUser} );
     $CloneTicketObj->Load($CloneTicket)
-        or Abort( loc("Ticket could not be loaded") );
+        or Abort( loc("Ticket could not be loaded"), Code => HTTP::Status::HTTP_BAD_REQUEST );
 
     my $clone = {
         Requestors => join( ',', $CloneTicketObj->RequestorAddresses ),
@@ -438,7 +438,7 @@ if ($CloneTicket) {
 my @results;
 
 my $QueueObj = RT::Queue->new($current_user);
-$QueueObj->Load($Queue) || Abort(loc("Queue [_1] could not be loaded.", $Queue||''));
+$QueueObj->Load($Queue) || Abort(loc("Queue [_1] could not be loaded.", $Queue||''), Code => HTTP::Status::HTTP_BAD_REQUEST);
 
 my $title = loc("Create a new ticket in [_1]", $m->scomp("/Ticket/Elements/ShowQueue", QueueObj => $QueueObj));
 
@@ -446,7 +446,7 @@ $m->callback( QueueObj => $QueueObj, title => \$title, results => \@results, ARG
 
 $m->scomp( '/Articles/Elements/SubjectOverride', ARGSRef => \%ARGS, QueueObj => $QueueObj, results => \@results );
 
-$QueueObj->Disabled && Abort(loc("Cannot create tickets in a disabled queue."));
+$QueueObj->Disabled && Abort(loc("Cannot create tickets in a disabled queue."), Code => HTTP::Status::HTTP_NOT_FOUND);
 
 my $ticket = RT::Ticket->new($current_user); # empty ticket object
 
diff --git a/share/html/Ticket/Display.html b/share/html/Ticket/Display.html
index f992a1e74..a6bfb85b1 100644
--- a/share/html/Ticket/Display.html
+++ b/share/html/Ticket/Display.html
@@ -133,7 +133,7 @@ my (@Actions, $title);
 
 
 unless ($id || $TicketObj) {
-    Abort('No ticket specified');
+    Abort('No ticket specified', Code => HTTP::Status::HTTP_BAD_REQUEST);
 }
 
 if ($ARGS{'id'} eq 'new') {
@@ -142,16 +142,16 @@ if ($ARGS{'id'} eq 'new') {
     my $Queue = RT::Queue->new( $session{'CurrentUser'} );
     $Queue->Load($ARGS{'Queue'});
     unless ( $Queue->id ) {
-        Abort('Queue not found');
+        Abort('Queue not found', Code => HTTP::Status::HTTP_NOT_FOUND);
     }
 
     unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
-        Abort('You have no permission to create tickets in that queue.');
+        Abort('You have no permission to create tickets in that queue.', Code => HTTP::Status::HTTP_FORBIDDEN);
     }
 
     ($TicketObj, @Actions) = CreateTicket( %ARGS );
     unless ( $TicketObj->CurrentUserHasRight('ShowTicket') ) {
-        Abort("No permission to view newly created ticket #".$TicketObj->id.".");
+        Abort("No permission to view newly created ticket #".$TicketObj->id.".", Code => HTTP::Status::HTTP_FORBIDDEN);
     }
 } else { 
     $TicketObj ||= LoadTicket($ARGS{'id'});
@@ -197,9 +197,9 @@ if ($ARGS{'id'} eq 'new') {
     if ( !$SkipProcessing ) {
         unless ($TicketObj->CurrentUserHasRight('ShowTicket')) {
             if (@Actions) {
-                Abort("A change was applied successfully, but you no longer have permissions to view the ticket", Actions => \@Actions);
+                Abort("A change was applied successfully, but you no longer have permissions to view the ticket", Actions => \@Actions, Code => HTTP::Status::HTTP_FORBIDDEN);
             } else {
-                Abort("No permission to view ticket");
+                Abort("No permission to view ticket", Code => HTTP::Status::HTTP_FORBIDDEN);
             }
         }
         if ( $ARGS{'MarkAsSeen'} ) {

commit db9b71e9c4a0b85ca449ef4ae7a4ea7b9727f3a8
Author: Robert <rspier at pobox.com>
Date:   Sun Dec 24 21:50:10 2017 -0800

    Fix tests failing because they assume a 200 status response

diff --git a/lib/RT/Test/Web.pm b/lib/RT/Test/Web.pm
index 87070cd07..f64890f1d 100644
--- a/lib/RT/Test/Web.pm
+++ b/lib/RT/Test/Web.pm
@@ -55,6 +55,7 @@ use base qw(Test::WWW::Mechanize);
 use MIME::Base64 qw//;
 use Encode 'encode_utf8';
 use Storable 'thaw';
+use HTTP::Status qw();
 
 BEGIN { require RT::Test; }
 require Test::More;
@@ -126,7 +127,7 @@ sub logged_in_as {
     my $self = shift;
     my $user = shift || '';
 
-    unless ( $self->status == 200 ) {
+    unless ( $self->status == HTTP::Status::HTTP_OK ) {
         Test::More::diag( "error: status is ". $self->status );
         return 0;
     }
@@ -144,12 +145,12 @@ sub logout {
     my $url = $self->rt_base_url;
     $self->get($url);
     Test::More::diag( "error: status is ". $self->status )
-        unless $self->status == 200;
+        unless $self->status == HTTP::Status::HTTP_OK;
 
     if ( $self->content =~ /Logout/i ) {
         $self->follow_link( text => 'Logout' );
         Test::More::diag( "error: status is ". $self->status ." when tried to logout" )
-            unless $self->status == 200;
+            unless $self->status == HTTP::Status::HTTP_OK;
     }
     else {
         return 1;
@@ -167,6 +168,7 @@ sub goto_ticket {
     my $self = shift;
     my $id   = shift;
     my $view = shift || 'Display';
+    my $status = shift || HTTP::Status::HTTP_OK;
     unless ( $id && int $id ) {
         Test::More::diag( "error: wrong id ". defined $id? $id : '(undef)' );
         return 0;
@@ -175,7 +177,7 @@ sub goto_ticket {
     my $url = $self->rt_base_url;
     $url .= "Ticket/${ view }.html?id=$id";
     $self->get($url);
-    unless ( $self->status == 200 ) {
+    unless ( $self->status == $status ) {
         Test::More::diag( "error: status is ". $self->status );
         return 0;
     }
diff --git a/t/security/CVE-2011-2084-attach-tickets.t b/t/security/CVE-2011-2084-attach-tickets.t
index 3d189cbc4..6b1366a5f 100644
--- a/t/security/CVE-2011-2084-attach-tickets.t
+++ b/t/security/CVE-2011-2084-attach-tickets.t
@@ -2,6 +2,7 @@ use strict;
 use warnings;
 
 use RT::Test tests => undef;
+use HTTP::Status qw();
 
 my $user = RT::Test->load_or_create_user(
     Name            => 'user',
@@ -39,7 +40,8 @@ my $ticket_b = RT::Test->last_ticket;
 my ($baseurl, $m) = RT::Test->started_ok;
 ok $m->login( 'user', 'password' ), 'logged in as user';
 
-$m->get_ok("$baseurl/Ticket/Display.html?id=" . $ticket_b->id);
+$m->get("$baseurl/Ticket/Display.html?id=" . $ticket_b->id);
+is($m->status, HTTP::Status::HTTP_FORBIDDEN, 'No permission to view ticket.');
 $m->content_contains('No permission');
 $m->warning_like(qr/no permission/i, 'no permission warning');
 
diff --git a/t/web/compilation_errors.t b/t/web/compilation_errors.t
index e4845e0de..966470912 100644
--- a/t/web/compilation_errors.t
+++ b/t/web/compilation_errors.t
@@ -2,12 +2,14 @@ use strict;
 use warnings;
 use Test::More;
 use File::Find;
+use HTTP::Status qw();
+
 BEGIN {
     sub wanted {
         -f && /\.html$/ && $_ !~ /Logout.html$/ && $File::Find::dir !~ /RichText/;
     }
     my $tests = 7;
-    find( sub { wanted() and $tests += 4 }, 'share/html/' );
+    find( sub { wanted() and $tests += 3 }, 'share/html/' );
     plan tests => $tests + 1; # plus one for warnings check
 }
 
@@ -28,11 +30,11 @@ $agent->cookie_jar($cookie_jar);
 my $url = $agent->rt_base_url;
 $agent->get($url);
 
-is($agent->status, 200, "Loaded a page");
+is($agent->status, HTTP::Status::HTTP_OK, "Loaded a page");
 
 # follow the link marked "Login"
 $agent->login(root => 'password');
-is($agent->status, 200, "Fetched the page ok");
+is($agent->status, HTTP::Status::HTTP_OK, "Fetched the page ok");
 $agent->content_contains('Logout', "Found a logout link");
 
 
@@ -48,8 +50,8 @@ sub test_get {
         $file =~ s#^share/html/##;
         diag( "testing $url/$file" );
 
-        $agent->get_ok("$url/$file");
-        is($agent->status, 200, "Loaded $file");
+        $agent->get("$url/$file");
+        isnt($agent->status, HTTP::Status::HTTP_INTERNAL_SERVER_ERROR, "Loaded $file");
         $agent->content_lacks('Not logged in', "Still logged in for  $file");
         $agent->content_lacks('raw error', "Didn't get a Mason compilation error on $file") or do {
             if (my ($error) = $agent->content =~ /<pre>(.*?line.*?)$/s) {
diff --git a/t/web/csrf.t b/t/web/csrf.t
index d693b5504..bdd895d92 100644
--- a/t/web/csrf.t
+++ b/t/web/csrf.t
@@ -147,7 +147,7 @@ $m->title_is('Possible cross-site request forgery');
 # Create.html errors out.
 my $link = $m->find_link(text_regex => qr{resume your request});
 (my $broken_url = $link->url) =~ s/(CSRF_Token)=\w+/$1=crud/;
-$m->get_ok($broken_url);
+$m->get($broken_url);
 $m->content_like(qr/Queue\s+could not be loaded/);
 $m->title_is('RT Error');
 $m->warning_like(qr/Queue\s+could not be loaded/);
diff --git a/t/web/dashboards-basics.t b/t/web/dashboards-basics.t
index f29e1d407..024439f13 100644
--- a/t/web/dashboards-basics.t
+++ b/t/web/dashboards-basics.t
@@ -1,6 +1,7 @@
 use strict;
 use warnings;
 
+use HTTP::Status qw();
 use RT::Test tests => 105;
 my ($baseurl, $m) = RT::Test->started_ok;
 
@@ -40,7 +41,8 @@ $m->content_lacks('<a href="/Dashboards/Modify.html?Create=1">New</a>',
 
 $m->no_warnings_ok;
 
-$m->get_ok($url."Dashboards/Modify.html?Create=1");
+$m->get($url."Dashboards/Modify.html?Create=1");
+is($m->status, HTTP::Status::HTTP_FORBIDDEN);
 $m->content_contains("Permission Denied");
 $m->content_lacks("Save Changes");
 
@@ -49,7 +51,8 @@ $m->warning_like(qr/Permission Denied/, "got a permission denied warning");
 $user_obj->PrincipalObj->GrantRight(Right => 'ModifyOwnDashboard', Object => $RT::System);
 
 # Modify itself is no longer good enough, you need Create
-$m->get_ok($url."Dashboards/Modify.html?Create=1");
+$m->get($url."Dashboards/Modify.html?Create=1");
+is($m->status, HTTP::Status::HTTP_FORBIDDEN);
 $m->content_contains("Permission Denied");
 $m->content_lacks("Save Changes");
 
@@ -150,7 +153,8 @@ $m->content_unlike( qr/Bookmarked Tickets.*Bookmarked Tickets/s,
     'only dashboard queries show up' );
 $m->content_contains("dashboard test", "ticket subject");
 
-$m->get_ok("/Dashboards/Modify.html?id=$id&Delete=1");
+$m->get("/Dashboards/Modify.html?id=$id&Delete=1");
+is($m->status, HTTP::Status::HTTP_FORBIDDEN);
 $m->content_contains("Permission Denied", "unable to delete dashboard because we lack DeleteOwnDashboard");
 
 $m->warning_like(qr/Couldn't delete dashboard.*Permission Denied/, "got a permission denied warning when trying to delete the dashboard");
diff --git a/t/web/path-traversal.t b/t/web/path-traversal.t
index 2a81d9821..204ed405e 100644
--- a/t/web/path-traversal.t
+++ b/t/web/path-traversal.t
@@ -1,49 +1,50 @@
 use strict;
 use warnings;
 
+use HTTP::Status qw();
 use RT::Test tests => undef;
 
 my ($baseurl, $agent) = RT::Test->started_ok;
 ok($agent->login);
 
 $agent->get("$baseurl/NoAuth/../Elements/HeaderJavascript");
-is($agent->status, 400);
+is($agent->status, HTTP::Status::HTTP_BAD_REQUEST);
 $agent->warning_like(qr/Invalid request.*aborting/);
 
 $agent->get("$baseurl/NoAuth/../%45lements/HeaderJavascript");
-is($agent->status, 400);
+is($agent->status, HTTP::Status::HTTP_BAD_REQUEST);
 $agent->warning_like(qr/Invalid request.*aborting/);
 
 $agent->get("$baseurl/NoAuth/%2E%2E/Elements/HeaderJavascript");
-is($agent->status, 400);
+is($agent->status, HTTP::Status::HTTP_BAD_REQUEST);
 $agent->warning_like(qr/Invalid request.*aborting/);
 
 $agent->get("$baseurl/NoAuth/../../../etc/RT_Config.pm");
-is($agent->status, 400);
+is($agent->status, HTTP::Status::HTTP_BAD_REQUEST);
 $agent->warning_like(qr/Invalid request.*aborting/) unless $ENV{RT_TEST_WEB_HANDLER} =~ /^apache/;
 
 $agent->get("$baseurl/static/css/web2/images/../../../../../../etc/RT_Config.pm");
-# Apache hardcodes a 400m but the static handler returns a 403 for traversal too high
-is($agent->status, $ENV{RT_TEST_WEB_HANDLER} =~ /^apache/ ? 400 : 403);
+# Apache hardcodes a 400 but the static handler returns a 403 for traversal too high
+is($agent->status, $ENV{RT_TEST_WEB_HANDLER} =~ /^apache/ ? HTTP::Status::HTTP_BAD_REQUEST : HTTP::Status::HTTP_FORBIDDEN);
 
 # Do not reject a simple /. in the URL, for downloading uploaded
 # dotfiles, for example.
 $agent->get("$baseurl/Ticket/Attachment/28/9/.bashrc");
-is($agent->status, 200); # Even for a file not found, we return 200
+is($agent->status, HTTP::Status::HTTP_NOT_FOUND);
 $agent->next_warning_like(qr/could not be loaded/, "couldn't loaded warning");
 $agent->content_like(qr/Attachment \S+ could not be loaded/);
 
 # do not reject these URLs, even though they contain /. outside the path
 $agent->get("$baseurl/index.html?ignored=%2F%2E");
-is($agent->status, 200);
+is($agent->status, HTTP::Status::HTTP_OK);
 
 $agent->get("$baseurl/index.html?ignored=/.");
-is($agent->status, 200);
+is($agent->status, HTTP::Status::HTTP_OK);
 
 $agent->get("$baseurl/index.html#%2F%2E");
-is($agent->status, 200);
+is($agent->status, HTTP::Status::HTTP_OK);
 
 $agent->get("$baseurl/index.html#/.");
-is($agent->status, 200);
+is($agent->status, HTTP::Status::HTTP_OK);
 
 done_testing;
diff --git a/t/web/ticket_display.t b/t/web/ticket_display.t
index 33336d7a6..7183b5ea7 100644
--- a/t/web/ticket_display.t
+++ b/t/web/ticket_display.t
@@ -44,7 +44,8 @@ diag "test ShowTicket right";
         'got no permission warning' );
 
 
-    $m->goto_ticket($id);
+    $m->goto_ticket($id, undef, HTTP::Status::HTTP_FORBIDDEN);
+    is($m->status, HTTP::Status::HTTP_FORBIDDEN, 'No permission');
     $m->content_contains( "No permission to view ticket",
         'got no permission msg' );
     $m->warning_like( qr/No permission to view ticket/, 'got warning' );

commit 7ad0ed9e1be23ee4427fde30603e69bf1df824ee
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Aug 18 17:47:41 2017 +0000

    Avoid spuriously unchecking all recipient checkboxes
    
    This 4.4.2 regression, introduced in
    f0a7105da8bc8f00a54bcdb5bfd48d788bbb888b and
    4b11cf37360ad5084f6428c62553ccc6858313a7, could happen if you caused a
    second refresh while the first one was still running. serializeArray()
    doesn't include disabled form fields, so what the AJAX request reported
    to the server made it look like the user had explicitly unchecked
    recipients.
    
    Fixes: I#33027

diff --git a/share/html/Ticket/Update.html b/share/html/Ticket/Update.html
index 075940306..8b4ba94c2 100644
--- a/share/html/Ticket/Update.html
+++ b/share/html/Ticket/Update.html
@@ -217,8 +217,18 @@ jQuery( function() {
        jQuery('#recipients div.titlebox-content').addClass('refreshing');
        jQuery('#previewscrips div.titlebox-content').addClass('refreshing');
 
+       /* temporarily re-enable the checkboxes so they will be included in
+          payload */
+       jQuery("#recipients input[name=TxnSendMailToAll], #recipients input[name=TxnSendMailTo]").attr('disabled', false);
+       jQuery("#previewscrips input[name=TxnSendMailToAll], #previewscrips input[name=TxnSendMailTo]").attr('disabled', false);
+
+       var payload = jQuery('form[name=TicketUpdate]').serializeArray();
+
+       jQuery("#recipients input[name=TxnSendMailToAll], #recipients input[name=TxnSendMailTo]").attr('disabled', true);
+       jQuery("#previewscrips input[name=TxnSendMailToAll], #previewscrips input[name=TxnSendMailTo]").attr('disabled', true);
+
        jQuery('#recipients div.titlebox-content').load( '<% RT->Config->Get('WebPath')%>/Helpers/ShowSimplifiedRecipients',
-           jQuery('form[name=TicketUpdate]').serializeArray(),
+           payload,
            function() {
                jQuery('#recipients div.titlebox-content').removeClass('refreshing');
                var txn_send_field = jQuery("#recipients input[name=TxnSendMailTo]");
@@ -232,7 +242,7 @@ jQuery( function() {
        );
 
        jQuery('#previewscrips div.titlebox-content').load( '<% RT->Config->Get('WebPath')%>/Helpers/PreviewScrips',
-           jQuery('form[name=TicketUpdate]').serializeArray(),
+           payload,
            function() {
                jQuery('#previewscrips div.titlebox-content').removeClass('refreshing');
                var txn_send_field = jQuery("#previewscrips input[name=TxnSendMailTo]");
@@ -244,9 +254,6 @@ jQuery( function() {
                }
            }
        );
-
-       jQuery("#recipients input[name=TxnSendMailToAll], #recipients input[name=TxnSendMailTo]").attr('disabled', true);
-       jQuery("#previewscrips input[name=TxnSendMailToAll], #previewscrips input[name=TxnSendMailTo]").attr('disabled', true);
    };
    updateScrips();
 

commit 2d9444120fb14f51061175a5e6c9425b11121d1e
Author: Maureen E. Mirville <maureen at bestpractical.com>
Date:   Tue Jan 23 14:03:58 2018 -0500

    Make RT owner dropdown limit a config option
    
    The Owner dropdown menu, used in various places in RT, automatically
    changes from a dropdown menu to an autocomplete field once there are
    greater than 50 values. As some users may want to change this limit,
    a new config option was added so it can easily be updated, rather
    than using a callback. Existing callbacks on this limit will continue
    to work as expected.

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 48fd0fe52..4ef8f0979 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1575,8 +1575,11 @@ builder are replaced by text fields that autocomplete.  This can
 alleviate the sometimes huge owner list for installations where many
 users have the OwnTicket right.
 
-Autocompleter is automatically turned on if list contains more than
-50 users, but penalty of executing potentially slow query is still paid.
+The Owner entry is automatically converted to an autocomplete box if the list
+of owners exceeds C<$DropdownMenuLimit> items. However, the query to generate
+the list of owners is still run and this can increase page load times. If
+your owner lists exceed the limit and you are using the autocomplete box, you
+can improve performance by explicitly setting C<$AutocompleteOwners>.
 
 Drop down doesn't show unprivileged users. If your setup allows unprivileged
 to own ticket then you have to enable autocompleting.
@@ -1585,6 +1588,22 @@ to own ticket then you have to enable autocompleting.
 
 Set($AutocompleteOwners, 0);
 
+=item C<$DropdownMenuLimit>
+
+The Owner dropdown menu, used in various places in RT including the Query
+Builder and ticket edit pages, automatically changes from a dropdown menu to
+an autocomplete field once the menu holds more than the C<$DropdownMenuLimit>
+owners. Dropdown menus become more difficult to use when they contain a large
+number of values and the autocomplete textbox can be more usable.
+
+If you have very large numbers of users who can be owners, this can cause
+slow page loads on pages with an Owner selection. See L</$AutocompleteOwners>
+for a way to potentially speed up page loads.
+
+=cut
+
+Set($DropdownMenuLimit, 50);
+
 =item C<$AutocompleteOwnersForSearch>
 
 If set to 1, the owner drop-downs for the query builder are always
diff --git a/share/html/Elements/SelectOwnerDropdown b/share/html/Elements/SelectOwnerDropdown
index 57f364125..a20a7c343 100644
--- a/share/html/Elements/SelectOwnerDropdown
+++ b/share/html/Elements/SelectOwnerDropdown
@@ -79,7 +79,7 @@ foreach my $object (@$Objects) {
     }
 }
 
-my $dropdown_limit = 50;
+my $dropdown_limit = RT->Config->Get( 'DropdownMenuLimit' ) || 50;
 $m->callback( CallbackName => 'ModifyDropdownLimit', DropdownLimit => \$dropdown_limit );
 
 if (keys(%user_uniq_hash) > $dropdown_limit ) {

commit 73ea73c1ec9316a9df1474b0e0851371610845a2
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Wed Jan 31 10:49:35 2018 -0500

    Add support for Queue Default Article for web UI
    
    For the Defaults page for Queues, add an option for a Default Article.
    Where the content of the Article will be loaded into the message box of
    any Ticket, on create in that Queue.

diff --git a/share/html/Admin/Queues/DefaultValues.html b/share/html/Admin/Queues/DefaultValues.html
index aff93cc0b..475985db1 100644
--- a/share/html/Admin/Queues/DefaultValues.html
+++ b/share/html/Admin/Queues/DefaultValues.html
@@ -71,6 +71,11 @@
         Grouping => 'Basics',
         InTable => 1,
     &>
+
+    <tr><td class="label"><&|/l&>Article</&>:</td>
+        <td><& /Elements/ArticleSelect, QueueObj => $queue, Default => $queue->DefaultValue('QueueDefaultArticle') &>
+    </td></tr>
+
     </table>
     </&>
 </div>
@@ -149,7 +154,7 @@ if ( $ARGS{Reset} ) {
     }
 }
 elsif ( $ARGS{Update} ) {
-    for my $field ( qw/InitialPriority FinalPriority Starts Due/ ) {
+    for my $field ( qw/InitialPriority FinalPriority Starts Due QueueDefaultArticle/ ) {
         my ($ret, $msg) = $queue->SetDefaultValue(
             Name => $field,
             Value => $ARGS{$field},
diff --git a/share/html/Articles/Elements/IncludeArticle b/share/html/Articles/Elements/IncludeArticle
index 5307875cf..fb4326cf4 100644
--- a/share/html/Articles/Elements/IncludeArticle
+++ b/share/html/Articles/Elements/IncludeArticle
@@ -78,6 +78,19 @@ foreach my $arg ( keys %$parent_args ) {
         Value => $parent_args->{$arg},
         Queue => $Queue->Id,
     );
+
+    # Check if Ticket id is present, if it is not we know we are creating a Ticket
+    if ( !$parent_args->{'id'} ){
+        my $queue_id = $parent_args->{'Queue'};
+
+        my $QueueObj = RT::Queue->new($session{'CurrentUser'});
+        $QueueObj->Load( $queue_id );
+
+        if ($QueueObj->DefaultValue('QueueDefaultArticle') ){
+            $article->LoadByCols( Name => $QueueObj->DefaultValue('QueueDefaultArticle') );
+        }
+    }
+
     next unless $article && $article->id;
 
     my $formatted_article = $m->scomp('/Articles/Article/Elements/Preformatted',
diff --git a/share/html/Articles/Elements/IncludeArticle b/share/html/Elements/ArticleSelect
similarity index 55%
copy from share/html/Articles/Elements/IncludeArticle
copy to share/html/Elements/ArticleSelect
index 5307875cf..6cb6e3df6 100644
--- a/share/html/Articles/Elements/IncludeArticle
+++ b/share/html/Elements/ArticleSelect
@@ -44,56 +44,31 @@
 %# works based on those contributions, and sublicense and distribute
 %# those contributions and any derivatives thereof.
 %#
-%# END BPS TAGGED BLOCK }}}
-<%INIT>
-
-my $parent_args = $m->caller_args(-1);
-
-my $name_prefix = '';
-$name_prefix = $ARGS{'Name'} .'-'
-    if $ARGS{'Name'}
-    && grep rindex($_, "$ARGS{'Name'}-Articles-", 0) == 0,
-        keys %$parent_args;
-
-foreach my $arg ( keys %$parent_args ) {
-    next if $name_prefix && substr($arg, 0, length($name_prefix)) ne $name_prefix;
 
-    my $Ticket = $ARGS{Ticket};
-    if ( !$Ticket and $parent_args->{id} and $parent_args->{id} ne 'new' ) {
-        $Ticket = RT::Ticket->new($session{'CurrentUser'});
-        $Ticket->Load($parent_args->{id});
-        unless ( $Ticket->id ) {
-            $RT::Logger->error("Couldn't load ticket ". $parent_args->{id} )
-        }
-    }
+<& "SelectArticle$Widget", articles => $articles, QueueObj => $QueueObj, Default => $Default &>
 
-    my $Queue = RT::Queue->new($session{CurrentUser});
-    if ($Ticket && $Ticket->Id) {
-        $Queue = $Ticket->QueueObj;
-    }
-
-    my $article = RT::Article->new($session{'CurrentUser'});
-    $article->LoadByInclude(
-        Field => substr($arg, length($name_prefix)),
-        Value => $parent_args->{$arg},
-        Queue => $Queue->Id,
-    );
-    next unless $article && $article->id;
+<%INIT>
 
-    my $formatted_article = $m->scomp('/Articles/Article/Elements/Preformatted',
-        Article => $article, Ticket => $Ticket
-    );
+my $articles = RT::Articles->new( $session{'CurrentUser'} );
+$articles->LimitHotlistClasses;
+$articles->LimitAppliedClasses( Queue => $QueueObj );
 
-    $m->callback( Article => $article, Ticket => $Ticket, formatted_article => \$formatted_article );
+my $dropdown_limit = 50;
+$m->callback( CallbackName => 'ModifyDropdownLimit', DropdownLimit => \$dropdown_limit );
 
-    if (RT->Config->Get('MessageBoxRichText',  $session{'CurrentUser'})) {
-        $formatted_article =~ s/>/>/g;
-        $formatted_article =~ s/</</g;
-        $formatted_article =~ s/&/&/g;
-        $formatted_article =~ s/\n/\n<br \/>/g;
-    }
-    $m->print($formatted_article);
+my $Widget =  $articles->Count > $dropdown_limit ? 'Autocomplete' : 'Dropdown';
 
+my $default_article = RT::Article->new($session{'CurrentUser'});
+# Default Article is "" if no value selected
+if ( defined $QueueObj->DefaultValue('QueueDefaultArticle') && $QueueObj->DefaultValue('QueueDefaultArticle') ne "") {
+    my ($ret, $msg) = $default_article->LoadByCol( Name => $QueueObj->DefaultValue('QueueDefaultArticle') );
+    RT::Logger->error($msg) unless $ret;
+    $Default = $default_article->Name;
 }
-return;
+
 </%INIT>
+
+<%ARGS>
+$QueueObj
+$Default => ""
+</%ARGS>
diff --git a/share/html/Articles/Elements/IncludeArticle b/share/html/Elements/SelectArticleAutocomplete
similarity index 55%
copy from share/html/Articles/Elements/IncludeArticle
copy to share/html/Elements/SelectArticleAutocomplete
index 5307875cf..19dbe1b92 100644
--- a/share/html/Articles/Elements/IncludeArticle
+++ b/share/html/Elements/SelectArticleAutocomplete
@@ -45,55 +45,8 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<%INIT>
+<input data-autocomplete="Articles" data-autocomplete-return="Name" name='QueueDefaultArticle' value="<% $Default %>" selected>
 
-my $parent_args = $m->caller_args(-1);
-
-my $name_prefix = '';
-$name_prefix = $ARGS{'Name'} .'-'
-    if $ARGS{'Name'}
-    && grep rindex($_, "$ARGS{'Name'}-Articles-", 0) == 0,
-        keys %$parent_args;
-
-foreach my $arg ( keys %$parent_args ) {
-    next if $name_prefix && substr($arg, 0, length($name_prefix)) ne $name_prefix;
-
-    my $Ticket = $ARGS{Ticket};
-    if ( !$Ticket and $parent_args->{id} and $parent_args->{id} ne 'new' ) {
-        $Ticket = RT::Ticket->new($session{'CurrentUser'});
-        $Ticket->Load($parent_args->{id});
-        unless ( $Ticket->id ) {
-            $RT::Logger->error("Couldn't load ticket ". $parent_args->{id} )
-        }
-    }
-
-    my $Queue = RT::Queue->new($session{CurrentUser});
-    if ($Ticket && $Ticket->Id) {
-        $Queue = $Ticket->QueueObj;
-    }
-
-    my $article = RT::Article->new($session{'CurrentUser'});
-    $article->LoadByInclude(
-        Field => substr($arg, length($name_prefix)),
-        Value => $parent_args->{$arg},
-        Queue => $Queue->Id,
-    );
-    next unless $article && $article->id;
-
-    my $formatted_article = $m->scomp('/Articles/Article/Elements/Preformatted',
-        Article => $article, Ticket => $Ticket
-    );
-
-    $m->callback( Article => $article, Ticket => $Ticket, formatted_article => \$formatted_article );
-
-    if (RT->Config->Get('MessageBoxRichText',  $session{'CurrentUser'})) {
-        $formatted_article =~ s/>/>/g;
-        $formatted_article =~ s/</</g;
-        $formatted_article =~ s/&/&/g;
-        $formatted_article =~ s/\n/\n<br \/>/g;
-    }
-    $m->print($formatted_article);
-
-}
-return;
-</%INIT>
+<%ARGS>
+$Default => ""
+</%ARGS>
\ No newline at end of file
diff --git a/share/html/Articles/Elements/IncludeArticle b/share/html/Elements/SelectArticleDropdown
similarity index 55%
copy from share/html/Articles/Elements/IncludeArticle
copy to share/html/Elements/SelectArticleDropdown
index 5307875cf..599b0c471 100644
--- a/share/html/Articles/Elements/IncludeArticle
+++ b/share/html/Elements/SelectArticleDropdown
@@ -45,55 +45,18 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<%INIT>
 
-my $parent_args = $m->caller_args(-1);
+<select name='QueueDefaultArticle'>
+<option value="">-</option>
+% while (my $article = $articles->Next) {
+<option <% ( $article->Name eq $Default) ? qq[ selected="selected"] : '' |n %>
+    value="<%$article->Name%>"
+><% $article->Name %></option>
+% }
+</select>
 
-my $name_prefix = '';
-$name_prefix = $ARGS{'Name'} .'-'
-    if $ARGS{'Name'}
-    && grep rindex($_, "$ARGS{'Name'}-Articles-", 0) == 0,
-        keys %$parent_args;
+<%ARGS>
+$articles
+$Default => undef
+</%ARGS>
 
-foreach my $arg ( keys %$parent_args ) {
-    next if $name_prefix && substr($arg, 0, length($name_prefix)) ne $name_prefix;
-
-    my $Ticket = $ARGS{Ticket};
-    if ( !$Ticket and $parent_args->{id} and $parent_args->{id} ne 'new' ) {
-        $Ticket = RT::Ticket->new($session{'CurrentUser'});
-        $Ticket->Load($parent_args->{id});
-        unless ( $Ticket->id ) {
-            $RT::Logger->error("Couldn't load ticket ". $parent_args->{id} )
-        }
-    }
-
-    my $Queue = RT::Queue->new($session{CurrentUser});
-    if ($Ticket && $Ticket->Id) {
-        $Queue = $Ticket->QueueObj;
-    }
-
-    my $article = RT::Article->new($session{'CurrentUser'});
-    $article->LoadByInclude(
-        Field => substr($arg, length($name_prefix)),
-        Value => $parent_args->{$arg},
-        Queue => $Queue->Id,
-    );
-    next unless $article && $article->id;
-
-    my $formatted_article = $m->scomp('/Articles/Article/Elements/Preformatted',
-        Article => $article, Ticket => $Ticket
-    );
-
-    $m->callback( Article => $article, Ticket => $Ticket, formatted_article => \$formatted_article );
-
-    if (RT->Config->Get('MessageBoxRichText',  $session{'CurrentUser'})) {
-        $formatted_article =~ s/>/>/g;
-        $formatted_article =~ s/</</g;
-        $formatted_article =~ s/&/&/g;
-        $formatted_article =~ s/\n/\n<br \/>/g;
-    }
-    $m->print($formatted_article);
-
-}
-return;
-</%INIT>

commit b4a79f43dc0eaf9af2540bf1cc3601d7ed8476ae
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Mon Feb 12 13:09:36 2018 -0500

    Add autocomplete for Articles

diff --git a/share/html/Helpers/Autocomplete/Articles b/share/html/Helpers/Autocomplete/Articles
new file mode 100644
index 000000000..74f02f034
--- /dev/null
+++ b/share/html/Helpers/Autocomplete/Articles
@@ -0,0 +1,89 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
+%#                                          <sales at 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 }}}
+% $r->content_type('application/json; charset=utf-8');
+<% JSON( \@suggestions ) |n %>
+% $m->abort;
+<%ARGS>
+$term       => undef
+$max        => 10
+$op         => 'STARTSWITH'
+$right      => undef
+$return     => 'Name'
+</%ARGS>
+<%INIT>
+# Only allow certain return fields
+$return = 'Name'
+    unless $return =~ /^(?:id|Name)$/;
+
+$m->abort unless defined $return
+             and defined $term
+             and length $term;
+
+# Sanity check the operator
+$op = 'STARTSWITH' unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+
+$m->callback( CallbackName => 'ModifyMaxResults', max => \$max );
+
+my $articles = RT::Articles->new( $session{CurrentUser} );
+$articles->RowsPerPage( $max );
+$articles->Limit(
+    FIELD           => 'Name',
+    OPERATOR        => $op,
+    VALUE           => $term,
+    ENTRYAGGREGATOR => 'OR',
+    CASESENSITIVE   => 0,
+);
+
+my @suggestions;
+while (my $a = $articles->Next) {
+    next if $right and not $a->CurrentUserHasRight($right);
+    my $value = $a->$return;
+    push @suggestions, { label => $a->Name, value => $value };
+    $m->callback( CallbackName => "ModifySuggestion", suggestions => @suggestions, label => $a );
+}
+</%INIT>
diff --git a/share/static/js/autocomplete.js b/share/static/js/autocomplete.js
index cd8ab2b0d..f5d0b46e7 100644
--- a/share/static/js/autocomplete.js
+++ b/share/static/js/autocomplete.js
@@ -5,7 +5,8 @@ window.RT.Autocomplete.Classes = {
     Users: 'user',
     Groups: 'group',
     Tickets: 'tickets',
-    Queues: 'queues'
+    Queues: 'queues',
+    Articles: 'articles'
 };
 
 window.RT.Autocomplete.bind = function(from) {

commit 54242d489829d4248fb26970c8e5b8ef634151ab
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Wed Feb 7 15:52:26 2018 -0500

    Test Ticket create web UI when Default Article
    
    Test that when a Queue Default Value is selected for Article, the
    content of the Article is loaded on the Ticket create page.

diff --git a/t/web/ticket-create-utf8.t b/t/web/ticket-create-utf8.t
index ebb2d5eab..25db00e3e 100644
--- a/t/web/ticket-create-utf8.t
+++ b/t/web/ticket-create-utf8.t
@@ -2,7 +2,7 @@
 use strict;
 use warnings;
 
-use RT::Test tests => 43;
+use RT::Test tests => 49;
 
 my $ru_test = "\x{442}\x{435}\x{441}\x{442}";
 my $ru_support = "\x{43f}\x{43e}\x{434}\x{434}\x{435}\x{440}\x{436}\x{43a}\x{430}";
@@ -86,3 +86,23 @@ foreach my $test_str ( $ru_test, $l1_test ) {
     }
 }
 
+my $article = RT::Article->new($RT::SystemUser);
+my ( $id, $msg ) = $article->Create(
+    Class   => 'General',
+    Name    => 'My Article',
+    'CustomField-Content' => 'My Article Test Content',
+);
+ok( $id, $msg );
+(my $ret, $msg) = $article->Load(1);
+ok ($ret, $msg);
+
+my $queue = RT::Queue->new(RT->SystemUser);
+$queue->Load('General');
+ok( $queue, 'Loaded General Queue' );
+($ret, $msg) = $queue->SetDefaultValue( Name => 'QueueDefaultArticle', Value => $article->Name);
+ok( $ret, $msg );
+
+ok $m->login(root => 'password'), "logged in";
+$m->goto_create_ticket('General');
+$m->scraped_id_is('Content', '#1: My Article <br />-------------- <br />Content: <br />------- <br />My Article Test Content <br />');
+

-----------------------------------------------------------------------


More information about the rt-commit mailing list