From 06f8a484b2441eda4ba413b4402c9bad41687797 Mon Sep 17 00:00:00 2001 From: Zsolt Nagy Date: Fri, 30 Jan 2026 17:04:48 +0100 Subject: [PATCH 1/5] Add file size validation. --- .../server_general/server_general.module | 28 ++++++ .../server_theme/server_theme.libraries.yml | 9 ++ .../src/js/file-size-validation.js | 85 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 web/themes/custom/server_theme/src/js/file-size-validation.js diff --git a/web/modules/custom/server_general/server_general.module b/web/modules/custom/server_general/server_general.module index 795c8c19f..c4be045a7 100644 --- a/web/modules/custom/server_general/server_general.module +++ b/web/modules/custom/server_general/server_general.module @@ -5,6 +5,7 @@ * Module file. */ +use Drupal\Component\Utility\Bytes; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\ContentEntityFormInterface; use Drupal\Core\Entity\EntityInterface; @@ -373,4 +374,31 @@ function server_general_page_attachments(array &$attachments) { ]; $attachments['#attached']['html_head'][] = [$noindex_tag, 'noindex_error_pages']; } + + // Attach file size validation library and settings for client-side + // validation of file uploads. + $attachments['#attached']['library'][] = 'server_theme/file-size-validation'; + $attachments['#attached']['drupalSettings']['fileSizeValidation'] = [ + 'maxFileSize' => server_general_get_max_upload_size(), + ]; +} + +/** + * Get the maximum file upload size in bytes. + * + * Returns the smaller of upload_max_filesize and post_max_size PHP settings. + * + * @return int + * Maximum upload size in bytes. + */ +function server_general_get_max_upload_size(): int { + $upload_max = Bytes::toNumber(ini_get('upload_max_filesize')); + $post_max = Bytes::toNumber(ini_get('post_max_size')); + + // post_max_size of 0 means unlimited. + if ($post_max == 0) { + return (int) $upload_max; + } + + return (int) min($upload_max, $post_max); } diff --git a/web/themes/custom/server_theme/server_theme.libraries.yml b/web/themes/custom/server_theme/server_theme.libraries.yml index 6b05401da..2fb3a046f 100644 --- a/web/themes/custom/server_theme/server_theme.libraries.yml +++ b/web/themes/custom/server_theme/server_theme.libraries.yml @@ -87,3 +87,12 @@ expanding-text: - core/drupal - core/jquery - core/once + +file-size-validation: + js: + dist/js/file-size-validation.js: {} + dependencies: + - core/drupal + - core/drupalSettings + - core/jquery + - core/once diff --git a/web/themes/custom/server_theme/src/js/file-size-validation.js b/web/themes/custom/server_theme/src/js/file-size-validation.js new file mode 100644 index 000000000..4f41102ab --- /dev/null +++ b/web/themes/custom/server_theme/src/js/file-size-validation.js @@ -0,0 +1,85 @@ +/** + * @file + * Client-side file size validation to prevent upload errors. + * + * Validates file size before upload to show a user-friendly error message + * instead of cryptic AJAX errors when files exceed PHP limits. + */ +(function ($, Drupal, once) { + + /** + * Validates file size before upload. + * + * @type {Drupal~behavior} + */ + Drupal.behaviors.fileSizeValidation = { + attach: function (context, settings) { + const maxFileSize = settings.fileSizeValidation?.maxFileSize; + if (!maxFileSize) { + return; + } + + const $fileInputs = $( + once('file-size-validate', 'input[type="file"]', context) + ); + + if (!$fileInputs.length) { + return; + } + + $fileInputs.on('change.fileSizeValidation', function (event) { + const input = this; + const files = input.files; + + if (!files || !files.length) { + return; + } + + // Remove any previous file size errors. + $(input) + .closest('div.js-form-managed-file, .form-item') + .find('.file-size-error') + .remove(); + + // Check each file's size. + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (file.size > maxFileSize) { + const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2); + const maxSizeMB = (maxFileSize / (1024 * 1024)).toFixed(0); + + const error = Drupal.t( + 'The file "@filename" (@filesize MB) exceeds the maximum upload size of @maxsize MB. Please choose a smaller file.', + { + '@filename': file.name, + '@filesize': fileSizeMB, + '@maxsize': maxSizeMB, + } + ); + + $(input) + .closest('div.js-form-managed-file, .form-item') + .prepend( + '
' + error + '
' + ); + + // Clear the file input to prevent upload attempt. + input.value = ''; + + // Stop processing and prevent upload. + event.stopImmediatePropagation(); + return; + } + } + }); + }, + detach: function (context, settings, trigger) { + if (trigger === 'unload') { + $(once.remove('file-size-validate', 'input[type="file"]', context)).off( + '.fileSizeValidation' + ); + } + }, + }; + +})(jQuery, Drupal, once); From 34e28576035d1738d2e5b785fa0e536ded2ae542 Mon Sep 17 00:00:00 2001 From: Zsolt Nagy Date: Thu, 12 Feb 2026 10:35:23 +0100 Subject: [PATCH 2/5] Block file upload if size validation fails. --- .../server_general/server_general.module | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/web/modules/custom/server_general/server_general.module b/web/modules/custom/server_general/server_general.module index c4be045a7..009a95557 100644 --- a/web/modules/custom/server_general/server_general.module +++ b/web/modules/custom/server_general/server_general.module @@ -375,14 +375,26 @@ function server_general_page_attachments(array &$attachments) { $attachments['#attached']['html_head'][] = [$noindex_tag, 'noindex_error_pages']; } - // Attach file size validation library and settings for client-side - // validation of file uploads. - $attachments['#attached']['library'][] = 'server_theme/file-size-validation'; + // Attach file size validation settings for client-side validation of file + // uploads. The library itself is loaded via hook_library_info_alter() as a + // dependency of core's drupal.file, ensuring our change handler fires before + // core's auto-upload handler. $attachments['#attached']['drupalSettings']['fileSizeValidation'] = [ 'maxFileSize' => server_general_get_max_upload_size(), ]; } +/** + * Implements hook_library_info_alter(). + */ +function server_general_library_info_alter(&$libraries, $extension) { + // Make our file size validation library load before core's file JS, so our + // change event handler is registered first and can prevent the upload. + if ($extension === 'file' && isset($libraries['drupal.file'])) { + $libraries['drupal.file']['dependencies'][] = 'server_theme/file-size-validation'; + } +} + /** * Get the maximum file upload size in bytes. * From 39cd217b3f5da9571a1170b8031bac01510fcc31 Mon Sep 17 00:00:00 2001 From: Zsolt Nagy Date: Thu, 12 Feb 2026 10:41:30 +0100 Subject: [PATCH 3/5] Phpstan fix. --- web/modules/custom/server_general/server_general.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/modules/custom/server_general/server_general.module b/web/modules/custom/server_general/server_general.module index 009a95557..5d3511701 100644 --- a/web/modules/custom/server_general/server_general.module +++ b/web/modules/custom/server_general/server_general.module @@ -387,7 +387,7 @@ function server_general_page_attachments(array &$attachments) { /** * Implements hook_library_info_alter(). */ -function server_general_library_info_alter(&$libraries, $extension) { +function server_general_library_info_alter(array &$libraries, string $extension): void { // Make our file size validation library load before core's file JS, so our // change event handler is registered first and can prevent the upload. if ($extension === 'file' && isset($libraries['drupal.file'])) { From fe2636380a6c1ae0fea28069aeaf8797e3f1b237 Mon Sep 17 00:00:00 2001 From: Zsolt Nagy Date: Thu, 12 Feb 2026 10:51:46 +0100 Subject: [PATCH 4/5] Consider file limit, not only php limit. --- .../server_general/server_general.module | 25 +++++++++++++++++++ .../src/js/file-size-validation.js | 9 +++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/web/modules/custom/server_general/server_general.module b/web/modules/custom/server_general/server_general.module index 5d3511701..daa0c888a 100644 --- a/web/modules/custom/server_general/server_general.module +++ b/web/modules/custom/server_general/server_general.module @@ -384,6 +384,31 @@ function server_general_page_attachments(array &$attachments) { ]; } +/** + * Implements hook_element_info_alter(). + */ +function server_general_element_info_alter(array &$info): void { + // Add a process callback to managed_file elements to pass the per-field + // file size limit to the client as a data attribute. + if (isset($info['managed_file'])) { + $info['managed_file']['#process'][] = 'server_general_managed_file_process'; + } +} + +/** + * Process callback for managed_file elements. + * + * Adds a data-max-filesize attribute to the upload input so client-side + * validation can use the per-field limit (which is already the min of the + * PHP limit and the configured field limit). + */ +function server_general_managed_file_process(array &$element, FormStateInterface &$form_state, array &$form): array { + if (isset($element['#upload_validators']['FileSizeLimit']['fileLimit'])) { + $element['upload']['#attributes']['data-max-filesize'] = (int) $element['#upload_validators']['FileSizeLimit']['fileLimit']; + } + return $element; +} + /** * Implements hook_library_info_alter(). */ diff --git a/web/themes/custom/server_theme/src/js/file-size-validation.js b/web/themes/custom/server_theme/src/js/file-size-validation.js index 4f41102ab..b4892f687 100644 --- a/web/themes/custom/server_theme/src/js/file-size-validation.js +++ b/web/themes/custom/server_theme/src/js/file-size-validation.js @@ -35,6 +35,11 @@ return; } + // Use per-field limit if available (min of PHP and field config), + // otherwise fall back to the global PHP limit. + const fieldLimit = parseInt(input.getAttribute('data-max-filesize'), 10); + const effectiveMax = fieldLimit > 0 ? fieldLimit : maxFileSize; + // Remove any previous file size errors. $(input) .closest('div.js-form-managed-file, .form-item') @@ -44,9 +49,9 @@ // Check each file's size. for (let i = 0; i < files.length; i++) { const file = files[i]; - if (file.size > maxFileSize) { + if (file.size > effectiveMax) { const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2); - const maxSizeMB = (maxFileSize / (1024 * 1024)).toFixed(0); + const maxSizeMB = (effectiveMax / (1024 * 1024)).toFixed(0); const error = Drupal.t( 'The file "@filename" (@filesize MB) exceeds the maximum upload size of @maxsize MB. Please choose a smaller file.', From 873b495b17ea09ec9f1d100544d617abca7158f7 Mon Sep 17 00:00:00 2001 From: Zsolt Nagy Date: Thu, 12 Feb 2026 10:59:06 +0100 Subject: [PATCH 5/5] Make it work pre D10.2. --- web/modules/custom/server_general/server_general.module | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/modules/custom/server_general/server_general.module b/web/modules/custom/server_general/server_general.module index daa0c888a..391a49f0b 100644 --- a/web/modules/custom/server_general/server_general.module +++ b/web/modules/custom/server_general/server_general.module @@ -403,9 +403,13 @@ function server_general_element_info_alter(array &$info): void { * PHP limit and the configured field limit). */ function server_general_managed_file_process(array &$element, FormStateInterface &$form_state, array &$form): array { + // D10.2+ uses 'FileSizeLimit', earlier versions use 'file_validate_size'. if (isset($element['#upload_validators']['FileSizeLimit']['fileLimit'])) { $element['upload']['#attributes']['data-max-filesize'] = (int) $element['#upload_validators']['FileSizeLimit']['fileLimit']; } + elseif (isset($element['#upload_validators']['file_validate_size'][0])) { + $element['upload']['#attributes']['data-max-filesize'] = (int) $element['#upload_validators']['file_validate_size'][0]; + } return $element; }