Skip to content

Commit 0bee908

Browse files
committed
Merge branch 'release/8.1.0'
2 parents 5005941 + 4b43f4a commit 0bee908

File tree

7 files changed

+157
-37
lines changed

7 files changed

+157
-37
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# v8.1.0
2+
## 11/03/2025
3+
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
7+
1. [](#improved)
8+
- Added a field-based configuration of basic-captcha [#622](https://github.com/getgrav/grav-plugin-form/issues/622)
9+
- Improved filesize min/max error handling
10+
111
# v8.0.6
212
## 10/07/2025
313

assets/captcha/basic-captcha-refresh.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@
99
return;
1010
}
1111

12-
// Force reload by adding/updating timestamp
12+
// Get the base URL and field ID
13+
const baseUrl = img.dataset.baseUrl || img.src.split('?')[0];
14+
const fieldId = img.dataset.fieldId || container.dataset.fieldId;
15+
16+
// Force reload by adding/updating timestamp and field ID
1317
const timestamp = new Date().getTime();
14-
const baseUrl = img.src.split('?')[0];
15-
img.src = baseUrl + '?t=' + timestamp;
18+
let newUrl = baseUrl + '?t=' + timestamp;
19+
if (fieldId) {
20+
newUrl += '&field=' + fieldId;
21+
}
22+
img.src = newUrl;
1623

1724
// Also clear the input field if we can find it
1825
const formField = container.closest('.form-field');

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.0.6
4+
version: 8.1.0
55
description: Enables forms handling and processing
66
icon: check-square
77
author:

classes/Captcha/BasicCaptcha.php

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,42 @@ class BasicCaptcha
88
{
99
protected $session = null;
1010
protected $key = 'basic_captcha_value';
11+
protected $typeKey = 'basic_captcha_type';
12+
protected $config = null;
1113

12-
public function __construct()
14+
public function __construct($fieldConfig = null)
1315
{
1416
$this->session = Grav::instance()['session'];
17+
18+
// Load global configuration
19+
$globalConfig = Grav::instance()['config']->get('plugins.form.basic_captcha', []);
20+
21+
// Merge field-specific config with global config
22+
if ($fieldConfig && is_array($fieldConfig)) {
23+
$this->config = array_replace_recursive($globalConfig, $fieldConfig);
24+
} else {
25+
$this->config = $globalConfig;
26+
}
1527
}
1628

1729
public function getCaptchaCode($length = null): string
1830
{
19-
$config = Grav::instance()['config']->get('plugins.form.basic_captcha');
20-
$type = $config['type'] ?? 'characters';
31+
// Support both 'type' (from global config) and 'captcha_type' (from field config)
32+
$type = $this->config['captcha_type'] ?? $this->config['type'] ?? 'characters';
33+
34+
// Store the captcha type in session for validation
35+
$this->setSession($this->typeKey, $type);
2136

2237
switch ($type) {
2338
case 'dotcount':
24-
return $this->getDotCountCaptcha($config);
39+
return $this->getDotCountCaptcha($this->config);
2540
case 'position':
26-
return $this->getPositionCaptcha($config);
41+
return $this->getPositionCaptcha($this->config);
2742
case 'math':
28-
return $this->getMathCaptcha($config);
43+
return $this->getMathCaptcha($this->config);
2944
case 'characters':
3045
default:
31-
return $this->getCharactersCaptcha($config, $length);
46+
return $this->getCharactersCaptcha($this->config, $length);
3247
}
3348
}
3449

@@ -158,15 +173,37 @@ public function getSession($key = null): ?string
158173
*/
159174
public function createCaptchaImage($captcha_code)
160175
{
161-
$config = Grav::instance()['config']->get('plugins.form.basic_captcha');
162-
$width = $config['image']['width'] ?? 135;
163-
$height = $config['image']['height'] ?? 40;
176+
// Determine image dimensions based on type
177+
$isCharacterCaptcha = false;
178+
if (strpos($captcha_code, '|') === false && !preg_match('/[\+\-x]/', $captcha_code)) {
179+
$isCharacterCaptcha = true;
180+
}
181+
182+
// Use box_width/box_height for character captchas if specified, otherwise use default image dimensions
183+
if ($isCharacterCaptcha && isset($this->config['chars']['box_width'])) {
184+
$width = $this->config['chars']['box_width'];
185+
} else {
186+
$width = $this->config['image']['width'] ?? 135;
187+
}
188+
189+
if ($isCharacterCaptcha && isset($this->config['chars']['box_height'])) {
190+
$height = $this->config['chars']['box_height'];
191+
} else {
192+
$height = $this->config['image']['height'] ?? 40;
193+
}
164194

165195
// Create a blank image
166196
$image = imagecreatetruecolor($width, $height);
167197

168-
// Set background color
169-
$bg = $this->hexToRgb($config['image']['bg'] ?? '#ffffff');
198+
// Set background color (support both image.bg and chars.bg for character captchas)
199+
$bgColor = '#ffffff';
200+
if ($isCharacterCaptcha && isset($this->config['chars']['bg'])) {
201+
$bgColor = $this->config['chars']['bg'];
202+
} elseif (isset($this->config['image']['bg'])) {
203+
$bgColor = $this->config['image']['bg'];
204+
}
205+
206+
$bg = $this->hexToRgb($bgColor);
170207
$backgroundColor = imagecolorallocate($image, $bg[0], $bg[1], $bg[2]);
171208
imagefill($image, 0, 0, $backgroundColor);
172209

@@ -177,16 +214,16 @@ public function createCaptchaImage($captcha_code)
177214

178215
switch ($type) {
179216
case 'count_dots':
180-
return $this->createDotCountImage($image, $parts, $config);
217+
return $this->createDotCountImage($image, $parts, $this->config);
181218
case 'position':
182-
return $this->createPositionImage($image, $parts, $config);
219+
return $this->createPositionImage($image, $parts, $this->config);
183220
}
184221
} else {
185222
// Assume it's a character or math captcha if no type indicator
186223
if (preg_match('/[\+\-x]/', $captcha_code)) {
187-
return $this->createMathImage($image, $captcha_code, $config);
224+
return $this->createMathImage($image, $captcha_code, $this->config);
188225
} else {
189-
return $this->createCharacterImage($image, $captcha_code, $config);
226+
return $this->createCharacterImage($image, $captcha_code, $this->config);
190227
}
191228
}
192229

@@ -408,21 +445,28 @@ protected function createCharacterImage($image, $captcha_code, $config)
408445
$width = imagesx($image);
409446
$height = imagesy($image);
410447

411-
// Get font settings
448+
// Get font settings with support for custom box dimensions, position, and colors
412449
$fontPath = __DIR__.'/../../fonts/'.($config['chars']['font'] ?? 'zxx-xed.ttf');
413450
$fontSize = $config['chars']['size'] ?? 16;
414-
$textColor = imagecolorallocate($image, 0, 0, 0);
451+
452+
// Support custom text color (defaults to black)
453+
$textColorHex = $config['chars']['text'] ?? '#000000';
454+
$textRgb = $this->hexToRgb($textColorHex);
455+
$textColor = imagecolorallocate($image, $textRgb[0], $textRgb[1], $textRgb[2]);
456+
457+
// Support custom start position (useful for fine-tuning text placement)
458+
$startX = $config['chars']['start_x'] ?? ($width / (strlen($captcha_code) + 2));
459+
$baseY = $config['chars']['start_y'] ?? ($height / 2 + 5);
415460

416461
// Draw each character with random rotation and position
417462
$charWidth = $width / (strlen($captcha_code) + 2);
418-
$startX = $charWidth; // Leave some space at the beginning
419463

420464
for ($i = 0; $i < strlen($captcha_code); $i++) {
421465
$char = $captcha_code[$i];
422466
$angle = mt_rand(-15, 15); // Random rotation
423467

424-
// Random vertical position
425-
$y = mt_rand($height / 2 - 5, $height / 2 + 5);
468+
// Random vertical position with custom base Y
469+
$y = $baseY + mt_rand(-5, 5);
426470

427471
imagettftext($image, $fontSize, $angle, $startX, $y, $textColor, $fontPath, $char);
428472

classes/Captcha/BasicCaptchaProvider.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public function validate(array $form, array $params = []): array
2929
// Make sure to use the same session key that the image generation code uses
3030
$expectedValue = $session->basic_captcha_value ?? null; // Changed from basic_captcha to basic_captcha_value
3131

32+
// Get the captcha type from session (stored during generation)
33+
$captchaType = $session->basic_captcha_type ?? null;
34+
3235
// Get the user's answer
3336
$userValue = $form['basic-captcha'] ?? null;
3437

@@ -48,13 +51,16 @@ public function validate(array $form, array $params = []): array
4851
];
4952
}
5053

51-
// Compare the values (case-insensitive string comparison for character captchas)
52-
$captchaType = $this->config['type'] ?? 'math';
54+
// Compare the values based on the type stored in session
55+
// If type is not in session, try to infer from global/field config
56+
if (!$captchaType) {
57+
$captchaType = $this->config['captcha_type'] ?? $this->config['type'] ?? 'characters';
58+
}
5359

5460
if ($captchaType === 'characters') {
5561
$isValid = strtolower((string)$userValue) === strtolower((string)$expectedValue);
5662
} else {
57-
// For math, ensure both are treated as integers
63+
// For math, dotcount, position - ensure both are treated as integers or exact match
5864
$isValid = (int)$userValue === (int)$expectedValue;
5965
}
6066

@@ -69,8 +75,9 @@ public function validate(array $form, array $params = []): array
6975
];
7076
}
7177

