Skip to content

Commit 4c89399

Browse files
committed
Merge branch 'release/8.2.0'
2 parents 0bee908 + b528593 commit 4c89399

File tree

8 files changed

+132
-25
lines changed

8 files changed

+132
-25
lines changed

CHANGELOG.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
# v8.2.0
2+
## 112/27/2025
3+
4+
1. [](#improved)
5+
- Use new Twig::setEscaper() helper if it exists
6+
- Automated form none refresh `refresh_nonce` (false by default)
7+
1. [](#bugfix)
8+
- Fix spacer field [#623](https://github.com/getgrav/grav-plugin-form/pulls/623)
9+
- Fix number field
10+
111
# v8.1.0
212
## 11/03/2025
313

4-
1. [](#bugfix)
5-
- Fixed an issue with DropZone file field with `js_pipeline` enabled [#621](https://github.com/getgrav/grav-plugin-form/issues/621)
6-
- Fixed general pipeline issues with form javascript
714
1. [](#improved)
815
- Added a field-based configuration of basic-captcha [#622](https://github.com/getgrav/grav-plugin-form/issues/622)
916
- Improved filesize min/max error handling
17+
2. [](#bugfix)
18+
- Fixed an issue with DropZone file field with `js_pipeline` enabled [#621](https://github.com/getgrav/grav-plugin-form/issues/621)
19+
- Fixed general pipeline issues with form javascript
1020

1121
# v8.0.6
1222
## 10/07/2025

assets/form-nonce-refresh.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
(function() {
2+
function refreshNonces() {
3+
var nonces = document.querySelectorAll('.form-nonce-field');
4+
if (nonces.length === 0) return;
5+
6+
var actions = {};
7+
nonces.forEach(function(field) {
8+
var action = field.getAttribute('data-nonce-action');
9+
if (!actions[action]) actions[action] = [];
10+
actions[action].push(field);
11+
});
12+
13+
Object.keys(actions).forEach(function(action) {
14+
try {
15+
var urlObj = new URL(window.location.href);
16+
urlObj.searchParams.set('task', 'get-nonce');
17+
urlObj.searchParams.set('action', action);
18+
var fetchUrl = urlObj.toString();
19+
20+
fetch(fetchUrl, {
21+
headers: { 'X-Requested-With': 'XMLHttpRequest' }
22+
})
23+
.then(function(response) { return response.json(); })
24+
.then(function(data) {
25+
if (data.nonce) {
26+
actions[action].forEach(function(field) {
27+
field.value = data.nonce;
28+
});
29+
}
30+
})
31+
.catch(function(e) {
32+
console.error('Grav Form Plugin: Failed to refresh nonce', e);
33+
});
34+
} catch (e) {
35+
console.error('Grav Form Plugin: URL parsing failed', e);
36+
}
37+
});
38+
}
39+
40+
// Refresh based on configured interval (default to 15 minutes)
41+
var interval = (window.GravForm && window.GravForm.refresh_nonce_interval) || 900000;
42+
43+
function scheduleRefresh() {
44+
var nonces = document.querySelectorAll('.form-nonce-field');
45+
if (nonces.length === 0) return;
46+
47+
var parsed = Number(interval);
48+
var delay = !isNaN(parsed) && parsed > 0 ? parsed : 900000;
49+
delay = Math.max(delay, 1000);
50+
setTimeout(function() {
51+
refreshNonces();
52+
setInterval(refreshNonces, delay);
53+
}, delay);
54+
}
55+
56+
if (document.readyState === 'loading') {
57+
document.addEventListener('DOMContentLoaded', scheduleRefresh);
58+
} else {
59+
scheduleRefresh();
60+
}
61+
62+
})();

blueprints.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: Form
22
slug: form
33
type: plugin
4-
version: 8.1.0
4+
version: 8.2.0
55
description: Enables forms handling and processing
66
icon: check-square
77
author:

form.php

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@
3232
use RocketTheme\Toolbox\Event\Event;
3333
use RuntimeException;
3434
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
35-
use Twig\Environment;
3635
use Twig\Extension\CoreExtension;
37-
use Twig\Extension\EscaperExtension;
3836
use Twig\TwigFunction;
3937
use function count;
4038
use function function_exists;
@@ -116,7 +114,20 @@ class_alias(Form::class, 'Grav\Plugin\Form');
116114

117115
// Initialize the captcha manager
118116
CaptchaManager::initialize();
119-
117+
118+
/** @var Uri $uri */
119+
$uri = $this->grav['uri'];
120+
121+
// Refresh Nonce Logic - Run early to catch both frontend and admin
122+
// Uri::param() returns false when missing, so use fallback even on falsey values.
123+
$task = $uri->param('task') ?: $uri->query('task') ?: ($_REQUEST['task'] ?? null);
124+
if ($task === 'get-nonce') {
125+
$action = $uri->param('action') ?: $uri->query('action') ?: ($_REQUEST['action'] ?? 'form');
126+
$nonce = Utils::getNonce($action);
127+
$response = new Response(200, ['Content-Type' => 'application/json'], json_encode(['nonce' => $nonce]));
128+
129+
$this->grav->close($response);
130+
}
120131

121132
if ($this->isAdmin()) {
122133
$this->enable([
@@ -126,11 +137,7 @@ class_alias(Form::class, 'Grav\Plugin\Form');
126137
return;
127138
}
128139

129-
/** @var Uri $uri */
130-
$uri = $this->grav['uri'];
131-
132140
// Mini Keep-Alive Logic
133-
$task = $uri->param('task');
134141
if ($task === 'keep-alive') {
135142
$response = new Response(200);
136143

@@ -368,17 +375,15 @@ public function onTwigInitialized(): void
368375
new TwigFunction('forms', [$this, 'getForm'])
369376
);
370377

371-
if (Environment::VERSION_ID > 20000) {
372-
// Twig 2/3
373-
$this->grav['twig']->twig()->getExtension(EscaperExtension::class)->setEscaper(
374-
'yaml',
375-
function ($twig, $string, $charset) {
376-
return Yaml::dump($string);
377-
}
378-
);
378+
// Grav 1.8+ has setEscaper() helper that handles all Twig versions
379+
$twig = $this->grav['twig'];
380+
if (method_exists($twig, 'setEscaper')) {
381+
$twig->setEscaper('yaml', function ($twig, $string, $charset) {
382+
return Yaml::dump($string);
383+
});
379384
} else {
380-
// Twig 1.x
381-
$this->grav['twig']->twig()->getExtension(CoreExtension::class)->setEscaper(
385+
// Grav 1.7 with Twig 1.x
386+
$twig->twig()->getExtension(CoreExtension::class)->setEscaper(
382387
'yaml',
383388
function ($twig, $string, $charset) {
384389
return Yaml::dump($string);
@@ -441,6 +446,18 @@ public function onTwigVariables(?Event $event = null): void
441446
if ($this->config->get('plugins.form.built_in_css')) {
442447
$this->grav['assets']->addCss('plugin://form/assets/form-styles.css');
443448
}
449+
if ($this->config->get('plugins.form.refresh_nonce')) {
450+
$timeout = (int)$this->config->get('system.session.timeout', 1800);
451+
// Nonce lifetime is ~12h (current + previous tick); cap refresh window to that.
452+
$effectiveTimeout = min($timeout, 43200);
453+
// Refresh close to expiry: 10% lead time, capped between 5s and 60s.
454+
$leadTime = min(60, max(5, (int)round($effectiveTimeout * 0.10)));
455+
$intervalSeconds = max(1, $effectiveTimeout - $leadTime);
456+
$interval = $intervalSeconds * 1000;
457+
458+
$this->grav['assets']->addInlineJs("window.GravForm = window.GravForm || {}; window.GravForm.refresh_nonce_interval = $interval;", ['group' => 'bottom', 'position' => 'before']);
459+
$this->grav['assets']->addJs('plugin://form/assets/form-nonce-refresh.js', ['group' => 'bottom', 'defer' => true]);
460+
}
444461
$twig->twig_vars['form_max_filesize'] = Form::getMaxFilesize();
445462
$twig->twig_vars['form_json_response'] = $this->json_response;
446463
}

form.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
enabled: true
22
built_in_css: true
33
inline_css: true
4+
refresh_nonce: false
45
refresh_prevention: false
56
client_side_validation: true
67
debug: false
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
{{ nonce_field(form.getNonceAction() ?? 'form', form.getNonceName() ?? 'form-nonce')|raw }}
1+
{% set nonce_action = form.getNonceAction() ?? 'form' %}
2+
{% set nonce_name = form.getNonceName() ?? 'form-nonce' %}
3+
{% set nonce_value = form.getNonce() %}
4+
<input type="hidden" name="{{ nonce_name }}" value="{{ nonce_value }}" class="form-nonce-field" data-nonce-action="{{ nonce_action }}" />

templates/forms/fields/number/number.html.twig

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,19 @@
55
{% if field.validate.min is defined %}min="{{ field.validate.min }}"{% endif %}
66
{% if field.validate.max is defined %}max="{{ field.validate.max }}"{% endif %}
77
{% if field.validate.step is defined %}step="{{ field.validate.step }}"{% endif %}
8-
{{ parent() }}
8+
{# Skip text.html.twig's minlength/maxlength and go directly to field.html.twig #}
9+
{% if field.size %}size="{{ field.size }}"{% endif %}
10+
{% if field.classes is defined %}class="{{ field.classes }}" {% endif %}
11+
{% if field.id is defined %}id="{{ field.id }}" {% endif %}
12+
{% if field.style is defined %}style="{{ field.style }}" {% endif %}
13+
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
14+
{% if field.placeholder %}placeholder="{{ field.placeholder|t }}"{% endif %}
15+
{% if field.autofocus in ['on', 'true', 1] %}autofocus="autofocus"{% endif %}
16+
{% if field.novalidate in ['on', 'true', 1] %}novalidate="novalidate"{% endif %}
17+
{% if field.readonly in ['on', 'true', 1] %}readonly="readonly"{% endif %}
18+
{% if field.autocomplete is defined %}autocomplete="{{ field.autocomplete }}"{% endif %}
19+
{% if field.validate.required in ['on', 'true', 1] %}required="required"{% endif %}
20+
{% if field.validate.pattern %}pattern="{{ field.validate.pattern }}"{% endif %}
21+
{% if field.validate.message %}title="{{ field.validate.message|t }}"
22+
{% elseif field.title is defined %}title="{{ field.title|t }}" {% endif %}
923
{% endblock %}

templates/forms/fields/spacer/spacer.html.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
{% block field %}
44
<div class="form-field form-spacer {{ field.classes }}">
55
{% if field.title %}
6-
<{{ title_type|default('h3') }}>{{- field.title|t|raw -}}</ {{ title_type|default('h3') }}>
6+
<{{ field.title_type|default('h3') }}>{{- field.title|t|raw -}}</{{ field.title_type|default('h3') }}>
77
{% endif %}
88

99
{% if field.markdown %}
10-
<p>{{- field.text|t|markdown|raw -}}</p>
10+
{{- field.text|t|markdown|raw -}}
1111
{% else %}
1212
<p>{{- field.text|t|raw -}}</p>
1313
{% endif %}

0 commit comments

Comments
 (0)