[Bps-public-commit] rt-extension-rightsdebugger branch, master, updated. 78eacafa3fc8d203d089693eca2ba23cdb389819

Shawn Moore shawn at bestpractical.com
Tue Mar 7 12:54:28 EST 2017


The branch, master has been updated
       via  78eacafa3fc8d203d089693eca2ba23cdb389819 (commit)
       via  6fb27e1bd63ad72cbfaf7dc5fc065cc48d9e1545 (commit)
       via  7a39182e93d16bd2374d20f43bbb6b649e61135d (commit)
       via  9eb1e283c8f45c089bc3ae314dd57a895dfc1f67 (commit)
       via  b947d9b018c7aab88cccd55048911b82fd4a1312 (commit)
       via  1418689efa49906ed713afb6dffafa2d4d35d195 (commit)
       via  d0b8fb5be4ecc29f4dd955dcbc1da7a0118e5069 (commit)
       via  e968b96855948b646e38fa9408b88f6b99bd8a27 (commit)
       via  8200ecb7a18d503a25fcf68f3cf552c9aafd4611 (commit)
       via  acd22f7f5da97724a4ab172923f4b913b8a31cfc (commit)
       via  ff256c00d17a21b98df707cc53f669c2ec84922e (commit)
       via  3a74dff552e010bb16ccf4cd677ff60455232fe6 (commit)
       via  e9b092dffcf8627fa2c8e034a95258751a1b5412 (commit)
       via  4f5dfb6c19a947f92a665d8c2b4bc1b6c0417760 (commit)
       via  24396d15038d435d19e36173471e7a9894b65636 (commit)
       via  0a9ac7e46dfc9c366c9508725706285ed26ba023 (commit)
       via  f73920b67c1b8ecb42cb6ef3b546b579f853ba0c (commit)
       via  5efa0447aebae97b785788e302c5f8d4da7d5dc0 (commit)
      from  261bfdcf56f1e7be5e540e00db9594b1cac598ab (commit)

Summary of changes:
 html/Admin/RightsDebugger/index.html |   7 +
 html/Helpers/RightsDebugger/Search   |  96 +-----------
 lib/RT/Extension/RightsDebugger.pm   | 275 ++++++++++++++++++++++++++++++++---
 static/css/rights-debugger.css       |  27 +++-
 static/js/rights-debugger.js         |  78 +++++++---
 5 files changed, 349 insertions(+), 134 deletions(-)

- Log -----------------------------------------------------------------
commit 5efa0447aebae97b785788e302c5f8d4da7d5dc0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 15:20:56 2017 +0000

    Make revoke cell only as wide as it needs to be

diff --git a/static/css/rights-debugger.css b/static/css/rights-debugger.css
index 53c94e4..6057eeb 100644
--- a/static/css/rights-debugger.css
+++ b/static/css/rights-debugger.css
@@ -4,6 +4,11 @@
     width: 15em;
 }
 
+#rights-debugger .results .result .cell.revoke {
+    display: inline-block;
+    width: auto;
+}
+
 #rights-debugger .search .loading {
     display: none;
 }

commit f73920b67c1b8ecb42cb6ef3b546b579f853ba0c
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 15:29:46 2017 +0000

    Truncate long object names

diff --git a/static/css/rights-debugger.css b/static/css/rights-debugger.css
index 6057eeb..9c7ff77 100644
--- a/static/css/rights-debugger.css
+++ b/static/css/rights-debugger.css
@@ -4,6 +4,11 @@
     width: 15em;
 }
 
+#rights-debugger .results .result .cell {
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
 #rights-debugger .results .result .cell.revoke {
     display: inline-block;
     width: auto;

commit 0a9ac7e46dfc9c366c9508725706285ed26ba023
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 15:30:31 2017 +0000

    Use ->Subject rather than nonexistent ->Name for rendering Tickets

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index 099ea6a..30a468b 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -82,6 +82,10 @@ sub LabelForRecord {
     my $self = shift;
     my $record = shift;
 
+    if ($record->isa('RT::Ticket')) {
+        return $record->Subject;
+    }
+
     return $record->Name;
 }
 

commit 24396d15038d435d19e36173471e7a9894b65636
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 15:34:16 2017 +0000

    Move search implementation from /Helpers into lib/

diff --git a/html/Helpers/RightsDebugger/Search b/html/Helpers/RightsDebugger/Search
index c5d0698..6481e45 100644
--- a/html/Helpers/RightsDebugger/Search
+++ b/html/Helpers/RightsDebugger/Search
@@ -1,97 +1,6 @@
 <%INIT>
