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);