diff --git a/class/comment.php b/class/comment.php index c00166d..6bfb915 100644 --- a/class/comment.php +++ b/class/comment.php @@ -257,6 +257,7 @@ function commentListShow($tpl_dir) $assign['im'] = $settings['comment_use_antispam']; $assign['comment_allowed_extensions'] = $settings['comment_allowed_extensions'] ?? 'jpg,jpeg,png,gif'; $assign['comment_max_file_size'] = $settings['comment_max_file_size'] ?? 2048; + $assign['comment_max_files'] = (int)($settings['comment_max_files'] ?? 5); $comments = array(); if ($settings['comment_use_page_nav'] == 1) @@ -482,6 +483,21 @@ function commentListShow($tpl_dir) $uploaded_files = []; if ($settings['comment_allow_files'] == 1 && isset($_FILES['comment_image'])) { + // --- ДОБАВЛЯЕМ ПРОВЕРКУ ЛИМИТА КОЛИЧЕСТВА --- + $max_files_limit = (int)($settings['comment_max_files'] ?? 5); + $total_incoming = 0; + if (is_array($_FILES['comment_image']['name'])) { + foreach($_FILES['comment_image']['name'] as $fname) if(!empty($fname)) $total_incoming++; + } elseif(!empty($_FILES['comment_image']['name'])) { + $total_incoming = 1; + } + + if ($total_incoming > $max_files_limit) { + if ($ajax) { echo 'error_max_files'; exit; } + else { header('Location:' . $link . '#end'); exit; } + } + // --- КОНЕЦ ПРОВЕРКИ --- + $upload_path = BASE_DIR . '/uploads/comments/'; if (!is_dir($upload_path)) { @mkdir($upload_path, 0775, true); @@ -612,6 +628,7 @@ function commentPostEdit($comment_id) if ($comment_id <= 0 || $user_group <= 0) exit('INVALID_ID'); // 2. Получаем данные комментария и настройки модуля (JOIN) + // ДОБАВЛЕНО: cmnt.comment_max_files в выборку $row = $AVE_DB->Query(" SELECT msg.*, @@ -620,7 +637,8 @@ function commentPostEdit($comment_id) cmnt.comment_user_groups, cmnt.comment_edit_time, cmnt.comment_allowed_extensions, - cmnt.comment_max_file_size + cmnt.comment_max_file_size, + cmnt.comment_max_files FROM " . PREFIX . "_module_comment_info AS msg JOIN " . PREFIX . "_module_comments AS cmnt ON cmnt.Id = 1 WHERE msg.Id = '" . $comment_id . "' @@ -685,10 +703,24 @@ function commentPostEdit($comment_id) } } - // --- Б. Загрузка новых файлов (если есть) --- - // Проверяем, что пришел массив файлов + // --- Б. Загрузка новых файлов (с проверкой лимита) --- if (isset($_FILES['comment_image']) && is_array($_FILES['comment_image']['name'])) { + // 1. Считаем, сколько реально новых файлов пытаются загрузить + $new_files_to_upload_count = 0; + foreach ($_FILES['comment_image']['name'] as $k => $fname) { + if (!empty($fname) && $_FILES['comment_image']['error'][$k] == UPLOAD_ERR_OK) { + $new_files_to_upload_count++; + } + } + + // 2. Проверка лимита: (Оставшиеся старые + Новые) не должно быть больше MAX + $max_limit = (int)($row['comment_max_files'] ?? 5); + if ((count($current_files) + $new_files_count) > $max_limit) { + echo "MAX_FILES_LIMIT_EXCEEDED"; // Это поймает JS + exit; + } + $allowed_ext_str = $row['comment_allowed_extensions'] ?? 'jpg,jpeg,png,gif,webp'; $allowed_extensions = array_map('trim', explode(',', strtolower($allowed_ext_str))); $max_kb = (int)($row['comment_max_file_size'] ?? 2048); diff --git a/js/comment.js b/js/comment.js index 5ad4d6e..e7f0e2e 100644 --- a/js/comment.js +++ b/js/comment.js @@ -530,26 +530,27 @@ if ($foundImgs.length > 0) { } // --- СБОРКА ФОРМЫ --- - var editHtml = ` -
- - ${starsEditBlock} - -

Загруженные файлы:

- ${existingImagesHtml} - -
- - -
- - -
- - - Осталось: -
-
`; +var editHtml = ` +
+ + ${starsEditBlock} + +

Загруженные файлы:

+ ${existingImagesHtml} + +
+ + + +
+ + +
+ + + Осталось: +
+
`; $textBlock.hide(); $attachedImagesBlock.hide(); @@ -562,51 +563,69 @@ if ($foundImgs.length > 0) { $wrapper.find('.edit-form-container').data('pendingFiles', pendingFiles); - $('#new_file_' + cid).on('change', function() { - var files = this.files; - var $previewContainer = $('#new_files_preview_' + cid); - - if ($previewContainer.length === 0) { - $(this).after(`
`); - $previewContainer = $('#new_files_preview_' + cid); - } - - // Мы НЕ удаляем старые превью ($previewContainer.empty() убрали), - // а просто добавляем новые в массив и в контейнер - if (files) { - $.each(files, function(i, file) { - if (!file.type.match('image.*')) return; +$('#new_file_' + cid).on('change', function() { + var files = this.files; + var $input = $(this); + var $currentEditForm = $input.closest('.edit-form-container'); + + // ИЩЕМ ТОЛЬКО ПО КЛАССУ ОТНОСИТЕЛЬНО ИНПУТА + var $errorDisplay = $input.prev('.js-file-error'); + + var maxLimit = parseInt(typeof MAX_FILES_COUNT !== 'undefined' ? MAX_FILES_COUNT : 5); + + if (files) { + // Очищаем при каждом новом выборе + $errorDisplay.text('').hide(); + + $.each(files, function(i, file) { + var alreadyInPost = $currentEditForm.find('[id^="existing_wrapper_"]').length; + var inPending = pendingFiles.filter(f => f !== null).length; + + if ((alreadyInPost + inPending) >= maxLimit) { + // ВЫВОДИМ ТЕКСТ + $errorDisplay.text('⚠️ Лимит: ' + maxLimit + ' шт. Лишние файлы проигнорированы.').show(); - // Добавляем файл в наш список - pendingFiles.push(file); - var currentIndex = pendingFiles.length - 1; + $input.val(''); + return false; + } - var reader = new FileReader(); - reader.onload = function(e) { - $previewContainer.append(` -
- - NEW - -
`); - }; - reader.readAsDataURL(file); - }); - } - // Очищаем инпут, чтобы браузер позволил выбрать те же файлы снова - $(this).val(''); - }); + if (!file.type.match('image.*')) return true; - // Обработчик удаления НОВОГО файла из превью (до сохранения) - $doc.off('click', '.remove-new-file').on('click', '.remove-new-file', function() { - var $item = $(this).closest('.new-file-item'); - var index = $item.data('index'); - pendingFiles[index] = null; // Помечаем как удаленный - $item.remove(); - }); + pendingFiles.push(file); + var currentIndex = pendingFiles.length - 1; + + var reader = new FileReader(); + reader.onload = function(e) { + var $previewContainer = $('#new_files_preview_' + cid); + if ($previewContainer.length === 0) { + $input.after(`
`); + $previewContainer = $('#new_files_preview_' + cid); + } + $previewContainer.append(` +
+ + +
`); + }; + reader.readAsDataURL(file); + }); + } +}); + +// Обработчик удаления НОВОГО файла из превью (до сохранения) +$doc.off('click', '.remove-new-file').on('click', '.remove-new-file', function() { + var $item = $(this).closest('.new-file-item'); + var index = $item.data('index'); + + pendingFiles[index] = null; // Помечаем как удаленный + $item.remove(); + + // ЗАМЕНЯЕМ СТАРУЮ СТРОКУ НА ЭТУ: + // Ищем через ближайший контейнер формы, чтобы точно попасть в нужный блок текста + $(this).closest('.edit-form-container').find('.js-file-error').hide().text(''); +}); // --- КОНЕЦ ВСТАВКИ --- // Добавил поддержку лимита символов @@ -622,10 +641,12 @@ if ($foundImgs.length > 0) { $doc.on('click', '.remove-existing-img', function() { var cid = $(this).data('id'); - var imgPath = $(this).data('img-path'); // Имя файла + var imgPath = $(this).data('img-path'); var targetId = $(this).data('target'); - // Добавляем имя файла в скрытый инпут для сервера + // 1. Очищаем текст ошибки, так как место освободилось + $('#file_error_' + cid).addClass('d-none').text(''); + var $deleteInput = $('#delete_files_' + cid); var currentDeleteList = $deleteInput.val() ? $deleteInput.val().split(',') : []; @@ -634,29 +655,34 @@ $doc.on('click', '.remove-existing-img', function() { $deleteInput.val(currentDeleteList.join(',')); } - // Скрываем блок с картинкой - $('#' + targetId).fadeOut(300); + // 2. Скрываем и УДАЛЯЕМ элемент, чтобы счетчик .length его больше не видел + $('#' + targetId).fadeOut(300, function() { + $(this).remove(); + }); }); // Обработка выбора НОВОГО файла при редактировании $doc.on('change', 'input[id^="new_file_"]', function() { var cid = $(this).attr('id').replace('new_file_', ''); var input = this; - var $errorBox = $('#file_error_' + cid); + + // 1. Сначала находим блок (БЕЗ ОШИБОК В ПЕРЕМЕННЫХ) + var $errorBox = $('#file_error_' + cid); var $previewWrapper = $('#edit_preview_wrapper_' + cid); - // Очищаем превью перед новым выбором - $previewWrapper.find('.preview-item-new').remove(); - $errorBox.addClass('d-none').text(''); - if (input.files && input.files.length > 0) { + // 2. НЕ ОЧИЩАЕМ ТЕКСТ СРАЗУ, даем checkFileConsistency шанс вывести ошибку + $previewWrapper.find('.preview-item-new').remove(); $previewWrapper.removeClass('d-none'); Array.from(input.files).forEach(function(file) { + // 3. Передаем именно найденный объект if (checkFileConsistency(file, $errorBox)) { + // Если файл прошел проверку - только тогда можно спрятать старую ошибку + $errorBox.addClass('d-none').text(''); + var reader = new FileReader(); reader.onload = function(e) { - // Создаем новое превью для каждого файла var item = `
@@ -666,10 +692,6 @@ $doc.on('change', 'input[id^="new_file_"]', function() { reader.readAsDataURL(file); } }); - - // Прячем старое фото и помечаем на удаление - $('#existing_preview_wrapper_' + cid).addClass('d-none'); - $('#del_img_' + cid).prop('checked', true); } }); @@ -750,6 +772,8 @@ $doc.on('click', '.saveButton', function() { }); }); + + // Глобальный массив для файлов НОВОГО комментария var newCommentPendingFiles = []; @@ -758,27 +782,40 @@ var newCommentPendingFiles = []; $doc.off('change', '#comment_image').on('change', '#comment_image', function() { var files = this.files; var $previewWrapper = $('#image_preview_wrapper'); - var $errorDisplay = $('#file_error'); // добавили переменную для ошибок + var $errorDisplay = $('#file_error'); + // 1. Лимит из настроек + var maxLimit = parseInt(typeof MAX_FILES_COUNT !== 'undefined' ? MAX_FILES_COUNT : 5); + if (files) { + $errorDisplay.addClass('d-none').text(''); // Сбрасываем старые ошибки + $.each(files, function(i, file) { - // --- ТВОЯ ВАЛИДАЦИЯ (Screenshot 7 и 8) --- + // 2. Считаем, сколько сейчас РЕАЛЬНО файлов в очереди (не null) + var currentInQueue = newCommentPendingFiles.filter(function(f) { return f !== null; }).length; + + // 3. ПРОВЕРКА ЛИМИТА: если уже достигли максимума + if (currentInQueue >= maxLimit) { + $errorDisplay.removeClass('d-none').text('Достигнут лимит (' + maxLimit + ' шт.). Лишние файлы проигнорированы.'); + return false; // break - полностью выходим из цикла $.each + } + + // 4. Твоя существующая валидация (размер, расширение) if (typeof checkFileConsistency === 'function') { if (!checkFileConsistency(file, $errorDisplay)) { - return true; // если файл плохой - пропускаем его + return true; // continue - этот файл плохой, идем к следующему } } - // ------------------------------------------ + // Если прошли все проверки — добавляем в очередь newCommentPendingFiles.push(file); var currentIndex = newCommentPendingFiles.length - 1; var reader = new FileReader(); reader.onload = function(e) { - // ПРОВЕРКА: если превью для этого файла уже есть - не добавляем второе if ($previewWrapper.find(`[data-index="${currentIndex}"]`).length > 0) return; - $previewWrapper.removeClass('d-none'); // показываем только когда есть что показать + $previewWrapper.removeClass('d-none'); $previewWrapper.append(`
@@ -791,7 +828,8 @@ $doc.off('change', '#comment_image').on('change', '#comment_image', function() { reader.readAsDataURL(file); }); } - // Очищаем инпут + + // Очищаем инпут в любом случае, чтобы можно было выбрать те же файлы снова $(this).val(''); }); @@ -800,6 +838,7 @@ $doc.on('click', '.remove-new-comment-img', function() { var $item = $(this).closest('.new-comment-file-item'); var index = $item.data('index'); newCommentPendingFiles[index] = null; // Помечаем удаленным + $('#file_error').addClass('d-none').text(''); $item.fadeOut(200, function() { $(this).remove(); if ($('#image_preview_wrapper').children().length === 0) { diff --git a/sql.php b/sql.php index 5a4106d..56dc0f8 100644 --- a/sql.php +++ b/sql.php @@ -5,7 +5,7 @@ * * Обновленная структура с поддержкой выбора типа рейтинга, * идентификации анонимов, загрузки файлов и защиты по IP. - * ДОБАВЛЕНО: Управление расширениями и максимальным размером файлов. + * ДОБАВЛЕНО: Управление расширениями, размером и КОЛИЧЕСТВОМ файлов. * СИНХРОНИЗАЦИЯ: Добавлен формат webp по умолчанию. */ @@ -39,6 +39,7 @@ $module_sql_install[] = "CREATE TABLE `%%PRFX%%_module_comments` ( `comment_allow_files` tinyint(1) NOT NULL default '0', `comment_allowed_extensions` varchar(255) NOT NULL default 'jpg,jpeg,png,gif,webp', `comment_max_file_size` int(10) NOT NULL default '2048', + `comment_max_files` tinyint(2) NOT NULL default '5', `comment_rating_type` tinyint(1) NOT NULL default '0', `comment_show_user_rating` tinyint(1) NOT NULL default '0', `comment_show_user_rating_replies` tinyint(1) NOT NULL default '0', @@ -93,8 +94,8 @@ $module_sql_install[] = "CREATE TABLE `%%PRFX%%_module_comment_votes` ( KEY `remote_addr` (`remote_addr`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;"; -/* Настройки по умолчанию */ -$module_sql_install[] = "INSERT INTO `%%PRFX%%_module_comments` VALUES (1, 1000, '1,3', '1,2,3,4', '0', '1', '1' , '0', '', 1, 0, '', 1, 0, '', 0, 'jpg,jpeg,png,gif,webp', 2048, 0, 1, 0, 0, 0, 0, 60, 30);"; +/* Настройки по умолчанию (добавлено значение 5 для comment_max_files) */ +$module_sql_install[] = "INSERT INTO `%%PRFX%%_module_comments` VALUES (1, 1000, '1,3', '1,2,3,4', '0', '1', '1' , '0', '', 1, 0, '', 1, 0, '', 0, 'jpg,jpeg,png,gif,webp', 2048, 5, 0, 1, 0, 0, 0, 0, 60, 30);"; // ================================================================================= // 2. ОБНОВЛЕНИЕ МОДУЛЯ (ALTER TABLE) @@ -112,5 +113,6 @@ $module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD COLUMN IF NOT /* Новые поля для файлов при обновлении */ $module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD COLUMN IF NOT EXISTS `comment_allowed_extensions` VARCHAR(255) NOT NULL DEFAULT 'jpg,jpeg,png,gif,webp';"; $module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD COLUMN IF NOT EXISTS `comment_max_file_size` INT(10) NOT NULL DEFAULT '2048';"; +$module_sql_update[] = "ALTER TABLE `%%PRFX%%_module_comments` ADD COLUMN IF NOT EXISTS `comment_max_files` TINYINT(2) NOT NULL DEFAULT '5';"; ?> \ No newline at end of file diff --git a/templates/admin_settings.tpl b/templates/admin_settings.tpl index 41e5f4c..8c45147 100644 --- a/templates/admin_settings.tpl +++ b/templates/admin_settings.tpl @@ -202,7 +202,7 @@ Допустимые расширения: - + Макс. размер файла (Кб): @@ -210,6 +210,15 @@ KB + + Макс. кол-во файлов: + + + шт. на один комментарий + + + + diff --git a/templates/comments_tree.tpl b/templates/comments_tree.tpl index 3ccc6c4..ccdabb4 100644 --- a/templates/comments_tree.tpl +++ b/templates/comments_tree.tpl @@ -151,12 +151,12 @@
Разрешены: {$comment_allowed_extensions|default:'jpg, png, gif'}. Макс. размер: {$comment_max_file_size|default:'2048'} Кб. + {* ВНЕДРЯЕМ ЛИМИТ КОЛИЧЕСТВА *} + Макс. количество: {$comment_max_files|default:'5'} шт.
- {* Ключевое изменение: name="comment_image[]" и атрибут multiple *} - {* Контейнер для нескольких превью *}
{* Сюда JS будет добавлять превью *}
@@ -271,6 +271,7 @@ // --- ПЕРЕМЕННЫЕ ДЛЯ ВАЛИДАЦИИ ФАЙЛОВ --- var ALLOWED_EXTENSIONS = '{$comment_allowed_extensions|default:"jpg,jpeg,png,gif"}'; var MAX_FILE_SIZE_KB = '{$comment_max_file_size|default:2048}'; + var MAX_FILES_COUNT = '{$comment_max_files|default:5}';