-my @results;
-my %search;
-
-my $ACL = RT::ACL->new($session{CurrentUser});
-
-my $has_search = 0;
-
-if ($ARGS{right}) {
-    $has_search = 1;
-    for my $word (split ' ', $ARGS{right}) {
-        $ACL->Limit(
-            FIELD           => 'RightName',
-            OPERATOR        => 'LIKE',
-            VALUE           => $word,
-            CASESENSITIVE   => 0,
-            ENTRYAGGREGATOR => 'OR',
-        );
-    }
-}
-
-$ACL->UnLimit unless $has_search;
-
-for my $key (qw/principal object right/) {
-    if (my $search = $ARGS{$key}) {
-        my @matchers;
-        for my $word (split ' ', $search) {
-            push @matchers, qr/\Q$word\E/i;
-        }
-        $search{$key} = \@matchers;
-    }
-}
-
-my $escape_html = sub {
-    my $s = shift;
-    RT::Interface::Web::EscapeHTML(\$s);
-    return $s;
-};
-
-my $highlight_term = sub {
-    my ($text, $re) = @_;
-
-    $text =~ s{
-        \G         # where we left off the previous iteration thanks to /g
-        (.*?)      # non-matching text before the match
-        ($re|$)    # matching text, or the end of the line (to escape any
-                   # text after the last match)
-    }{
-      $escape_html->($1) .
-      (length $2 ? '<span class="match">' . $escape_html->($2) . '</span>' : '')
-    }xeg;
-
-    return $text; # now escape as html
-};
-
-ACE: while (my $ACE = $ACL->Next) {
-    my $serialized = RT::Extension::RightsDebugger->SerializeACE($ACE);
-
-    # this is hacky, but doing the searching in SQL is absolutely a nonstarter
-    for my $key (qw/principal object/) {
-        if (my $matchers = $search{$key}) {
-            my $record = $serialized->{$key};
-            for my $re (@$matchers) {
-                next ACE unless $record->{class}  =~ $re
-                             || $record->{id}     =~ $re
-                             || $record->{label}  =~ $re
-                             || $record->{detail} =~ $re;
-            }
-        }
-    }
-
-    # highlight matching words
-    $serialized->{right_highlighted} = $highlight_term->($serialized->{right}, join '|', @{ $search{right} || [] });
-
-    for my $key (qw/principal object/) {
-        my $record = $serialized->{$key};
-
-        if (my $matchers = $search{$key}) {
-            my $re = join '|', @$matchers;
-            for my $column (qw/label detail/) {
-                $record->{$column . '_highlighted'} = $highlight_term->($record->{$column}, $re);
-            }
-        }
-
-        for my $column (qw/label detail/) {
-            # make sure we escape html if there was no search
-            $record->{$column . '_highlighted'} //= $escape_html->($record->{$column});
-        }
-    }
-
-    push @results, $serialized;
-}
-
+my $results = RT::Extension::RightsDebugger->Search(%ARGS);
 $r->content_type('application/json; charset=utf-8');
-$m->out(JSON({ count => $ACL->Count, results => \@results }));
+$m->out(JSON({ results => $results }));
 $m->abort;
 </%INIT>
diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index 30a468b..d124740 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -11,6 +11,110 @@ RT->AddJavaScript("handlebars-4.0.6.min.js");
 
 $RT::Interface::Web::WHITELISTED_COMPONENT_ARGS{'/Admin/RightsDebugger/index.html'} = ['Principal', 'Object', 'Right'];
 
