From 2997595eaf5c57c974b6a1e4850fe42672651775 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Fri, 17 Oct 2025 12:57:42 -0400 Subject: [PATCH] Bug 41048: Add ability to disallow empty patron searches In high traffic environments with large numbers of patrons, empty patron searches can take a very long time and cause over-use of resources. It would be good to have the option to require search criteria for patron searches. Test Plan: 1) Apply this patch 2) Restart all the things! 3) Enable DisallowEmptyPatronSearches 4) Browse to any patron search form ( such as members-home.pl, or the one on acqui/basket.pl ) 5) Note the search button is disabled when the search is empty, and enabled which the search box contains text --- Koha/REST/V1/Patrons.pm | 28 +++++++++++++ api/v1/swagger/paths/patrons.yaml | 4 ++ .../data/mysql/atomicupdate/bug_41048.pl | 20 +++++++++ installer/data/mysql/mandatory/sysprefs.sql | 1 + .../prog/en/includes/patron-search.inc | 41 ++++++++++++++++++- .../en/modules/admin/preferences/patrons.pref | 6 +++ 6 files changed, 99 insertions(+), 1 deletion(-) create mode 100755 installer/data/mysql/atomicupdate/bug_41048.pl diff --git a/Koha/REST/V1/Patrons.pm b/Koha/REST/V1/Patrons.pm index 7d733c10dc1..cb1d413cb00 100644 --- a/Koha/REST/V1/Patrons.pm +++ b/Koha/REST/V1/Patrons.pm @@ -47,6 +47,34 @@ sub list { return try { + # Check if empty searches are disallowed + if ( C4::Context->preference('DisallowEmptyPatronSearches') ) { + my $has_search_criteria = 0; + my $params = $c->req->query_params->to_hash; + + # Remove special parameters that aren't search criteria + delete $params->{_per_page}; + delete $params->{_page}; + delete $params->{_order_by}; + delete $params->{_match}; + + # Check if any search parameters are provided + $has_search_criteria = 1 if scalar( keys %$params ) > 0; + + # Check if we're not in restricted mode (which would add a criterion) + $has_search_criteria = 1 if $c->param('restricted'); + + if ( !$has_search_criteria ) { + return $c->render( + status => 422, + openapi => { + error => 'Search criteria are required', + code => 'empty_search_not_allowed' + } + ); + } + } + my $query = {}; my $restricted = $c->param('restricted'); $c->req->params->remove('restricted'); diff --git a/api/v1/swagger/paths/patrons.yaml b/api/v1/swagger/paths/patrons.yaml index a7df190ba3e..dc1ca322eaf 100644 --- a/api/v1/swagger/paths/patrons.yaml +++ b/api/v1/swagger/paths/patrons.yaml @@ -392,6 +392,10 @@ description: Access forbidden schema: $ref: "../swagger.yaml#/definitions/error" + "422": + description: Unprocessable Content + schema: + $ref: "../swagger.yaml#/definitions/error" "500": description: | Internal server error. Possible `error_code` attribute values: diff --git a/installer/data/mysql/atomicupdate/bug_41048.pl b/installer/data/mysql/atomicupdate/bug_41048.pl new file mode 100755 index 00000000000..24766a34aa0 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_41048.pl @@ -0,0 +1,20 @@ +use Modern::Perl; +use Koha::Installer::Output qw(say_warning say_success say_info); + +return { + bug_number => "41048", + description => ":dd ability to disallow empty patron searches", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + $dbh->do( + q{ +INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES +('DisallowEmptyPatronSearches','0',NULL,'If enabled, the Patron REST API will return a 422 status code when a search is performed without any criteria.','YesNo') + } + ); + + say $out "Added new system preference 'DisallowEmptyPatronSearches'"; + }, +}; diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql index c3fa93d5041..9f646788c7f 100644 --- a/installer/data/mysql/mandatory/sysprefs.sql +++ b/installer/data/mysql/mandatory/sysprefs.sql @@ -214,6 +214,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('DefaultLongOverdueSkipPatronCategories', '', NULL, "Set the patron categories that will not be listed when longoverdue cronjob is executed", 'choice'), ('DefaultPatronSearchFields', 'firstname|preferred_name|middle_name|surname|othernames|cardnumber|userid',NULL,'Pipe separated list defining the default fields to be used during a patron search using the "standard" option. If empty Koha will default to "firstname|surname|othernames|cardnumber|userid". Additional fields added to this preference will be added as search options in the dropdown menu on the patron search page.','free'), ('DefaultPatronSearchMethod','starts_with','Choose which search method to use by default when searching with PatronAutoComplete','starts_with|contains','Choice'), +('DisallowEmptyPatronSearches','0',NULL,'If enabled, the Patron REST API will return a 422 status code when a search is performed without any criteria.','YesNo'), ('DefaultSaveRecordFileID','biblionumber','biblionumber|controlnumber','Defines whether the advanced cataloging editor will use the bibliographic record number or control number field to populate the name of the save file','Choice'), ('defaultSortField','relevance','relevance|popularity|call_number|pubdate|acqdate|title|author','Specify the default field used for sorting','Choice'), ('defaultSortOrder','dsc','asc|dsc|az|za','Specify the default sort order','Choice'), diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search.inc index 8ca22ec1999..68d6ff70724 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-search.inc @@ -343,10 +343,28 @@ [% END %] $(document).ready(function(){ - let parent_block = $("#[% search_results_block_id | html %]"); let patron_search_form = get_patron_search_form(); + let patron_search_filter = patron_search_form.find(".search_patron_filter"); // jQuery object + let search_patron_filter_btn = patron_search_form.find(".search_patron_filter_btn"); // assuming you already have this + + [% IF Koha.Preference('DisallowEmptyPatronSearches') %] + // Check for passed search value on load + if (!patron_search_filter.val()) { + search_patron_filter_btn.prop('disabled', true); + } + + // Check for empty search value on input + patron_search_filter.on("input", function() { + if (!patron_search_filter.val()) { + search_patron_filter_btn.prop('disabled', true); + } else { + search_patron_filter_btn.prop('disabled', false); + } + }); + [% END %] + parent_block.find(".info").hide(); parent_block.find(".error").hide(); @@ -493,6 +511,27 @@ first_draw = 0; [% END %] return json.data; + }, + error: function(xhr, error, thrown) { + if (xhr.status === 422) { + // Directly manipulate tbody instead of clear().draw(), that causes an infinite loop + var tbody = table_node.find('tbody'); + tbody.empty(); + + // Hide the "Processing..." model + $(".dt-processing").hide(); + + // Display a message to the librarian + var colCount = table_node.find('thead th').length; + tbody.append( + '' + + _("Empty patron search, please add a search term") + + '' + ); + + // Re-enable the patron search button + patron_search_form.find(".search_patron_filter_btn").prop({disabled: false}); + } } }, [% IF open_on_row_click OR preview_on_name_click OR remember_selections %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref index eae4c9723f6..b2a0248794d 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref @@ -468,6 +468,12 @@ Patrons: - pref: minPasswordLength class: integer - characters long. + - + - pref: DisallowEmptyPatronSearches + choices: + 1: "Require" + 0: "Don't require" + - "at least one search criterion when searching for patrons." - - pref: RequireStrongPassword choices: -- 2.39.5 (Apple Git-154)