-
-
Notifications
You must be signed in to change notification settings - Fork 13
Add Cap proof‑of‑work CAPTCHA guard (privacy‑friendly bot protection) #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| <?php | ||
|
|
||
| namespace tobimori\DreamForm\Guards; | ||
|
|
||
| use Kirby\Http\Remote; | ||
| use tobimori\DreamForm\DreamForm; | ||
| use tobimori\DreamForm\Models\SubmissionPage; | ||
|
|
||
| class CapGuard extends Guard | ||
| { | ||
| public const TYPE = 'cap'; | ||
|
|
||
| public static function endpoint(): string|null | ||
| { | ||
| return DreamForm::option('guards.cap.endpoint'); | ||
| } | ||
|
|
||
| protected static function secretKey(): string|null | ||
| { | ||
| return DreamForm::option('guards.cap.secretKey'); | ||
| } | ||
|
|
||
| public function run(): void | ||
| { | ||
| $data = [ | ||
| 'secret' => static::secretKey(), | ||
| 'response' => SubmissionPage::valueFromBody('cap-token') | ||
| ]; | ||
tobimori marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| $remote = Remote::post(DreamForm::option('guards.cap.endpoint') . 'siteverify', [ | ||
| 'data' => $data | ||
| ]); | ||
|
Comment on lines
+30
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Build verification URL robustly and add timeout Be resilient to missing/trailing slashes and set a reasonable timeout to avoid hanging requests. - $remote = Remote::post(DreamForm::option('guards.cap.endpoint') . 'siteverify', [
- 'data' => $data
- ]);
+ $endpoint = static::endpoint();
+ $verifyUrl = rtrim((string)$endpoint, '/') . '/siteverify';
+ $remote = Remote::post($verifyUrl, [
+ 'data' => $data,
+ 'timeout' => 5,
+ ]);
🧰 Tools🪛 PHPMD (2.15.0)30-32: Avoid using static access to class '\Kirby\Http\Remote' in method 'run'. (Clean Code Rules) (StaticAccess) 30-30: Avoid using static access to class '\tobimori\DreamForm\DreamForm' in method 'run'. (Clean Code Rules) (StaticAccess) 🤖 Prompt for AI Agents |
||
|
|
||
| $result = $remote->json(); | ||
|
|
||
| if ( | ||
| $result['success'] !== true | ||
| ) { | ||
| $this->cancel(t('dreamform.submission.error.captcha')); | ||
| } | ||
| } | ||
|
Comment on lines
+34
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against network/JSON failures and undefined indexes Remote::post/json() can fail or return non-array; accessing $result['success'] directly risks warnings. Handle HTTP errors and parse safely. - $result = $remote->json();
-
- if (
- $result['success'] !== true
- ) {
- $this->cancel(t('dreamform.submission.error.captcha'));
- }
+ $result = $remote->json();
+
+ $ok = method_exists($remote, 'code') ? ($remote->code() >= 200 && $remote->code() < 400) : true;
+ $success = is_array($result) ? ($result['success'] ?? false) === true : false;
+
+ if (!$ok || !$success) {
+ $this->cancel(t('dreamform.submission.error.captcha'));
+ return;
+ }🤖 Prompt for AI Agents |
||
|
|
||
| public static function hasSnippet(): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| public static function isAvailable(): bool | ||
| { | ||
| return static::endpoint() !== null | ||
| && static::secretKey() !== null; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,52 @@ | ||||||||||||||||||
| title: Cap | ||||||||||||||||||
| description: Cap proof-of-work captcha integration | ||||||||||||||||||
| --- | ||||||||||||||||||
|
|
||||||||||||||||||
| DreamForm has built-in support for [Cap](https://capjs.js.org/), a lightweight, privacy-friendly CAPTCHA that uses SHA-256 proof-of-work instead of tracking or fingerprinting. It’s fast, self-hostable, and easy to integrate. | ||||||||||||||||||
|
|
||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Fix malformed front matter and add H1 to satisfy markdownlint MD041 Front matter isn’t delimited correctly. Also, markdownlint requires a top-level heading. Use proper front matter and add an H1. -title: Cap
-description: Cap proof-of-work captcha integration
----
+---
+title: Cap
+description: Cap proof-of-work captcha integration
+---
+
+Cap
+===
🧰 Tools🪛 markdownlint-cli2 (0.17.2)1-1: First line in a file should be a top-level heading (MD041, first-line-heading, first-line-h1) 🤖 Prompt for AI Agents |
||||||||||||||||||
| ## Adding Cap | ||||||||||||||||||
|
|
||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use setext-style headings (MD003) Docs lint expects setext headings. Convert the ATX headings accordingly. -## Adding Cap
+Adding Cap
+----------
-## How it works
+How it works
+------------
-## Options
+Options
+-------Also applies to: 35-36, 45-46 🧰 Tools🪛 markdownlint-cli2 (0.17.2)7-7: Heading style (MD003, heading-style) 🤖 Prompt for AI Agents |
||||||||||||||||||
| Follow the official Cap docs to deploy a Cap server (Node/Bun/Deno or standalone Docker) and create your keys. Then add the guard and required options to your `config.php`. | ||||||||||||||||||
|
|
||||||||||||||||||
| ```php | ||||||||||||||||||
| // site/config/config.php | ||||||||||||||||||
|
|
||||||||||||||||||
| return [ | ||||||||||||||||||
| 'tobimori.dreamform' => [ | ||||||||||||||||||
| 'guards' => [ | ||||||||||||||||||
| 'available' => ['cap', /* other guards here */], | ||||||||||||||||||
| 'cap' => [ | ||||||||||||||||||
| // Base API endpoint of your Cap server (https://<instance_url>/<site_key>/) | ||||||||||||||||||
| 'endpoint' => fn () => env('CAP_ENDPOINT'), | ||||||||||||||||||
| // Server secret used to verify the token server-to-server | ||||||||||||||||||
| 'secretKey' => fn () => env('CAP_SECRET_KEY'), | ||||||||||||||||||
| // Automatically inject the widget script | ||||||||||||||||||
| 'injectScript' => true, | ||||||||||||||||||
| // Load widget from your Cap server's asset endpoint instead of the CDN | ||||||||||||||||||
| 'useAssetServer' => false, | ||||||||||||||||||
| ], | ||||||||||||||||||
| ], | ||||||||||||||||||
| ], | ||||||||||||||||||
| ]; | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| Ideally, you should not commit these keys to your repository, but instead load them from environment variables, e.g. using the [kirby-dotenv plugin by Bruno Meilick](https://github.com/bnomei/kirby3-dotenv), as shown in the example above. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## How it works | ||||||||||||||||||
|
|
||||||||||||||||||
| - When a form includes the `cap` guard, DreamForm renders a `<cap-widget>` element. | ||||||||||||||||||
| - The widget obtains a challenge from your Cap server and, after solving the PoW, writes a token into the form as `cap-token`. | ||||||||||||||||||
| - On submit, DreamForm verifies the token against your Cap server using the configured `endpoint` and `secretKey`. | ||||||||||||||||||
|
|
||||||||||||||||||
| Cap supports customization via CSS variables, among other features. See the official docs for details: [Cap documentation](https://capjs.js.org/guide/). | ||||||||||||||||||
|
|
||||||||||||||||||
| Note: If you use HTMX, the integration automatically resets the widget between swaps for a clean state. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Options | ||||||||||||||||||
|
|
||||||||||||||||||
| | Option | Default | Accepts | Description | | ||||||||||||||||||
| | --- | --- | --- | --- | | ||||||||||||||||||
| | tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). | | ||||||||||||||||||
| | tobimori.dreamform.guards.cap.secretKey | `null` | `string|callable` | Secret key used by DreamForm to verify tokens with your Cap server. | | ||||||||||||||||||
| | tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. | | ||||||||||||||||||
| | tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https:///<instance_url>/widget.js` based on the configured endpoint. | | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Fix table parsing issues (escape pipes and correct URL) Unescaped pipes in inline code cause an extra column (MD056). Also, there’s an extra slash in the asset server URL. -| tobimori.dreamform.guards.cap.endpoint | `null` | `string|callable` | Base API endpoint of your Cap server ( `https://<instance_url>/<site_key>/`). |
-| tobimori.dreamform.guards.cap.secretKey | `null` | `string|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
+| tobimori.dreamform.guards.cap.endpoint | `null` | `string\|callable` | Base API endpoint of your Cap server (`https://<instance_url>/<site_key>/`). |
+| tobimori.dreamform.guards.cap.secretKey | `null` | `string\|callable` | Secret key used by DreamForm to verify tokens with your Cap server. |
| tobimori.dreamform.guards.cap.injectScript | `true` | `boolean` | Inject the client-side widget script automatically. Falls back to CDN `https://cdn.jsdelivr.net/npm/@cap.js/widget`. |
-| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https:///<instance_url>/widget.js` based on the configured endpoint. |
+| tobimori.dreamform.guards.cap.useAssetServer | `false` | `boolean` | If `true`, load the widget from your Cap server at `https://<instance_url>/widget.js` based on the configured endpoint. |📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.17.2)49-49: Table column count (MD056, table-column-count) 50-50: Table column count (MD056, table-column-count) 🤖 Prompt for AI Agents |
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,57 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @var tobimori\DreamForm\Models\FormPage $form | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @var tobimori\DreamForm\Guards\CapGuard $guard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use Kirby\Data\Json; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use tobimori\DreamForm\DreamForm; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use tobimori\DreamForm\Support\Htmx; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
tobimori marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Prepare attributes for the component | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $attrs = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'id' => 'cap', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'data-cap-api-endpoint' => $guard::endpoint(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'data-cap-i18n-verifying-label' => t('dreamform.guards.cap.i18n.verifying'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'data-cap-i18n-initial-state' => t('dreamform.guards.cap.i18n.initial'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'data-cap-i18n-solved-label' => t('dreamform.guards.cap.i18n.solved'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'data-cap-i18n-error-label' => t('dreamform.guards.cap.i18n.error') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Prepare attributes for the component | |
| $attrs = [ | |
| 'id' => 'cap', | |
| 'data-cap-api-endpoint' => $guard::endpoint(), | |
| 'data-cap-i18n-verifying-label' => t('dreamform.guards.cap.i18n.verifying'), | |
| 'data-cap-i18n-initial-state' => t('dreamform.guards.cap.i18n.initial'), | |
| 'data-cap-i18n-solved-label' => t('dreamform.guards.cap.i18n.solved'), | |
| 'data-cap-i18n-error-label' => t('dreamform.guards.cap.i18n.error') | |
| ]; | |
| // Prepare attributes for the component | |
| $attrs = [ | |
| // Use a unique ID per form; uid() generates a safe HTML id | |
| 'id' => 'cap-' . $form->uid(), | |
| 'data-cap-api-endpoint' => $guard::endpoint(), | |
| 'data-cap-i18n-verifying-label' => t('dreamform.guards.cap.i18n.verifying'), | |
| 'data-cap-i18n-initial-state' => t('dreamform.guards.cap.i18n.initial'), | |
| 'data-cap-i18n-solved-label' => t('dreamform.guards.cap.i18n.solved'), | |
| 'data-cap-i18n-error-label' => t('dreamform.guards.cap.i18n.error') | |
| ]; |
🤖 Prompt for AI Agents
In snippets/guards/cap.php around lines 12 to 20, the 'id' attribute is
statically set to 'cap', which can cause collisions if multiple forms render on
the same page. Modify the code to generate a unique ID for each Cap widget
instance, for example by appending a unique identifier or using a function that
generates unique strings, ensuring each form's Cap widget has a distinct 'id'
attribute.
tobimori marked this conversation as resolved.
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Script source concerns: package name, module type, and asset-server origin construction
- Package path looks like @cap.js/widget; please verify the correct scope (often it’s @cap-js/widget) and whether type="module" is required.
- For useAssetServer, IPv6 hosts need brackets when reconstructing the origin.
- $scriptSrc = 'https://cdn.jsdelivr.net/npm/@cap.js/widget';
+ // Verify the correct package name and module entry
+ $scriptSrc = 'https://cdn.jsdelivr.net/npm/@cap-js/widget';
if (DreamForm::option('guards.cap.useAssetServer')) {
$endpoint = $guard::endpoint();
if (is_string($endpoint) && $endpoint !== '') {
$parts = parse_url($endpoint);
if ($parts && isset($parts['scheme'], $parts['host'])) {
- $serverUrl = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : '');
+ $host = $parts['host'];
+ // Bracket IPv6 hosts
+ if (strpos($host, ':') !== false && $host[0] !== '[') {
+ $host = '[' . $host . ']';
+ }
+ $serverUrl = $parts['scheme'] . '://' . $host . (isset($parts['port']) ? ':' . $parts['port'] : '');
$scriptSrc = rtrim($serverUrl, '/') . '/assets/widget.js';
}
}
}🌐 Web query:
What is the canonical CDN URL and script tag for the Cap widget? Confirm:
- npm package scope/name
- whether the CDN requires type="module"
- recommended version pinning (e.g., @^x.y.z or explicit version)
💡 Result:
The Cap widget is a client-side component designed to integrate seamlessly into web applications. Here's the information you requested:
1. Canonical CDN URL and Script Tag:
To include the Cap widget in your project via a CDN, you can use the following script tag:
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget"></script>Alternatively, you can use unpkg:
<script src="https://unpkg.com/@cap.js/widget"></script>These URLs point to the latest version of the Cap widget. (uithub.com)
2. NPM Package Scope/Name:
The NPM package for the Cap widget is scoped under @cap.js with the name widget. (uithub.com)
3. Does the CDN Require type="module"?
The Cap widget is designed to be compatible with modern browsers and can be included using a standard <script> tag without the type="module" attribute. However, if you prefer to use ES modules, you can include it with type="module". Ensure that your project setup supports ES modules when using this approach. (uithub.com)
4. Recommended Version Pinning:
While the provided CDN URLs point to the latest version of the Cap widget, it's recommended to pin a specific version to avoid potential breaking changes in the future. For example, to pin version 0.1.17, you can use:
<script src="https://cdn.jsdelivr.net/npm/@cap.js/[email protected]"></script>This approach ensures that your application consistently uses the same version of the widget. (uithub.com)
By following these guidelines, you can effectively integrate the Cap widget into your web application.
Citations:
- 1: https://uithub.com/tiagorangel1/cap?utm_source=openai
- 2: https://uithub.com/tiagorangel1/cap?utm_source=openai
- 3: https://uithub.com/tiagorangel1/cap?utm_source=openai
- 4: https://uithub.com/tiagorangel1/cap?utm_source=openai
Update script source: pin version and handle IPv6 hosts
- The npm package scope/name is correctly
@cap.js/widget—no change needed. - To avoid unexpected breaking changes, pin a specific version in the CDN URL (e.g.,
@0.1.17). - A standard
<script>tag is sufficient;type="module"is optional if you need ES-module support. - When using
useAssetServer, bracket IPv6 hosts when rebuilding the origin.
Suggested diff:
- $scriptSrc = 'https://cdn.jsdelivr.net/npm/@cap.js/widget';
+ // Pin to a specific version to avoid breaking changes
+ $scriptSrc = 'https://cdn.jsdelivr.net/npm/@cap.js/[email protected]';
if (DreamForm::option('guards.cap.useAssetServer')) {
$endpoint = $guard::endpoint();
if (is_string($endpoint) && $endpoint !== '') {
$parts = parse_url($endpoint);
if ($parts && isset($parts['scheme'], $parts['host'])) {
- $serverUrl = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : '');
+ $host = $parts['host'];
+ // Bracket IPv6 hosts
+ if (strpos($host, ':') !== false && $host[0] !== '[') {
+ $host = '[' . $host . ']';
+ }
+ $serverUrl = $parts['scheme'] . '://' . $host . (isset($parts['port']) ? ':' . $parts['port'] : '');
$scriptSrc = rtrim($serverUrl, '/') . '/assets/widget.js';
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Inject script | |
| if (DreamForm::option('guards.cap.injectScript')) : | |
| $scriptSrc = 'https://cdn.jsdelivr.net/npm/@cap.js/widget'; | |
| if (DreamForm::option('guards.cap.useAssetServer')) { | |
| $endpoint = $guard::endpoint(); | |
| if (is_string($endpoint) && $endpoint !== '') { | |
| $parts = parse_url($endpoint); | |
| if ($parts && isset($parts['scheme'], $parts['host'])) { | |
| $serverUrl = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : ''); | |
| $scriptSrc = rtrim($serverUrl, '/') . '/assets/widget.js'; | |
| } | |
| } | |
| } | |
| // Inject script | |
| if (DreamForm::option('guards.cap.injectScript')) : | |
| // Pin to a specific version to avoid breaking changes | |
| $scriptSrc = 'https://cdn.jsdelivr.net/npm/@cap.js/[email protected]'; | |
| if (DreamForm::option('guards.cap.useAssetServer')) { | |
| $endpoint = $guard::endpoint(); | |
| if (is_string($endpoint) && $endpoint !== '') { | |
| $parts = parse_url($endpoint); | |
| if ($parts && isset($parts['scheme'], $parts['host'])) { | |
| $host = $parts['host']; | |
| // Bracket IPv6 hosts | |
| if (strpos($host, ':') !== false && $host[0] !== '[') { | |
| $host = '[' . $host . ']'; | |
| } | |
| $serverUrl = $parts['scheme'] . '://' . $host . (isset($parts['port']) ? ':' . $parts['port'] : ''); | |
| $scriptSrc = rtrim($serverUrl, '/') . '/assets/widget.js'; | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In snippets/guards/cap.php around lines 25 to 37, update the script source URL
to pin a specific version (e.g., @0.1.17) in the CDN path to prevent breaking
changes. When reconstructing the server URL from the endpoint, detect if the
host is an IPv6 address and wrap it in brackets to form a valid URL. Keep the
script tag standard without adding type="module" unless ES-module support is
required.
tobimori marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
HTMX reset: scope to the current widget and handle multi-widget pages
Current code resets the first cap-widget on every htmx:beforeSwap. Scope it to this form’s widget and handle multiple widgets safely.
- if (typeof htmx !== 'undefined') {
- htmx.on("htmx:beforeSwap", () => {
- // Reset the widget if needed
- const el = document.querySelector("cap-widget");
- if (el) {
- // Remove and recreate the widget for a clean reset
- const newEl = el.cloneNode(true);
- el.parentNode.replaceChild(newEl, el);
- }
- });
- }
+ if (typeof htmx !== 'undefined') {
+ htmx.on("htmx:beforeSwap", (evt) => {
+ const el = document.getElementById("cap-<?= $form->uid() ?>");
+ if (!el) return;
+ // Only reset if the swap targets a container that contains this widget
+ const target = evt?.detail?.target || evt?.target || document;
+ if (target && target.contains(el)) {
+ const newEl = el.cloneNode(true);
+ el.parentNode.replaceChild(newEl, el);
+ }
+ });
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <?php if (Htmx::isActive()) : ?> | |
| <script> | |
| // HTMX handlers for reset only | |
| if (typeof htmx !== 'undefined') { | |
| htmx.on("htmx:beforeSwap", () => { | |
| // Reset the widget if needed | |
| const el = document.querySelector("cap-widget"); | |
| if (el) { | |
| // Remove and recreate the widget for a clean reset | |
| const newEl = el.cloneNode(true); | |
| el.parentNode.replaceChild(newEl, el); | |
| } | |
| }); | |
| } | |
| </script> | |
| <?php endif; ?> | |
| <?php if (Htmx::isActive()) : ?> | |
| <script> | |
| // HTMX handlers for reset only | |
| if (typeof htmx !== 'undefined') { | |
| htmx.on("htmx:beforeSwap", (evt) => { | |
| const el = document.getElementById("cap-<?= $form->uid() ?>"); | |
| if (!el) return; | |
| // Only reset if the swap targets a container that contains this widget | |
| const target = evt?.detail?.target || evt?.target || document; | |
| if (target && target.contains(el)) { | |
| const newEl = el.cloneNode(true); | |
| el.parentNode.replaceChild(newEl, el); | |
| } | |
| }); | |
| } | |
| </script> | |
| <?php endif; ?> |
🤖 Prompt for AI Agents
In snippets/guards/cap.php between lines 41 and 56, the HTMX reset logic targets
only the first cap-widget globally, which can cause issues on pages with
multiple widgets. Modify the event handler to scope the reset to the widget
related to the current HTMX request by using the event target or closest form
element, and ensure it safely handles multiple widgets by resetting only the
relevant one instead of the first found globally.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,6 +182,10 @@ | |
| "forms": "Formuláře", | ||
| "forms.empty": "Žádné formuláře", | ||
| "fromField": "Z uživatelského vstupu", | ||
| "guards.cap.i18n.verifying": "Ověřování...", | ||
| "guards.cap.i18n.initial": "Jsem člověk", | ||
| "guards.cap.i18n.solved": "Jsem člověk", | ||
| "guards.cap.i18n.error": "Chyba", | ||
|
Comment on lines
+185
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Czech CAP i18n keys added — minor UX nit Keys are consistent. Consider using a distinct “solved” string (e.g., “Ověřeno”) to make state changes clearer for screen readers, instead of repeating “Jsem člověk”. 🤖 Prompt for AI Agents |
||
| "justNow": "právě teď", | ||
| "license.activate": "Aktivovat licenci", | ||
| "license.activate.domain": "Vaše licence bude aktivována pro doménu <strong>{{ domain }}</strong>.", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,6 +182,10 @@ | |
| "forms": "Formulare", | ||
| "forms.empty": "Keine Formulare", | ||
| "fromField": "Aus Eingabefeld", | ||
| "guards.cap.i18n.verifying": "Verifizieren...", | ||
| "guards.cap.i18n.initial": "Ich bin ein Mensch", | ||
| "guards.cap.i18n.solved": "Ich bin ein Mensch", | ||
| "guards.cap.i18n.error": "Fehler", | ||
|
Comment on lines
+185
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) German CAP i18n keys added — minor UX nit All good. You may want a distinct solved label (e.g., “Verifiziert”) rather than repeating “Ich bin ein Mensch” to improve a11y clarity. 🤖 Prompt for AI Agents |
||
| "justNow": "gerade eben", | ||
| "license.activate": "Lizenz aktivieren", | ||
| "license.activate.domain": "Deine Lizenz wird für die Domain <strong>{{ domain }}</strong> aktiviert.", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,6 +182,10 @@ | |
| "forms": "Forms", | ||
| "forms.empty": "No Forms", | ||
| "fromField": "From User Input", | ||
| "guards.cap.i18n.verifying": "Verifying...", | ||
| "guards.cap.i18n.initial": "I'm a human", | ||
| "guards.cap.i18n.solved": "I'm a human", | ||
| "guards.cap.i18n.error": "Error", | ||
|
Comment on lines
+185
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) English CAP i18n keys added — minor UX nit LGTM. Consider changing the solved label to “Verified” (or similar) so the state change is obvious to screen readers and visual users. 🤖 Prompt for AI Agents |
||
| "justNow": "just now", | ||
| "license.activate": "Activate license", | ||
| "license.activate.domain": "Your license will be activated for the domain <strong>{{ domain }}</strong>.", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,6 +182,10 @@ | |
| "forms": "Formularios", | ||
| "forms.empty": "No hay formularios", | ||
| "fromField": "De entrada de usuario", | ||
| "guards.cap.i18n.verifying": "Verificando...", | ||
| "guards.cap.i18n.initial": "Soy un humano", | ||
| "guards.cap.i18n.solved": "Soy un humano", | ||
| "guards.cap.i18n.error": "Error", | ||
|
Comment on lines
+185
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Spanish CAP i18n keys added — minor UX nit Looks good. Optionally differentiate the solved label (e.g., “Verificado”) instead of repeating “Soy un humano” to better convey state change for assistive tech. 🤖 Prompt for AI Agents |
||
| "justNow": "justo ahora", | ||
| "license.activate": "Activar licencia", | ||
| "license.activate.domain": "Tu licencia será activada para el dominio <strong>{{ domain }}</strong>.", | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.