72-
// Clear the session value to prevent reuse
78+
// Clear the session values to prevent reuse
7379
$session->basic_captcha_value = null;
80+
$session->basic_captcha_type = null;
7481

7582
return [
7683
'success' => true
@@ -89,14 +96,31 @@ public function validate(array $form, array $params = []): array
8996
*/
9097
public function getClientProperties(string $formId, array $field): array
9198
{
92-
$captchaType = $field['basic_captcha_type'] ?? $this->config['type'] ?? 'math';
99+
$grav = Grav::instance();
100+
$session = $grav['session'];
101+
102+
// Merge field-level configuration with global defaults
103+
$fieldConfig = array_replace_recursive($this->config, $field);
104+
105+
// Remove non-config keys from field array
106+
unset($fieldConfig['type'], $fieldConfig['label'], $fieldConfig['placeholder'],
107+
$fieldConfig['validate'], $fieldConfig['name'], $fieldConfig['classes']);
108+
109+
// Generate unique field ID for this form/field combination
110+
$fieldId = md5($formId . '_basic_captcha_' . ($field['name'] ?? 'default'));
111+
112+
// Store field configuration in session for image generation
113+
$session->{"basic_captcha_config_{$fieldId}"} = $fieldConfig;
114+
115+
$captchaType = $fieldConfig['type'] ?? 'math';
93116

94117
return [
95118
'provider' => 'basic-captcha',
96119
'type' => $captchaType,
97-
'imageUrl' => '/forms-basic-captcha-image.jpg',
120+
'imageUrl' => "/forms-basic-captcha-image.jpg?field={$fieldId}",
98121
'refreshable' => true,
99-
'containerId' => "basic-captcha-{$formId}"
122+
'containerId' => "basic-captcha-{$formId}",
123+
'fieldId' => $fieldId
100124
];
101125
}
102126

form.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,14 @@ public function onTwigExtensions(): void
398398
$twig->addFunction(new TwigFunction('captcha_template_exists', function ($template) use ($twig) {
399399
return $twig->getLoader()->exists($template);
400400
}));
401+
402+
// Add function to store basic captcha configuration in session
403+
$twig->addFunction(new TwigFunction('store_basic_captcha_config', function ($fieldId, $config) {
404+
$session = $this->grav['session'];
405+
$sessionKey = "basic_captcha_config_{$fieldId}";
406+
$session->{$sessionKey} = $config;
407+
return true;
408+
}));
401409
}
402410

