Skip to content

Commit

Permalink
Add centralized reCAPTCHA v3 handling and improve OPAC form scripts
Browse files Browse the repository at this point in the history
- Centralized reCAPTCHA v3 logic into a new `recaptcha.js` module for streamlined verification across forms.
- Corrected setting name for enabling reCAPTCHA: `recaptcha.enable` to `recaptcha.enabled`.
- Refactored OPAC searchbar template to improve form submission handling and removed inline scripts.
- Enhanced `recaptcha.js` with detailed logging for better debugging.
- Updated documentation with comprehensive setup SQL and placeholder reCAPTCHA keys for security.

Release-Note: Centralize reCAPTCHA v3 handling, improve form scripts, and update documentation.

Testing Plan:
- Verify reCAPTCHA appears on registration and search forms when enabled.
- Confirm form submission is blocked on failed reCAPTCHA.
- Check correct reCAPTCHA response logging in the console.

Signed-off-by: Ian Skelskey <ianskelskey@gmail.com>
  • Loading branch information
IanSkelskey committed Feb 24, 2025
1 parent 3da0774 commit b059c7e
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 81 deletions.
68 changes: 4 additions & 64 deletions Open-ILS/src/templates-bootstrap/opac/parts/recaptcha.tt2
Original file line number Diff line number Diff line change
@@ -1,82 +1,22 @@
<!--------------------------------------------------------------------------------
Module: templates-bootstrap/opac/parts/recaptcha.tt2
Author: Ian Skelskey <ianskelskey@gmail.com>
Organization: Bibliomation, Inc.
Year: 2024
Description: Template Toolkit file for reCAPTCHA form validation in the OPAC.
-------------------------------------------------------------------------------->
[%
org_unit = ctx.search_ou;
recaptcha_site_key = ctx.get_org_setting(org_unit, 'recaptcha.site_key');
action_name = action_name || 'register'; # Default action name if none is provided
submit_action = submit_action || 'submit'; # Default submit action if none is provided
target_element_id = target_element_id || 'recaptcha-form'; # Default target element ID
recaptcha_enabled = ctx.get_org_setting(ctx.search_ou, 'recaptcha.enable');
recaptcha_enabled = ctx.get_org_setting(ctx.search_ou, 'recaptcha.enabled');
%]
[% IF recaptcha_enabled && recaptcha_enabled == 1 %]
<!-- Dependencies -->
<script src="https://www.google.com/recaptcha/api.js?render=[% recaptcha_site_key %]"></script>
<script src="/opac/common/js/opensrf.js"></script>
<script src="/opac/common/js/opensrf_xhr.js"></script>
<script src="/opac/common/js/JSON_v1.js"></script>
<script src="/opac/common/js/recaptcha.js"></script>

<script>
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('[% target_element_id %]');
if (!form) return;

// Create and append reCAPTCHA container dynamically
const recaptchaContainer = createRecaptchaContainer();
form.appendChild(recaptchaContainer);

// Add event listener to the form for reCAPTCHA validation
form.addEventListener('[% submit_action %]', event => {
event.preventDefault();
if (!form.checkValidity()) {
form.classList.add('was-validated');
return;
}
grecaptcha.ready(() => {
grecaptcha.execute('[% recaptcha_site_key %]', { action: '[% action_name %]' })
.then(handleRecaptchaToken);
});
});

function createRecaptchaContainer() {
const container = document.createElement('div');
container.id = 'recaptcha-container';
return container;
}

function handleRecaptchaToken(token) {
console.log('Sending reCAPTCHA verification request...');
const session = new OpenSRF.ClientSession('biblio.recaptcha');
const request = session.request('biblio.recaptcha.verify', {
token,
org_unit: '[% org_unit %]'
});

request.oncomplete = response => processRecaptchaResponse(response, form);
request.send();
}

function processRecaptchaResponse(response, form) {
let msg;
while ((msg = response.recv())) {
try {
const responseContent = JSON.parse(msg.content());
console.log('reCAPTCHA response:', responseContent);
if (responseContent.success === 1) {
form.submit();
} else {
alert('reCAPTCHA validation failed. Please try again.');
}
} catch (error) {
console.error('Error parsing reCAPTCHA response as JSON:', error);
alert('Error in reCAPTCHA validation. Please try again.');
}
}
}
initializeRecaptcha('[% target_element_id %]', '[% recaptcha_site_key %]', '[% action_name %]', '[% submit_action %]');
});
</script>
[% ELSE %]
Expand All @@ -85,4 +25,4 @@
console.log('recaptcha_enabled:', '[% recaptcha_enabled %]');
console.log('reCAPTCHA is not enabled for this organization unit.');
</script>
[% END %]
[% END %]
51 changes: 36 additions & 15 deletions Open-ILS/src/templates-bootstrap/opac/parts/searchbar.tt2
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ END;