+sub Search {
+    my $self = shift;
+    my %args = (
+        principal => '',
+        object    => '',
+        right     => '',
+        @_,
+    );
+
+    my @results;
+    my %search;
+
+    my $ACL = RT::ACL->new($HTML::Mason::Commands::session{CurrentUser});
+
+    my $has_search = 0;
+
+    if ($args{right}) {
+        $has_search = 1;
+        for my $word (split ' ', $args{right}) {
+            $ACL->Limit(
+                FIELD           => 'RightName',
+                OPERATOR        => 'LIKE',
+                VALUE           => $word,
+                CASESENSITIVE   => 0,
+                ENTRYAGGREGATOR => 'OR',
+            );
+        }
+    }
+
+    $ACL->UnLimit unless $has_search;
+
+    for my $key (qw/principal object right/) {
+        if (my $search = $args{$key}) {
+            my @matchers;
+            for my $word (split ' ', $search) {
+                push @matchers, qr/\Q$word\E/i;
+            }
+            $search{$key} = \@matchers;
+        }
+    }
+
+    my $escape_html = sub {
+        my $s = shift;
+        RT::Interface::Web::EscapeHTML(\$s);
+        return $s;
+    };
+
+    my $highlight_term = sub {
+        my ($text, $re) = @_;
+
+        $text =~ s{
+            \G         # where we left off the previous iteration thanks to /g
+            (.*?)      # non-matching text before the match
+            ($re|$)    # matching text, or the end of the line (to escape any
+                       # text after the last match)
+        }{
+          $escape_html->($1) .
+          (length $2 ? '<span class="match">' . $escape_html->($2) . '</span>' : '')
+        }xeg;
+
+        return $text; # now escape as html
+    };
+
+    ACE: while (my $ACE = $ACL->Next) {
+        my $serialized = $self->SerializeACE($ACE);
+
+        # this is hacky, but doing the searching in SQL is absolutely a nonstarter
+        for my $key (qw/principal object/) {
+            if (my $matchers = $search{$key}) {
+                my $record = $serialized->{$key};
+                for my $re (@$matchers) {
+                    next ACE unless $record->{class}  =~ $re
+                                 || $record->{id}     =~ $re
+                                 || $record->{label}  =~ $re
+                                 || $record->{detail} =~ $re;
+                }
+            }
+        }
+
+        # highlight matching words
+        $serialized->{right_highlighted} = $highlight_term->($serialized->{right}, join '|', @{ $search{right} || [] });
+
+        for my $key (qw/principal object/) {
+            my $record = $serialized->{$key};
+
+            if (my $matchers = $search{$key}) {
+                my $re = join '|', @$matchers;
+                for my $column (qw/label detail/) {
+                    $record->{$column . '_highlighted'} = $highlight_term->($record->{$column}, $re);
+                }
+            }
+
+            for my $column (qw/label detail/) {
+                # make sure we escape html if there was no search
+                $record->{$column . '_highlighted'} //= $escape_html->($record->{$column});
+            }
+        }
+
+        push @results, $serialized;
+    }
+
+    return \@results;
+}
+
 sub SerializeACE {
     my $self = shift;
     my $ACE = shift;

commit 4f5dfb6c19a947f92a665d8c2b4bc1b6c0417760
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 15:36:54 2017 +0000

    Refactor lexical subs as private functions

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index d124740..c5f078f 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -11,6 +11,28 @@ RT->AddJavaScript("handlebars-4.0.6.min.js");
 
 $RT::Interface::Web::WHITELISTED_COMPONENT_ARGS{'/Admin/RightsDebugger/index.html'} = ['Principal', 'Object', 'Right'];
 
+sub _EscapeHTML {
+    my $s = shift;
+    RT::Interface::Web::EscapeHTML(\$s);
+    return $s;
+}
+
+sub _HighlightTerm {
+    my ($text, $re) = @_;
+
+    $text =~ s{
+        \G         # where we left off the previous iteration thanks to /g
+        (.*?)      # non-matching text before the match
+        ($re|$)    # matching text, or the end of the line (to escape any
+                   # text after the last match)
+    }{
+      _EscapeHTML($1) .
+      (length $2 ? '<span class="match">' . _EscapeHTML($2) . '</span>' : '')
+    }xeg;
+
+    return $text; # now escaped as html
+}
+
 sub Search {
     my $self = shift;
     my %args = (
@@ -52,28 +74,6 @@ sub Search {
         }
     }
 
-    my $escape_html = sub {
-        my $s = shift;
-        RT::Interface::Web::EscapeHTML(\$s);
-        return $s;
-    };
-
-    my $highlight_term = sub {
-        my ($text, $re) = @_;
-
-        $text =~ s{
-            \G         # where we left off the previous iteration thanks to /g
-            (.*?)      # non-matching text before the match
-            ($re|$)    # matching text, or the end of the line (to escape any
-                       # text after the last match)
-        }{
-          $escape_html->($1) .
-          (length $2 ? '<span class="match">' . $escape_html->($2) . '</span>' : '')
-        }xeg;
-
-        return $text; # now escape as html
-    };
-
     ACE: while (my $ACE = $ACL->Next) {
         my $serialized = $self->SerializeACE($ACE);
 
@@ -91,7 +91,7 @@ sub Search {
         }
 
         # highlight matching words
-        $serialized->{right_highlighted} = $highlight_term->($serialized->{right}, join '|', @{ $search{right} || [] });
+        $serialized->{right_highlighted} = _HighlightTerm($serialized->{right}, join '|', @{ $search{right} || [] });
 
         for my $key (qw/principal object/) {
             my $record = $serialized->{$key};
@@ -99,13 +99,13 @@ sub Search {
             if (my $matchers = $search{$key}) {
                 my $re = join '|', @$matchers;
                 for my $column (qw/label detail/) {
-                    $record->{$column . '_highlighted'} = $highlight_term->($record->{$column}, $re);
+                    $record->{$column . '_highlighted'} = _HighlightTerm($record->{$column}, $re);
                 }
             }
 
             for my $column (qw/label detail/) {
                 # make sure we escape html if there was no search
-                $record->{$column . '_highlighted'} //= $escape_html->($record->{$column});
+                $record->{$column . '_highlighted'} //= _EscapeHTML($record->{$column});
             }
         }
 

commit e9b092dffcf8627fa2c8e034a95258751a1b5412
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 15:41:12 2017 +0000

    Factor out _HighlightSerializedForSearch

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index c5f078f..0edf1b8 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -33,6 +33,32 @@ sub _HighlightTerm {
     return $text; # now escaped as html
 }
 
+sub _HighlightSerializedForSearch {
+    my $serialized = shift;
+    my $search     = shift;
+
+    # highlight matching words
+    $serialized->{right_highlighted} = _HighlightTerm($serialized->{right}, join '|', @{ $search->{right} || [] });
+
+    for my $key (qw/principal object/) {
+        my $record = $serialized->{$key};
+
+        if (my $matchers = $search->{$key}) {
+            my $re = join '|', @$matchers;
+            for my $column (qw/label detail/) {
+                $record->{$column . '_highlighted'} = _HighlightTerm($record->{$column}, $re);
+            }
+        }
+
+        for my $column (qw/label detail/) {
+            # make sure we escape html if there was no search
+            $record->{$column . '_highlighted'} //= _EscapeHTML($record->{$column});
+        }
+    }
+
+    return;
+}
+
 sub Search {
     my $self = shift;
     my %args = (
@@ -90,24 +116,7 @@ sub Search {
             }
         }
 
-        # highlight matching words
-        $serialized->{right_highlighted} = _HighlightTerm($serialized->{right}, join '|', @{ $search{right} || [] });
-
-        for my $key (qw/principal object/) {
-            my $record = $serialized->{$key};
-
-            if (my $matchers = $search{$key}) {
-                my $re = join '|', @$matchers;
-                for my $column (qw/label detail/) {
-                    $record->{$column . '_highlighted'} = _HighlightTerm($record->{$column}, $re);
-                }
-            }
-
-            for my $column (qw/label detail/) {
-                # make sure we escape html if there was no search
-                $record->{$column . '_highlighted'} //= _EscapeHTML($record->{$column});
-            }
-        }
+        _HighlightSerializedForSearch($serialized, \%search);
 
         push @results, $serialized;
     }

commit 3a74dff552e010bb16ccf4cd677ff60455232fe6
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 15:47:32 2017 +0000

    Support ORing principal and object search results
    
    RightName already works this way
    
    We'll have special support queue:1 etc for helping to avoid spurious
    matches

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index 0edf1b8..fbaf13f 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -104,15 +104,18 @@ sub Search {
         my $serialized = $self->SerializeACE($ACE);
 
         # this is hacky, but doing the searching in SQL is absolutely a nonstarter
-        for my $key (qw/principal object/) {
+        KEY: for my $key (qw/principal object/) {
             if (my $matchers = $search{$key}) {
                 my $record = $serialized->{$key};
                 for my $re (@$matchers) {
-                    next ACE unless $record->{class}  =~ $re
-                                 || $record->{id}     =~ $re
-                                 || $record->{label}  =~ $re
-                                 || $record->{detail} =~ $re;
+                    next KEY if $record->{class}  =~ $re
+                             || $record->{id}     =~ $re
+                             || $record->{label}  =~ $re
+                             || $record->{detail} =~ $re;
                 }
+
+                # no matches
+                next ACE;
             }
         }
 

commit ff256c00d17a21b98df707cc53f669c2ec84922e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 16:13:09 2017 +0000

    Implement chunked search result loading
    
    I'm sensitive to performance since this is not using SQL for searching
    
    Our modest sized RT has ~1500 ACLs, so waiting for the entire resultset
    to be processed might take long enough that (assuming we don't time out) the
    user will get annoyed

diff --git a/html/Admin/RightsDebugger/index.html b/html/Admin/RightsDebugger/index.html
index 38ebce1..7af2af6 100644
--- a/html/Admin/RightsDebugger/index.html
+++ b/html/Admin/RightsDebugger/index.html
@@ -10,6 +10,7 @@
   </div>
   <div class="results">
   </div>
+  <span class="loading"><img src="<%RT->Config->Get('WebPath')%>/static/images/loading.gif" alt="<%loc('Loading')%>" title="<%loc('Loading')%>" /></span>
 </form>
 
 <script type="text/x-template" id="debugger-record-template">
diff --git a/html/Helpers/RightsDebugger/Search b/html/Helpers/RightsDebugger/Search
index 6481e45..9ce83e6 100644
--- a/html/Helpers/RightsDebugger/Search
+++ b/html/Helpers/RightsDebugger/Search
@@ -1,6 +1,6 @@
 <%INIT>
 my $results = RT::Extension::RightsDebugger->Search(%ARGS);
 $r->content_type('application/json; charset=utf-8');
-$m->out(JSON({ results => $results }));
+$m->out(JSON($results));
 $m->abort;
 </%INIT>
diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index fbaf13f..bab43c2 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -88,8 +88,19 @@ sub Search {
         }
     }
 
+    if ($args{continueAfter}) {
+        $has_search = 1;
+        $ACL->Limit(
+            FIELD    => 'id',
+            OPERATOR => '>',
+            VALUE    => $args{continueAfter},
+        );
+    }
+
     $ACL->UnLimit unless $has_search;
 
+    $ACL->RowsPerPage(100);
+
     for my $key (qw/principal object right/) {
         if (my $search = $args{$key}) {
             my @matchers;
@@ -100,7 +111,10 @@ sub Search {
         }
     }
 
+    my $continueAfter;
+
     ACE: while (my $ACE = $ACL->Next) {
+        $continueAfter = $ACE->Id;
         my $serialized = $self->SerializeACE($ACE);
 
         # this is hacky, but doing the searching in SQL is absolutely a nonstarter
@@ -124,7 +138,14 @@ sub Search {
         push @results, $serialized;
     }
 
-    return \@results;
+    # if we didn't fill the whole page, then we know there are
+    # no more rows to consider
+    undef $continueAfter if $ACL->Count < $ACL->RowsPerPage;
+
+    return {
+        results => \@results,
+        continueAfter => $continueAfter,
+    };
 }
 
 sub SerializeACE {
diff --git a/static/css/rights-debugger.css b/static/css/rights-debugger.css
index 9c7ff77..07c7fb7 100644
--- a/static/css/rights-debugger.css
+++ b/static/css/rights-debugger.css
@@ -14,17 +14,23 @@
     width: auto;
 }
 
-#rights-debugger .search .loading {
+#rights-debugger .search .loading,
+#rights-debugger > .loading {
     display: none;
 }
 
 #rights-debugger .search .loading img,
+#rights-debugger > .loading img,
 #rights-debugger .results .revoke img {
     height: 1.5em;
     width: 1.5em;
 }
 
-#rights-debugger.refreshing .search .loading {
+#rights-debugger.awaiting-first-result .search .loading {
+    display: inline;
+}
+
+#rights-debugger.continuing-load > .loading {
     display: inline;
 }
 
@@ -33,7 +39,7 @@
     font-weight: bold;
 }
 
-#rights-debugger.refreshing .results {
+#rights-debugger.awaiting-first-result .results {
     opacity: 0.5;
 }
 
diff --git a/static/js/rights-debugger.js b/static/js/rights-debugger.js
index 9d72fcc..ab408db 100644
--- a/static/js/rights-debugger.js
+++ b/static/js/rights-debugger.js
@@ -28,33 +28,33 @@ jQuery(function () {
     };
 
     var displayRevoking = function (button) {
+        if (button.hasClass('ui-state-disabled')) {
+            return;
+        }
+
         button.addClass('ui-state-disabled').prop('disabled', true);
         button.after(loading.clone());
     };
 
-    var refreshResults = function () {
-        form.addClass('refreshing');
-        form.find('button').addClass('ui-state-disabled').prop('disabled', true);
-
-        var serialized = form.serializeArray();
-        var search = {};
-        jQuery.each(serialized, function(i, field){
-            search[field.name] = field.value;
-        });
-
-        if (existingRequest) {
-            existingRequest.abort();
-        }
+    var requestPage;
+    requestPage = function (search, continueAfter) {
+        search.continueAfter = continueAfter;
 
         existingRequest = jQuery.ajax({
             url: form.attr('action'),
             data: search,
             timeout: 30000, /* 30 seconds */
             success: function (response) {
-                form.removeClass('refreshing').removeClass('error');
-                display.empty();
+                form.removeClass('error');
 
                 var items = response.results;
+
+                /* change UI only after we find a result */
+                if (items.length && form.hasClass('awaiting-first-result')) {
+                    display.empty();
+                    form.removeClass('awaiting-first-result').addClass('continuing-load');
+                }
+
                 jQuery.each(items, function (i, item) {
                     display.append(renderItem({ search: search, item: item }));
                 });
@@ -63,19 +63,50 @@ jQuery(function () {
                     var revokeButton = buttonForAction(key);
                     displayRevoking(revokeButton);
                 });
+
+                if (response.continueAfter) {
+                    requestPage(search, response.continueAfter);
+                }
+                else {
+                    form.removeClass('continuing-load');
+
+                    if (form.hasClass('awaiting-first-result')) {
+                        display.empty();
+                        form.removeClass('awaiting-first-result');
+                        display.text('No results');
+                    }
+                }
             },
             error: function (xhr, reason) {
                 if (reason == 'abort') {
                     return;
                 }
 
-                form.removeClass('refreshing').addClass('error');
+                form.removeClass('awaiting-first-result').removeClass('continuing-load').addClass('error');
                 display.empty();
                 display.text('Error: ' + xhr.statusText);
             }
         });
     };
 
+    var beginSearch = function () {
+        form.removeClass('continuing-load').addClass('awaiting-first-result');
+        form.find('button').addClass('ui-state-disabled').prop('disabled', true);
+
+        var serialized = form.serializeArray();
+        var search = {};
+
+        jQuery.each(serialized, function(i, field){
+            search[field.name] = field.value;
+        });
+
+        if (existingRequest) {
+            existingRequest.abort();
+        }
+
+        requestPage(search, 0);
+    };
+
     display.on('click', '.revoke button', function (e) {
         e.preventDefault();
         var button = jQuery(e.target);
@@ -108,8 +139,8 @@ jQuery(function () {
     });
 
     form.find('.search input').on('input', function () {
-        refreshResults();
+        beginSearch();
     });
 
-    refreshResults();
+    beginSearch();
 });

commit acd22f7f5da97724a4ab172923f4b913b8a31cfc
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 16:37:47 2017 +0000

    Handle successful error responses from server
    
    e.g. unable to find user

diff --git a/static/js/rights-debugger.js b/static/js/rights-debugger.js
index ab408db..e3f03f0 100644
--- a/static/js/rights-debugger.js
+++ b/static/js/rights-debugger.js
@@ -36,6 +36,12 @@ jQuery(function () {
         button.after(loading.clone());
     };
 
+    var displayError = function (message) {
+        form.removeClass('awaiting-first-result').removeClass('continuing-load').addClass('error');
+        display.empty();
+        display.text('Error: ' + message);
+    }
+
     var requestPage;
     requestPage = function (search, continueAfter) {
         search.continueAfter = continueAfter;
@@ -45,6 +51,11 @@ jQuery(function () {
             data: search,
             timeout: 30000, /* 30 seconds */
             success: function (response) {
+                if (response.error) {
+                    displayError(response.error);
+                    return;
+                }
+
                 form.removeClass('error');
 
                 var items = response.results;
@@ -82,9 +93,7 @@ jQuery(function () {
                     return;
                 }
 
-                form.removeClass('awaiting-first-result').removeClass('continuing-load').addClass('error');
-                display.empty();
-                display.text('Error: ' + xhr.statusText);
+                displayError(xhr.statusText);
             }
         });
     };

commit 8200ecb7a18d503a25fcf68f3cf552c9aafd4611
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 16:51:18 2017 +0000

    Factor out a ->CurrentUser

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index bab43c2..ea1afe3 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -11,6 +11,10 @@ RT->AddJavaScript("handlebars-4.0.6.min.js");
 
 $RT::Interface::Web::WHITELISTED_COMPONENT_ARGS{'/Admin/RightsDebugger/index.html'} = ['Principal', 'Object', 'Right'];
 
+sub CurrentUser {
+    return $HTML::Mason::Commands::session{CurrentUser};
+}
+
 sub _EscapeHTML {
     my $s = shift;
     RT::Interface::Web::EscapeHTML(\$s);
@@ -71,7 +75,7 @@ sub Search {
     my @results;
     my %search;
 
-    my $ACL = RT::ACL->new($HTML::Mason::Commands::session{CurrentUser});
+    my $ACL = RT::ACL->new($self->CurrentUser);
 
     my $has_search = 0;
 

commit e968b96855948b646e38fa9408b88f6b99bd8a27
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 16:51:42 2017 +0000

    Inspecting a single principal and its group memberships
    
    When you specify e.g. user:root, it will also look up all groups root is in
    for ACLs. This is part of the answer to the question "why does this specific
    user have this right on this record?"

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index ea1afe3..8f884a9 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -63,6 +63,35 @@ sub _HighlightSerializedForSearch {
     return;
 }
 
+sub _PrincipalForSpec {
+    my $self       = shift;
+    my $type       = shift;
+    my $identifier = shift;
+
+    if ($type =~ /^g/i) {
+        my $group = RT::Group->new($self->CurrentUser);
+        if ( $identifier =~ /^\d+$/ ) {
+            $group->LoadByCols(
+                id => $identifier,
+            );
+        } else {
+            $group->LoadByCols(
+                Domain => 'UserDefined',
+                Name   => $identifier,
+            );
+        }
+
+        return $group->PrincipalObj if $group->Id;
+    }
+    else {
+        my $user = RT::User->new($self->CurrentUser);
+        $user->Load($identifier);
+        return $user->PrincipalObj if $user->Id;
+    }
+
+    return undef;
+}
+
 sub Search {
     my $self = shift;
     my %args = (
@@ -78,6 +107,53 @@ sub Search {
     my $ACL = RT::ACL->new($self->CurrentUser);
 
     my $has_search = 0;
+    my %use_regex_search_for = (
+        principal => 1,
+        object    => 1,
+    );
+
+    if ($args{principal}) {
+        for my $word (split ' ', $args{principal}) {
+            if (my ($type, $identifier) = $word =~ m{
+                ^
+                    (u|user|g|group)
+                    [:#]
+                    (\S+)
+                $
+            }xi) {
+                my $principal = $self->_PrincipalForSpec($type, $identifier);
+                if (!$principal) {
+                    return { error => 'Unable to find row' };
+                }
+
+                $has_search = 1;
+                $use_regex_search_for{principal} = 0;
+
+                my $principal_alias = $ACL->Join(
+                    ALIAS1 => 'main',
+                    FIELD1 => 'PrincipalId',
+                    TABLE2 => 'Principals',
+                    FIELD2 => 'id',
+                );
+                my $cgm_alias = $ACL->Join(
+                    ALIAS1 => 'main',
+                    FIELD1 => 'PrincipalId',
+                    TABLE2 => 'CachedGroupMembers',
+                    FIELD2 => 'GroupId',
+                );
+                $ACL->Limit(
+                    ALIAS => $cgm_alias,
+                    FIELD => 'MemberId',
+                    VALUE => $principal->Id,
+                );
+                $ACL->Limit(
+                    ALIAS => $cgm_alias,
+                    FIELD => 'Disabled',
+                    VALUE => 0,
+                );
+            }
+        }
+    }
 
     if ($args{right}) {
         $has_search = 1;
@@ -123,6 +199,8 @@ sub Search {
 
         # this is hacky, but doing the searching in SQL is absolutely a nonstarter
         KEY: for my $key (qw/principal object/) {
+            next KEY unless $use_regex_search_for{$key};
+
             if (my $matchers = $search{$key}) {
                 my $record = $serialized->{$key};
                 for my $re (@$matchers) {

commit d0b8fb5be4ecc29f4dd955dcbc1da7a0118e5069
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 16:55:04 2017 +0000

    Reuse principal and CGM joins
    
    This also happens to fix a bug where "user:root user:Nobody" returned no
    results, instead of the joined results for "user:root" OR "user:Nobody"

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index 8f884a9..cfc4f8e 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -113,6 +113,8 @@ sub Search {
     );
 
     if ($args{principal}) {
+        my ($principal_alias, $cgm_alias);
+
         for my $word (split ' ', $args{principal}) {
             if (my ($type, $identifier) = $word =~ m{
                 ^
@@ -129,28 +131,32 @@ sub Search {
                 $has_search = 1;
                 $use_regex_search_for{principal} = 0;
 
-                my $principal_alias = $ACL->Join(
+                $principal_alias ||= $ACL->Join(
                     ALIAS1 => 'main',
                     FIELD1 => 'PrincipalId',
                     TABLE2 => 'Principals',
                     FIELD2 => 'id',
                 );
-                my $cgm_alias = $ACL->Join(
-                    ALIAS1 => 'main',
-                    FIELD1 => 'PrincipalId',
-                    TABLE2 => 'CachedGroupMembers',
-                    FIELD2 => 'GroupId',
-                );
+
+                if (!$cgm_alias) {
+                    $cgm_alias = $ACL->Join(
+                        ALIAS1 => 'main',
+                        FIELD1 => 'PrincipalId',
+                        TABLE2 => 'CachedGroupMembers',
+                        FIELD2 => 'GroupId',
+                    );
+                    $ACL->Limit(
+                        ALIAS => $cgm_alias,
+                        FIELD => 'Disabled',
+                        VALUE => 0,
+                    );
+                }
+
                 $ACL->Limit(
                     ALIAS => $cgm_alias,
                     FIELD => 'MemberId',
                     VALUE => $principal->Id,
                 );
-                $ACL->Limit(
-                    ALIAS => $cgm_alias,
-                    FIELD => 'Disabled',
-                    VALUE => 0,
-                );
             }
         }
     }

commit 1418689efa49906ed713afb6dffafa2d4d35d195
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 16:59:59 2017 +0000

    Avoid caching

diff --git a/html/Helpers/RightsDebugger/Search b/html/Helpers/RightsDebugger/Search
index 9ce83e6..912689f 100644
--- a/html/Helpers/RightsDebugger/Search
+++ b/html/Helpers/RightsDebugger/Search
@@ -1,6 +1,7 @@
 <%INIT>
 my $results = RT::Extension::RightsDebugger->Search(%ARGS);
 $r->content_type('application/json; charset=utf-8');
+RT::Interface::Web::CacheControlExpiresHeaders( Time => 'no-cache' );
 $m->out(JSON($results));
 $m->abort;
 </%INIT>

commit b947d9b018c7aab88cccd55048911b82fd4a1312
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 17:08:10 2017 +0000

    Order by ACL ID
    
    Now that we're joining we need to be extra careful to maintain a consistent
    ordering so that chunked loading doesn't get lost in the woods

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index cfc4f8e..8c7809c 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -183,6 +183,12 @@ sub Search {
         );
     }
 
+    $ACL->OrderBy(
+        ALIAS => 'main',
+        FIELD => 'id',
+        ORDER => 'ASC',
+    );
+
     $ACL->UnLimit unless $has_search;
 
     $ACL->RowsPerPage(100);

commit 9eb1e283c8f45c089bc3ae314dd57a895dfc1f67
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 17:30:08 2017 +0000

    Display primary record under parent record
    
    e.g. if you assign a right to Everyone then search for user:root, then
    we display this last line here:
    
    Everyone
    System Group
    Contains root

diff --git a/html/Admin/RightsDebugger/index.html b/html/Admin/RightsDebugger/index.html
index 7af2af6..8e8444d 100644
--- a/html/Admin/RightsDebugger/index.html
+++ b/html/Admin/RightsDebugger/index.html
@@ -29,6 +29,12 @@
         (disabled)
       {{/if}}
     </span>
+
+    {{#each primary_records}}
+      <span class="primary">
+        Contains {{this.label}}
+      </span>
+    {{/each}}
   </span>
 </script>
 
diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index 8c7809c..6f4e4a5 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -111,6 +111,10 @@ sub Search {
         principal => 1,
         object    => 1,
     );
+    my %primary_records = (
+        principal => [],
+        object    => [],
+    );
 
     if ($args{principal}) {
         my ($principal_alias, $cgm_alias);
@@ -131,6 +135,8 @@ sub Search {
                 $has_search = 1;
                 $use_regex_search_for{principal} = 0;
 
+                push @{ $primary_records{principal} }, $principal;
+
                 $principal_alias ||= $ACL->Join(
                     ALIAS1 => 'main',
                     FIELD1 => 'PrincipalId',
@@ -207,10 +213,11 @@ sub Search {
 
     ACE: while (my $ACE = $ACL->Next) {
         $continueAfter = $ACE->Id;
-        my $serialized = $self->SerializeACE($ACE);
+        my $serialized = $self->SerializeACE($ACE, \%primary_records);
 
-        # this is hacky, but doing the searching in SQL is absolutely a nonstarter
         KEY: for my $key (qw/principal object/) {
+	    # filtering on the serialized record is hacky, but doing the
+	    # searching in SQL is absolutely a nonstarter
             next KEY unless $use_regex_search_for{$key};
 
             if (my $matchers = $search{$key}) {
@@ -245,10 +252,11 @@ sub Search {
 sub SerializeACE {
     my $self = shift;
     my $ACE = shift;
+    my $primary_records = shift;
 
     return {
-        principal      => $self->SerializeRecord($ACE->PrincipalObj),
-        object         => $self->SerializeRecord($ACE->Object),
+        principal      => $self->SerializeRecord($ACE->PrincipalObj, $primary_records->{principal}),
+        object         => $self->SerializeRecord($ACE->Object, $primary_records->{object}),
         right          => $ACE->RightName,
         ace            => { id => $ACE->Id },
         disable_revoke => $self->DisableRevoke($ACE),
@@ -275,9 +283,39 @@ sub DisableRevoke {
     return 0;
 }
 
+sub IsRecordDescendent {
+    my $self   = shift;
+    my $parent = shift;
+    my $child  = shift;
+
+    if ($parent->isa('RT::Group')) {
+        if ($child->isa('RT::Group') || $child->isa('RT::User') || $child->isa('RT::Principal')) {
+            return $parent->HasMember($child->id);
+        }
+    }
+
+    return 0;
+}
+
+sub SerializePrimaryRecords {
+    my $self            = shift;
+    my $record          = shift;
+    my $primary_records = shift;
+
+    my %seen;
+
+    return [
+        map { $self->SerializeRecord($_) }
+        grep { $self->IsRecordDescendent($record, $_) }
+        grep { !$seen{ref($_) . '-' . $_->id}++ }
+        @$primary_records
+    ];
+}
+
 sub SerializeRecord {
     my $self = shift;
     my $record = shift;
+    my $primary_records = shift;
 
     if ($record->isa('RT::Principal')) {
         $record = $record->Object;
@@ -300,12 +338,13 @@ sub SerializeRecord {
     }
 
     return {
-        class       => ref($record),
-        id          => $record->id,
-        label       => $self->LabelForRecord($record),
-        detail      => $self->DetailForRecord($record),
-        url         => $self->URLForRecord($record),
-        disabled    => $self->DisabledForRecord($record) ? JSON::true : JSON::false,
+        class           => ref($record),
+        id              => $record->id,
+        label           => $self->LabelForRecord($record),
+        detail          => $self->DetailForRecord($record),
+        url             => $self->URLForRecord($record),
+        disabled        => $self->DisabledForRecord($record) ? JSON::true : JSON::false,
+        primary_records => $self->SerializePrimaryRecords($record, $primary_records),
     };
 }
 
diff --git a/static/css/rights-debugger.css b/static/css/rights-debugger.css
index 07c7fb7..0afae25 100644
--- a/static/css/rights-debugger.css
+++ b/static/css/rights-debugger.css
@@ -67,3 +67,8 @@
     text-decoration: line-through;
 }
 
+#rights-debugger .results .result .primary {
+    font-size: 80%;
+    font-style: italic;
+    display: block;
+}

commit 7a39182e93d16bd2374d20f43bbb6b649e61135d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 17:43:29 2017 +0000

    Avoid multi-principal/multi-object matching
    
    It's looking to be intractable to re-construct the relationships after the CGM
    SQL query for displaying "Contains $user" or "Contains $group"

diff --git a/html/Admin/RightsDebugger/index.html b/html/Admin/RightsDebugger/index.html
index 8e8444d..12f694e 100644
--- a/html/Admin/RightsDebugger/index.html
+++ b/html/Admin/RightsDebugger/index.html
@@ -30,11 +30,11 @@
       {{/if}}
     </span>
 
-    {{#each primary_records}}
+    {{#if primary_record}}
       <span class="primary">
-        Contains {{this.label}}
+        Contains {{primary_record.label}}
       </span>
-    {{/each}}
+    {{/if}}
   </span>
 </script>
 
diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index 6f4e4a5..36a0196 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -112,58 +112,55 @@ sub Search {
         object    => 1,
     );
     my %primary_records = (
-        principal => [],
-        object    => [],
+        principal => undef,
+        object    => undef,
     );
 
     if ($args{principal}) {
-        my ($principal_alias, $cgm_alias);
-
-        for my $word (split ' ', $args{principal}) {
-            if (my ($type, $identifier) = $word =~ m{
-                ^
-                    (u|user|g|group)
-                    [:#]
-                    (\S+)
-                $
-            }xi) {
-                my $principal = $self->_PrincipalForSpec($type, $identifier);
-                if (!$principal) {
-                    return { error => 'Unable to find row' };
-                }
+        if (my ($type, $identifier) = $args{principal} =~ m{
+            ^
+                \s*
+                (u|user|g|group)
+                \s*
+                [:#]
+                \s*
+                (.+?)
+                \s*
+            $
+        }xi) {
+            my $principal = $self->_PrincipalForSpec($type, $identifier);
+            if (!$principal) {
+                return { error => 'Unable to find row' };
+            }
 
-                $has_search = 1;
-                $use_regex_search_for{principal} = 0;
-
-                push @{ $primary_records{principal} }, $principal;
-
-                $principal_alias ||= $ACL->Join(
-                    ALIAS1 => 'main',
-                    FIELD1 => 'PrincipalId',
-                    TABLE2 => 'Principals',
-                    FIELD2 => 'id',
-                );
-
-                if (!$cgm_alias) {
-                    $cgm_alias = $ACL->Join(
-                        ALIAS1 => 'main',
-                        FIELD1 => 'PrincipalId',
-                        TABLE2 => 'CachedGroupMembers',
-                        FIELD2 => 'GroupId',
-                    );
-                    $ACL->Limit(
-                        ALIAS => $cgm_alias,
-                        FIELD => 'Disabled',
-                        VALUE => 0,
-                    );
-                }
+            $has_search = 1;
+            $use_regex_search_for{principal} = 0;
 
-                $ACL->Limit(
-                    ALIAS => $cgm_alias,
-                    FIELD => 'MemberId',
-                    VALUE => $principal->Id,
-                );
-            }
+            $primary_records{principal} = $principal;
+
+            my $principal_alias = $ACL->Join(
+                ALIAS1 => 'main',
+                FIELD1 => 'PrincipalId',
+                TABLE2 => 'Principals',
+                FIELD2 => 'id',
+            );
+
+            my $cgm_alias = $ACL->Join(
+                ALIAS1 => 'main',
+                FIELD1 => 'PrincipalId',
+                TABLE2 => 'CachedGroupMembers',
+                FIELD2 => 'GroupId',
+            );
+            $ACL->Limit(
+                ALIAS => $cgm_alias,
+                FIELD => 'Disabled',
+                VALUE => 0,
+            );
+            $ACL->Limit(
+                ALIAS => $cgm_alias,
+                FIELD => 'MemberId',
+                VALUE => $principal->Id,
+            );
         }
     }
 
@@ -202,7 +199,7 @@ sub Search {
     for my $key (qw/principal object right/) {
         if (my $search = $args{$key}) {
             my @matchers;
-            for my $word (split ' ', $search) {
+            for my $word ($key eq 'right' ? (split ' ', $search) : $search) {
                 push @matchers, qr/\Q$word\E/i;
             }
             $search{$key} = \@matchers;
@@ -283,39 +280,12 @@ sub DisableRevoke {
     return 0;
 }
 
-sub IsRecordDescendent {
-    my $self   = shift;
-    my $parent = shift;
-    my $child  = shift;
-
-    if ($parent->isa('RT::Group')) {
-        if ($child->isa('RT::Group') || $child->isa('RT::User') || $child->isa('RT::Principal')) {
-            return $parent->HasMember($child->id);
-        }
-    }
-
-    return 0;
-}
-
-sub SerializePrimaryRecords {
-    my $self            = shift;
-    my $record          = shift;
-    my $primary_records = shift;
-
-    my %seen;
-
-    return [
-        map { $self->SerializeRecord($_) }
-        grep { $self->IsRecordDescendent($record, $_) }
-        grep { !$seen{ref($_) . '-' . $_->id}++ }
-        @$primary_records
-    ];
-}
-
 sub SerializeRecord {
     my $self = shift;
     my $record = shift;
-    my $primary_records = shift;
+    my $primary_record = shift;
+
+    return undef unless $record;
 
     if ($record->isa('RT::Principal')) {
         $record = $record->Object;
@@ -337,15 +307,17 @@ sub SerializeRecord {
         }
     }
 
-    return {
+    my $serialized = {
         class           => ref($record),
         id              => $record->id,
         label           => $self->LabelForRecord($record),
         detail          => $self->DetailForRecord($record),
         url             => $self->URLForRecord($record),
         disabled        => $self->DisabledForRecord($record) ? JSON::true : JSON::false,
-        primary_records => $self->SerializePrimaryRecords($record, $primary_records),
+        primary_record  => $self->SerializeRecord($primary_record),
     };
+
+    return $serialized;
 }
 
 sub LabelForRecord {

commit 6fb27e1bd63ad72cbfaf7dc5fc065cc48d9e1545
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 17:47:54 2017 +0000

    No need to special case these role group names
    
    RoleClass handles them all just fine

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index 36a0196..50492b4 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -355,16 +355,7 @@ sub DetailForRecord {
 
     # like RT::Group->SelfDescription but without the redundant labels
     if ($record->isa('RT::Group')) {
-        if ($record->Domain eq 'RT::System-Role') {
-            return "System Role";
-        }
-        elsif ($record->Domain eq 'RT::Queue-Role') {
-            return "Queue Role";
-        }
-        elsif ($record->Domain eq 'RT::Ticket-Role') {
-            return "Ticket Role";
-        }
-        elsif ($record->RoleClass) {
+        if ($record->RoleClass) {
             my $class = $record->RoleClass;
             $class =~ s/^RT:://i;
             return "$class Role";

commit 78eacafa3fc8d203d089693eca2ba23cdb389819
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Mar 7 17:48:53 2017 +0000

    word -> term

diff --git a/lib/RT/Extension/RightsDebugger.pm b/lib/RT/Extension/RightsDebugger.pm
index 50492b4..12c8922 100644
--- a/lib/RT/Extension/RightsDebugger.pm
+++ b/lib/RT/Extension/RightsDebugger.pm
@@ -41,7 +41,7 @@ sub _HighlightSerializedForSearch {
     my $serialized = shift;
     my $search     = shift;
 
-    # highlight matching words
+    # highlight matching terms
     $serialized->{right_highlighted} = _HighlightTerm($serialized->{right}, join '|', @{ $search->{right} || [] });
 
     for my $key (qw/principal object/) {
@@ -166,11 +166,11 @@ sub Search {
 
     if ($args{right}) {
         $has_search = 1;
-        for my $word (split ' ', $args{right}) {
+        for my $term (split ' ', $args{right}) {
             $ACL->Limit(
                 FIELD           => 'RightName',
                 OPERATOR        => 'LIKE',
-                VALUE           => $word,
+                VALUE           => $term,
                 CASESENSITIVE   => 0,
                 ENTRYAGGREGATOR => 'OR',
             );
@@ -199,8 +199,8 @@ sub Search {
     for my $key (qw/principal object right/) {
         if (my $search = $args{$key}) {
             my @matchers;
-            for my $word ($key eq 'right' ? (split ' ', $search) : $search) {
-                push @matchers, qr/\Q$word\E/i;
+            for my $term ($key eq 'right' ? (split ' ', $search) : $search) {
+                push @matchers, qr/\Q$term\E/i;
             }
             $search{$key} = \@matchers;
         }

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


More information about the Bps-public-commit mailing list