403411
/**
@@ -1242,7 +1250,19 @@ protected function udate($format = 'u', $raw = false)
12421250
protected function processBasicCaptchaImage(Uri $uri): void
12431251
{
12441252
if ($uri->path() === '/forms-basic-captcha-image.jpg') {
1245-
$captcha = new BasicCaptcha();
1253+
// Get field ID from query parameter
1254+
$fieldId = $_GET['field'] ?? null;
1255+
$fieldConfig = null;
1256+
1257+
// Retrieve field-specific configuration from session if available
1258+
if ($fieldId) {
1259+
$session = $this->grav['session'];
1260+
$sessionKey = "basic_captcha_config_{$fieldId}";
1261+
$fieldConfig = $session->{$sessionKey} ?? null;
1262+
}
1263+
1264+
// Create captcha with field-specific or global config
1265+
$captcha = new BasicCaptcha($fieldConfig);
12461266
$code = $captcha->getCaptchaCode();
12471267
$image = $captcha->createCaptchaImage($code);
12481268
$captcha->renderCaptchaImage($image);

templates/forms/fields/basic-captcha/basic-captcha.html.twig

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,24 @@
33
{% extends "forms/field.html.twig" %}
44

55
{% block prepend %}
6-
<div class="form-input-addon form-input-prepend" data-captcha-provider="basic-captcha">
7-
<img id="basic-captcha-reload-{{ form.id }}" src="{{ url('/forms-basic-captcha-image.jpg') }}" alt="human test" />
8-
<button type="button" id="reload-captcha-{{ form.id }}" class="reload-captcha-button"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="M14.74 22.39c4.68-1.24 8-5.49 8-10.4 0-5.95-4.79-10.75-10.75-10.75 -3.11 0-5.78 1.11-7.99 2.95 -.77.64-1.43 1.32-1.98 2.01 -.34.41-.57.75-.69.95 -.22.35-.1.81.25 1.02 .35.21.81.09 1.02-.26 .08-.15.27-.43.56-.79 .49-.62 1.08-1.23 1.76-1.81C6.87 3.67 9.21 2.7 11.94 2.7c5.13 0 9.25 4.12 9.25 9.25 0 4.22-2.86 7.88-6.9 8.94 -.41.1-.64.51-.54.91 .1.4.51.63.91.53Zm-12-14.84V2.99c-.001-.42-.34-.75-.75-.75 -.42 0-.75.33-.75.75v4.56c0 .41.33.75.75.75 .41 0 .75-.34.75-.75Zm-.75.75H4h2.43c.41 0 .75-.34.75-.75 0-.42-.34-.75-.75-.75H4 1.99c-.42 0-.75.33-.75.75 0 .41.33.75.75.75Z"/><path d="M1.25 12c0 1.09.16 2.16.48 3.18 .12.39.54.61.93.49 .39-.13.61-.55.49-.94 -.28-.89-.42-1.81-.42-2.75 0-.42-.34-.75-.75-.75 -.42 0-.75.33-.75.75Zm1.93 6.15c.61.88 1.36 1.67 2.22 2.33 .32.25.79.19 1.05-.14 .25-.33.19-.8-.14-1.06 -.74-.58-1.38-1.25-1.92-2.02 -.24-.34-.71-.43-1.05-.19 -.34.23-.43.7-.19 1.04Zm5.02 3.91c1 .37 2.06.6 3.15.66 .41.02.76-.3.79-.71 .02-.42-.3-.77-.71-.8 -.94-.06-1.85-.25-2.72-.58 -.39-.15-.83.04-.97.43 -.15.38.04.82.43.96Z"/></g></svg></button>
6+
{% set field_id = field.name|default('default') %}
7+
{% set config_hash = (form.id ~ '_basic_captcha_' ~ field_id)|md5 %}
8+
{% set image_url = url('/forms-basic-captcha-image.jpg') ~ '?field=' ~ config_hash %}
9+
10+
{# Store field configuration in session for image generation #}
11+
{% set global_config = grav.config.get('plugins.form.basic_captcha', {}) %}
12+
{% set merged_config = global_config|merge(field) %}
13+
{% do store_basic_captcha_config(config_hash, merged_config) %}
14+
15+
<div class="form-input-addon form-input-prepend"
16+
data-captcha-provider="basic-captcha"
17+
data-field-id="{{ config_hash }}">
18+
<img id="basic-captcha-reload-{{ form.id }}"
19+
src="{{ image_url }}"
20+
alt="human test"
21+
data-base-url="{{ url('/forms-basic-captcha-image.jpg') }}"
22+
data-field-id="{{ config_hash }}" />
23+
<button type="button" id="reload-captcha-{{ form.id }}" class="reload-captcha-button"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="M14.74 22.39c4.68-1.24 8-5.49 8-10.4 0-5.95-4.79-10.75-10.75-10.75 -3.11 0-5.78 1.11-7.99 2.95 -.77.64-1.43 1.32-1.98 2.01 -.34.41-.57.75-.69.95 -.22.35-.1.81.25 1.02 .35.21.81.09 1.02-.26 .08-.15.27-.43.56-.79 .49-.62 1.08-1.23 1.76-1.81C6.87 3.67 9.21 2.7 11.94 2.7c5.13 0 9.25 4.12 9.25 9.25 0 4.22-2.86 7.88-6.9 8.94 -.41.1-.64.51-.54.91 .1.4.51.63.91.53Zm-12-14.84V2.99c-.001-.42-.34-.75-.75-.75 -.42 0-.75.33-.75.75v4.56c0 .41.33.75.75.75 .41 0 .75-.34.75-.75Zm-.75.75H4h2.43c.41 0 .75-.34.75-.75 0-.42-.34-.75-.75-.75H4 1.99c-.42 0-.75.33-.75.75 0 .41.33.75.75.75Z"/><path d="M1.25 12c0 1.09.16 2.16.48 3.18 .12.39.54.61.93.49 .39-.13.61-.55.49-.94 -.28-.89-.42-1.81-.42-2.75 0-.42-.34-.75-.75-.75 -.42 0-.75.33-.75.75Zm1.93 6.15c.61.88 1.36 1.67 2.22 2.33 .32.25.79.19 1.05-.14 .25-.33.19-.8-.14-1.06 -.74-.58-1.38-1.25-1.92-2.02 -.24-.34-.71-.43-1.05-.19 -.34.23-.43.7-.19 1.04Zm5.02 3.91c1 .37 2.06.6 3.15.66 .41.02.76-.3.79-.71 .02-.42-.30-.77-.71-.80 -.94-.06-1.85-.25-2.72-.58 -.39-.15-.83.04-.97.43 -.15.38.04.82.43.96Z"/></g></svg></button>
924
</div>
1025

1126
{% do assets.addJs('plugin://form/assets/captcha/basic-captcha-refresh.js') %}
@@ -17,4 +32,4 @@
1732
{% if field.minlength is defined or field.validate.min is defined %}minlength="{{ field.minlength | default(field.validate.min) }}"{% endif %}
1833
{% if field.maxlength is defined or field.validate.max is defined %}maxlength="{{ field.maxlength | default(field.validate.max) }}"{% endif %}
1934
{{ parent() }}
20-
{% endblock %}
35+
{% endblock %}

0 commit comments

Comments
 (0)