<div id="search-wrapper" class="container-fluid">
[% UNLESS took_care_of_form -%]
<form action="[% ctx.opac_root %]/results" method="get">
<form id="opac-search-form" data-org-unit="[% ctx.search_ou %]">
[%- END %]
[% IF ctx.page == 'rresult' && ctx.metarecord && search.metarecord_default %]
<input type="hidden" name="modifier" value="metabib"/>
Expand Down Expand Up @@ -110,17 +110,22 @@ END;

<div class="col col-auto col-search-button">
<input id="detail" type="hidden" name="detail_record_view" value="[% show_detail_view %]"/>
<button id='search-submit-go' type="submit" class="btn btn-sm btn-opac"
onclick='setTimeout(function(){$("search-submit-spinner").className=""; $("search-submit-go").className="hidden";[% IF ctx.depth_sel_button AND NOT took_care_of_form %] $("search-submit-go-depth").className="hidden";[% END %]}, 2000)'><i class="fas fa-search" aria-hidden="true"></i> [% l('Search') %]</button>

<button id='search-submit-go' type="submit" class="btn btn-sm btn-opac">
<i class="fas fa-search" aria-hidden="true"></i> [% l('Search') %]
</button>
[%- IF ctx.depth_sel_button AND NOT took_care_of_form %]
<button id='search-submit-go-depth' type="submit" value="[% ctx.depth_sel_depth %]" name="depth" class="btn btn-sm btn-opac"
onclick='setTimeout(function(){$("search-submit-spinner").className=""; $("search-submit-go").className="hidden"; $("search-submit-go-depth").className="hidden";}, 2000)' title="[% ctx.depth_sel_tooltip | html %]"><i class="fas fa-globe" aria-hidden="true"></i> [% ctx.depth_sel_button_label | html %]</button>
<button id='search-submit-go-depth' type="submit" value="[% ctx.depth_sel_depth %]" name="depth" class="btn btn-sm btn-opac" title="[% ctx.depth_sel_tooltip | html %]"><i class="fas fa-globe" aria-hidden="true"></i> [% ctx.depth_sel_button_label | html %]</button>
[%- END %]
<img id='search-submit-spinner' src='[% ctx.media_prefix %]/opac/images/progressbar_green.gif[% ctx.cache_key %]' class='hidden' alt='[% l("Search In Progress") %]'/>
</div>
</div>

[% INCLUDE "opac/parts/recaptcha.tt2"
action_name="opac_search"
submit_action="submit"
target_element_id="opac-search-form"
%]

[% IF ctx.bookbag %]
<div id="search-only-bookbag-container" class="text-center">
<input type="checkbox" id="search-only-bookbag" name="bookbag"
Expand Down Expand Up @@ -209,15 +214,31 @@ END;
}
}
</script>
<!-- Canonicalized query:

[% ctx.canonicalized_query | html %]

-->
<!--
<div id="breadcrumb">
<a href="[% ctx.opac_root %]/home">[% l('Catalog Home') %]</a> &gt;
</div>
-->
<script>
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('opac-search-form');
if (!form) return;

form.addEventListener('submit', event => {
event.preventDefault();
setTimeout(() => {
document.getElementById('search-submit-spinner').className = '';
document.getElementById('search-submit-go').className = 'hidden';
// If depth button exists
const depthBtn = document.getElementById('search-submit-go-depth');
if (depthBtn) depthBtn.className = 'hidden';
}, 2000);

// Construct the GET request URL
const formData = new FormData(form);
const queryString = new URLSearchParams(formData).toString();
const actionUrl = '[% ctx.opac_root %]/results?' + queryString;

// Redirect to the constructed URL
window.location.href = actionUrl;
});
});
</script>
</div>
</div>
2 changes: 1 addition & 1 deletion Open-ILS/src/templates-bootstrap/opac/register.tt2
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ END;
) | html %]</h4>
[% END %]

