diff --git a/assets/legacy/util.js b/assets/legacy/util.js index 5440bd32f..84445b3d8 100644 --- a/assets/legacy/util.js +++ b/assets/legacy/util.js @@ -13,7 +13,7 @@ export function createLicense() { export function ignoreLine(link) { const hash = link.data('hash'); - $.post(`/reviews/add_ignore?hash=${hash}&package=${link.data('packname')}`); + $.post(`/ignored-matches?hash=${hash}&package=${link.data('packname')}`); $(`.hash-${hash}`).removeClass('risk-9'); const cs = $(`.hash-${hash} .fa-fire`); if (link.hasClass('current-selector')) { diff --git a/assets/main.js b/assets/main.js index 668c869ca..2443cf9fd 100644 --- a/assets/main.js +++ b/assets/main.js @@ -15,6 +15,7 @@ import {createLicense, ignoreLine, snippetNonLicense} from './legacy/util.js'; import ClassifySnippets from './vue/ClassifySnippets.vue'; import EditSnippet from './vue/EditSnippet.vue'; import IgnoredFiles from './vue/IgnoredFiles.vue'; +import IgnoredMatches from './vue/IgnoredMatches.vue'; import KnownLicenses from './vue/KnownLicenses.vue'; import KnownProducts from './vue/KnownProducts.vue'; import OpenReviews from './vue/OpenReviews.vue'; @@ -56,6 +57,10 @@ window.cavil = { app.mount('#edit-snippet'); }, + setupIgnoredMatches() { + createApp(IgnoredMatches).mount('#ignored-matches'); + }, + setupIgnoredFiles() { createApp(IgnoredFiles).mount('#ignored-files'); }, diff --git a/assets/vue/IgnoredMatches.vue b/assets/vue/IgnoredMatches.vue new file mode 100644 index 000000000..bf89d6826 --- /dev/null +++ b/assets/vue/IgnoredMatches.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/assets/vue/ProposedPatterns.vue b/assets/vue/ProposedPatterns.vue index 9dc1f5856..944ae24c7 100644 --- a/assets/vue/ProposedPatterns.vue +++ b/assets/vue/ProposedPatterns.vue @@ -192,7 +192,7 @@ export default { change.editUrl = `/snippet/edit/${change.data.snippet}`; change.removeUrl = `/licenses/proposed/remove/${change.token_hexsum}`; change.createUrl = `/snippet/decision/${change.data.snippet}`; - change.ignoreUrl = '/reviews/add_ignore'; + change.ignoreUrl = '/ignored-matches'; if (change.package !== null) change.package.pkgUrl = `/reviews/details/${change.package.id}`; if (change.closest !== null) change.closest.licenseUrl = `/licenses/edit_pattern/${change.closest.id}`; diff --git a/lib/Cavil.pm b/lib/Cavil.pm index e0c8f9403..dc67f7507 100644 --- a/lib/Cavil.pm +++ b/lib/Cavil.pm @@ -214,7 +214,6 @@ sub startup ($self) { ->to('Report#source', format => 'html'); $admin->post('/reviews/review_package/')->to('Reviewer#review_package')->name('review_package'); $manager->post('/reviews/fasttrack_package/')->to('Reviewer#fasttrack_package')->name('fasttrack_package'); - $admin->post('/reviews/add_ignore')->to('Reviewer#add_ignore'); $admin->post('/reviews/reindex/')->to('Reviewer#reindex_package')->name('reindex_package'); $public->get('/pagination/reviews/open')->to('Pagination#open_reviews')->name('pagination_open_reviews'); $public->get('/pagination/reviews/recent')->to('Pagination#recent_reviews')->name('pagination_recent_reviews'); @@ -229,9 +228,14 @@ sub startup ($self) { $logged_in->get('/licenses/recent')->to('License#recent')->name('recent_patterns'); $logged_in->get('/licenses/recent/meta')->to('License#recent_meta')->name('recent_patterns_meta'); + $admin->get('/ignored-matches')->to('Ignore#list_matches')->name('list_ignored_matches'); + $admin->post('/ignored-matches')->to('Ignore#add_match'); + $admin->delete('/ignored-matches/')->to('Ignore#remove_match')->name('remove_ignored_match'); + $admin->get('/pagination/matches/ignored')->to('Pagination#ignored_matches')->name('pagination_ignored_matches'); + $admin->get('/ignored-files')->to('Ignore#list_globs')->name('list_globs'); $admin->post('/ignored-files')->to('Ignore#add_glob')->name('add_ignore'); - $admin->delete('/ignored-files/')->to('Ignore#remove_glob')->name('remove_ignore'); + $admin->delete('/ignored-files/')->to('Ignore#remove_glob')->name('remove_ignored_file'); $admin->get('/pagination/files/ignored')->to('Pagination#ignored_files')->name('pagination_ignored_files'); # Public because of fine grained access controls (owner of proposal may remove it again) diff --git a/lib/Cavil/Controller/Ignore.pm b/lib/Cavil/Controller/Ignore.pm index fe2d1d0ca..5d8290f0b 100644 --- a/lib/Cavil/Controller/Ignore.pm +++ b/lib/Cavil/Controller/Ignore.pm @@ -28,14 +28,52 @@ sub add_glob ($self) { return $self->render(json => 'ok'); } +sub add_match ($self) { + my $validation = $self->validation; + $validation->required('hash')->like(qr/^[a-f0-9]{32}$/i); + $validation->required('package'); + $validation->optional('delay')->num; + $validation->optional('contributor'); + return $self->reply->json_validation_error if $validation->has_error; + + my $owner_id = $self->users->id_for_login($self->current_user); + my $contributor = $validation->param('contributor'); + my $contributor_id = $contributor ? $self->users->id_for_login($contributor) : undef; + my $delay = $validation->param('delay') // 0; + + my $hash = $validation->param('hash'); + $self->packages->ignore_line( + { + package => $validation->param('package'), + hash => $hash, + owner => $owner_id, + contributor => $contributor_id, + delay => $delay + } + ); + $self->patterns->remove_proposal($hash); + + return $self->render(json => 'ok'); +} + sub list_globs ($self) { $self->render('ignore/list_globs'); } +sub list_matches ($self) { + $self->render('ignore/list_matches'); +} + sub remove_glob ($self) { return $self->render(status => 400, json => {error => 'Glob does not exist'}) unless $self->ignored_files->remove($self->param('id'), $self->current_user); return $self->render(json => 'ok'); } +sub remove_match ($self) { + return $self->render(status => 400, json => {error => 'Ignored match does not exist'}) + unless $self->packages->remove_ignored_line($self->param('id'), $self->current_user); + return $self->render(json => 'ok'); +} + 1; diff --git a/lib/Cavil/Controller/Pagination.pm b/lib/Cavil/Controller/Pagination.pm index 1933282d0..123736174 100644 --- a/lib/Cavil/Controller/Pagination.pm +++ b/lib/Cavil/Controller/Pagination.pm @@ -16,6 +16,21 @@ package Cavil::Controller::Pagination; use Mojo::Base 'Mojolicious::Controller', -signatures; +sub ignored_matches ($self) { + my $v = $self->validation; + $v->optional('limit')->num; + $v->optional('offset')->num; + $v->optional('filter'); + return $self->reply->json_validation_error if $v->has_error; + my $limit = $v->param('limit') // 10; + my $offset = $v->param('offset') // 0; + my $search = $v->param('filter') // ''; + + my $page + = $self->helpers->patterns->paginate_ignored_matches({limit => $limit, offset => $offset, search => $search}); + $self->render(json => $page); +} + sub ignored_files ($self) { my $v = $self->validation; $v->optional('limit')->num; diff --git a/lib/Cavil/Controller/Reviewer.pm b/lib/Cavil/Controller/Reviewer.pm index ad4e87855..b83e7f95c 100644 --- a/lib/Cavil/Controller/Reviewer.pm +++ b/lib/Cavil/Controller/Reviewer.pm @@ -28,34 +28,6 @@ my $SMALL_REPORT_RE = qr/ )$ /xi; -sub add_ignore ($self) { - my $validation = $self->validation; - $validation->required('hash')->like(qr/^[a-f0-9]{32}$/i); - $validation->required('package'); - $validation->optional('delay')->num; - $validation->optional('contributor'); - return $self->reply->json_validation_error if $validation->has_error; - - my $owner_id = $self->users->id_for_login($self->current_user); - my $contributor = $validation->param('contributor'); - my $contributor_id = $contributor ? $self->users->id_for_login($contributor) : undef; - my $delay = $validation->param('delay') // 0; - - my $hash = $validation->param('hash'); - $self->packages->ignore_line( - { - package => $validation->param('package'), - hash => $hash, - owner => $owner_id, - contributor => $contributor_id, - delay => $delay - } - ); - $self->patterns->remove_proposal($hash); - - return $self->render(json => 'ok'); -} - sub details ($self) { my $id = $self->stash('id'); my $pkgs = $self->packages; diff --git a/lib/Cavil/Model/Packages.pm b/lib/Cavil/Model/Packages.pm index 832893a49..baefe6736 100644 --- a/lib/Cavil/Model/Packages.pm +++ b/lib/Cavil/Model/Packages.pm @@ -162,10 +162,17 @@ sub ignore_line ($self, $options) { on conflict do nothing', $options->{hash}, $options->{package}, $options->{owner}, $options->{contributor} ); - # as it affects all packages with the name, we need to update all reports - my $delay = $options->{delay} || 0; - my $ids = $db->select('bot_packages', 'id', {name => $options->{package}})->arrays->flatten->to_array; - $self->reindex($_, 3, [], $delay) for @$ids; + $self->reindex_packages($options->{package}, {delay => $options->{delay}}); +} + +sub remove_ignored_line ($self, $id, $user) { + return undef + unless my $hash = $self->pg->db->delete('ignored_lines', {id => $id}, {returning => ['hash', 'packname']})->hash; + $self->log->info(qq{User "$user" removed ignored match "$hash->{hash}"}); + + $self->reindex_packages($hash->{packname}); + + return 1; } sub imported ($self, $id) { @@ -430,6 +437,12 @@ sub reindex_matched_packages ($self, $pid, $priority = 0) { } } +sub reindex_packages ($self, $name, $options = {}) { + my $delay = $options->{delay} || 0; + my $ids = $self->pg->db->select('bot_packages', 'id', {name => $name})->arrays->flatten->to_array; + $self->reindex($_, 3, [], $delay) for @$ids; +} + sub remove_spdx_report ($self, $id) { my $path = $self->spdx_report_path($id); $path->remove; diff --git a/lib/Cavil/Model/Patterns.pm b/lib/Cavil/Model/Patterns.pm index 23ff07387..42d1e7981 100644 --- a/lib/Cavil/Model/Patterns.pm +++ b/lib/Cavil/Model/Patterns.pm @@ -196,6 +196,34 @@ sub pattern_exists ($self, $checksum) { return $hash ? $hash->{id} : undef; } +sub paginate_ignored_matches ($self, $options) { + my $db = $self->pg->db; + + my $search = ''; + if (length($options->{search}) > 0) { + my $quoted = $db->dbh->quote("\%$options->{search}\%"); + $search = "WHERE packname ILIKE $quoted"; + } + + my $results = $db->query( + qq{ + SELECT il.id, il.hash, il.packname, EXTRACT(EPOCH FROM il.created) AS created_epoch, bu1.login AS owner_login, + bu2.login AS contributor_login, COUNT(*) OVER() AS total + FROM ignored_lines il LEFT JOIN bot_users bu1 ON (bu1.id = il.owner) + LEFT JOIN bot_users bu2 ON (bu2.id = il.contributor) + $search + ORDER BY il.created DESC + LIMIT ? OFFSET ? + }, $options->{limit}, $options->{offset} + )->hashes->to_array; + + for my $result (@$results) { + $result->{snippet} = $db->query('SELECT id FROM snippets WHERE hash = ?', $result->{hash})->hash; + } + + return paginate($results, $options); +} + sub paginate_known_licenses ($self, $options) { my $db = $self->pg->db; diff --git a/t/login.t b/t/login.t index c46d5ec33..1fec75981 100644 --- a/t/login.t +++ b/t/login.t @@ -65,7 +65,8 @@ subtest 'Login required' => sub { subtest 'Not authenticated' => sub { $t->post_ok('/reviews/review_package/1')->status_is(403)->content_like(qr/Permission/); $t->post_ok('/reviews/fasttrack_package/1')->status_is(403)->content_like(qr/Permission/); - $t->post_ok('/reviews/add_ignore')->status_is(403)->content_like(qr/Permission/); + $t->post_ok('/ignored-matches')->status_is(403)->content_like(qr/Permission/); + $t->post_ok('/ignored-files')->status_is(403)->content_like(qr/Permission/); $t->post_ok('/reviews/reindex/1')->status_is(403)->content_like(qr/Permission/); $t->get_ok('/reviews/file_view/1/LICENSE')->status_is(403)->content_like(qr/Permission/); $t->get_ok('/licenses/new_pattern')->status_is(403)->content_like(qr/Permission/); diff --git a/t/proposal.t b/t/proposal.t index 62557728c..3d43905bd 100644 --- a/t/proposal.t +++ b/t/proposal.t @@ -157,13 +157,13 @@ subtest 'Pattern creation' => sub { ->content_like(qr/Conflicting ignore pattern proposal already exists/); my $ignore_form = {hash => '39e8204ddebdc31a4d0e77aa647f4241', package => 'perl-Mojolicious', contributor => 'tester'}; - $t->post_ok('/reviews/add_ignore' => form => $ignore_form)->status_is(403); + $t->post_ok('/ignored-matches' => form => $ignore_form)->status_is(403); $t->get_ok('/licenses/proposed/meta')->status_is(200)->json_has('/changes/0') ->json_is('/changes/0/action' => 'create_ignore')->json_is('/changes/0/data/pattern' => 'This is a license') ->json_is('/changes/0/data/highlighted' => [0])->json_is('/changes/0/data/edited' => 0)->json_hasnt('/changes/1'); $t->app->users->add_role(2, 'admin'); - $t->post_ok('/reviews/add_ignore' => form => $ignore_form)->status_is(200)->content_like(qr/ok/); + $t->post_ok('/ignored-matches' => form => $ignore_form)->status_is(200)->content_like(qr/ok/); $t->get_ok('/licenses/proposed/meta')->status_is(200)->json_hasnt('/changes/0'); $t->post_ok('/snippet/decision/1' => form => $form)->status_is(409) @@ -238,4 +238,31 @@ subtest 'Cancelled proposal' => sub { }; }; +subtest 'Remove ignored match' => sub { + $t->app->minion->perform_jobs; + $t->app->users->add_role(2, 'admin'); + + is $t->app->minion->jobs({tasks => ['index'], states => ['inactive']})->total, 0, 'no jobs'; + $t->post_ok( + '/ignored-matches' => form => {hash => 'abe8204ddebdc31a4d0e77aa647f42cd', package => 'package-with-snippets'}) + ->status_is(200)->content_like(qr/ok/); + is $t->app->minion->jobs({tasks => ['index'], states => ['inactive']})->total, 1, 'job created'; + $t->get_ok('/pagination/matches/ignored')->status_is(200)->json_has('/page/0')->json_is('/start', 1) + ->json_is('/end', 2)->json_is('/total', 2)->json_is('/page/0/hash', 'abe8204ddebdc31a4d0e77aa647f42cd') + ->json_is('/page/0/packname', 'package-with-snippets'); + my $id = $t->tx->res->json->{page}[0]{id}; + + $t->get_ok('/pagination/matches/ignored?filter=with-snippets')->status_is(200)->json_has('/page/0') + ->json_is('/start', 1)->json_is('/end', 1)->json_is('/total', 1) + ->json_is('/page/0/hash', 'abe8204ddebdc31a4d0e77aa647f42cd'); + $t->get_ok('/pagination/matches/ignored?filter=does_not_exist')->status_is(200)->json_is('/start', 1) + ->json_is('/end', 0)->json_is('/total', 0); + + $t->app->minion->perform_jobs; + is $t->app->minion->jobs({tasks => ['index'], states => ['inactive']})->total, 0, 'no jobs'; + $t->delete_ok("/ignored-matches/$id")->status_is(200)->json_is('ok'); + $t->delete_ok("/ignored-matches/$id")->status_is(400)->json_is({error => 'Ignored match does not exist'}); + is $t->app->minion->jobs({tasks => ['index'], states => ['inactive']})->total, 1, 'job created'; +}; + done_testing(); diff --git a/templates/ignore/list_matches.html.ep b/templates/ignore/list_matches.html.ep new file mode 100644 index 000000000..9e716f8af --- /dev/null +++ b/templates/ignore/list_matches.html.ep @@ -0,0 +1,8 @@ +% layout 'default'; +% title 'List ignored matches'; + +
+ +% content_for 'ready_function' => begin + cavil.setupIgnoredMatches(); +% end diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep index 3647f22ca..a1230dc6c 100644 --- a/templates/layouts/default.html.ep +++ b/templates/layouts/default.html.ep @@ -95,6 +95,9 @@
  • %= link_to 'Upload Tarball' => '/upload', class => 'dropdown-item'
  • +
  • + %= link_to 'Ignored Matches' => '/ignored-matches', class => 'dropdown-item' +
  • %= link_to 'Ignored Files' => '/ignored-files', class => 'dropdown-item'