<form method='POST' id="registration-form" class="needs-validation" novalidate>
<form method='POST' id="registration-form" class="needs-validation" novalidate data-org-unit="[% ctx.search_ou %]">
<div class="form-group row">
<label class="control-label col-md-2" for='stgu.home_ou'>[% l('Home Library') %]</label>
<div class="col-md-6">
Expand Down
71 changes: 71 additions & 0 deletions Open-ILS/web/opac/common/js/recaptcha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
document.addEventListener('DOMContentLoaded', () => {
function initializeRecaptcha(formId, siteKey, actionName, submitAction) {
console.log('Initializing reCAPTCHA for form:', formId);
const form = document.getElementById(formId);
if (!form) {
console.log('Form not found:', formId);
return;
}

// Create and append reCAPTCHA container dynamically
const recaptchaContainer = createRecaptchaContainer();
form.appendChild(recaptchaContainer);
console.log('reCAPTCHA container appended to form:', formId);

// Add event listener to the form for reCAPTCHA validation
form.addEventListener('submit', event => {
event.preventDefault();
console.log('Form submit action intercepted:', submitAction);
grecaptcha.ready(() => {
console.log('Executing reCAPTCHA with siteKey:', siteKey, 'and actionName:', actionName);
grecaptcha.execute(siteKey, { action: actionName })
.then(token => handleRecaptchaToken(token, form));
});
});

function createRecaptchaContainer() {
const container = document.createElement('div');
container.id = 'recaptcha-container';
console.log('Created reCAPTCHA container');
return container;
}

function handleRecaptchaToken(token, form) {
console.log('Received reCAPTCHA token:', token);
console.log('Sending reCAPTCHA verification request...');
const session = new OpenSRF.ClientSession('biblio.recaptcha');
const request = session.request('biblio.recaptcha.verify', {
token,
org_unit: form.dataset.orgUnit
});

request.oncomplete = response => processRecaptchaResponse(response, form);
request.send();
console.log('reCAPTCHA verification request sent');
}

function processRecaptchaResponse(response, form) {
console.log('Processing reCAPTCHA response...');
let msg;
while ((msg = response.recv())) {
try {
const responseContent = JSON.parse(msg.content());
console.log('reCAPTCHA response:', responseContent);
if (responseContent.success === 1) {
console.log('reCAPTCHA validation successful');
form.submit();
} else {
console.log('reCAPTCHA validation failed');
alert('reCAPTCHA validation failed. Please try again.');
}
} catch (error) {
console.error('Error parsing reCAPTCHA response as JSON:', error);
alert('Error in reCAPTCHA validation. Please try again.');
}
}
}
}

// Export the function to be used in other scripts
window.initializeRecaptcha = initializeRecaptcha;
});
40 changes: 39 additions & 1 deletion docs/modules/admin/pages/recaptcha.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ To enable reCAPTCHA v3, follow these steps:

1. **Obtain reCAPTCHA Keys**:
- Register your site with Google reCAPTCHA to obtain the site key and secret key.
- Visit the [Google reCAPTCHA Admin Console](https://www.google.com/recaptcha/admin) to register your site.
- Visit the link:https://www.google.com/recaptcha/admin[Google reCAPTCHA Admin Console^] to register your site.

2. **Configure reCAPTCHA in Evergreen**:
- Add the reCAPTCHA keys to your organizational settings in Evergreen.
Expand Down Expand Up @@ -54,6 +54,44 @@ Added the reCAPTCHA service to the list of services:
<service>biblio.recaptcha</service>
```

== Adding Library Settings ==

To enable reCAPTCHA v3, you need to add the following library settings to Evergreen:

* `recaptcha.site_key`: The site key obtained from Google reCAPTCHA.
* `recaptcha.secret_key`: The secret key obtained from Google reCAPTCHA.
* `recaptcha.enabled`: A flag to enable or disable reCAPTCHA (set to true to enable).

These settings should be applied at the consortium level and will automatically apply to all children. However, supplying different keys for different organizational units will override the consortial setting by default.

Here are the SQL insert statements to create these library settings:

```sql
-- Insert reCAPTCHA site key setting type
INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype) VALUES
('recaptcha.site_key', 'reCAPTCHA site key', 'sec', 'The site key obtained from Google reCAPTCHA.', 'string');

-- Insert reCAPTCHA secret key setting type
INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype) VALUES
('recaptcha.secret_key', 'reCAPTCHA secret key', 'sec', 'The secret key obtained from Google reCAPTCHA.', 'string');

-- Insert reCAPTCHA enabled setting type
INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype) VALUES
('recaptcha.enabled', 'Enable reCAPTCHA', 'sec', 'A flag to enable or disable reCAPTCHA (set to true to enable).', 'bool');

-- Insert reCAPTCHA site key setting
INSERT INTO actor.org_unit_setting (org_unit, name, value) VALUES
(1, 'recaptcha.site_key', '"<YOUR_SITE_KEY">');

-- Insert reCAPTCHA secret key setting
INSERT INTO actor.org_unit_setting (org_unit, name, value) VALUES
(1, 'recaptcha.secret_key', '"<YOUR_SECRET_KEY>"');

-- Insert reCAPTCHA enabled setting
INSERT INTO actor.org_unit_setting (org_unit, name, value) VALUES
(1, 'recaptcha.enabled', 'true');
```

== Template Modifications ==

The following template files were modified to include reCAPTCHA validation:
Expand Down

0 comments on commit b059c7e

Please sign in to comment.