diff --git a/assets/css/frontend/cms-download.css b/assets/css/frontend/cms-download.css index e22ca51a..8d1fa39d 100644 --- a/assets/css/frontend/cms-download.css +++ b/assets/css/frontend/cms-download.css @@ -203,6 +203,7 @@ .aksrt-dl-modal.is-cashier .aksrt-dl-modal__body { position: relative; min-height: 120px; + padding-top: 120px; } .aksrt-dl-modal .modal-colorful-header, @@ -276,7 +277,7 @@ .aksrt-dl-modal .modal-pay-body { padding: 15px; - margin-top: 120px; + margin-top: 0; } .aksrt-cashier__loading { diff --git a/src/Core/FrontSubmit.php b/src/Core/FrontSubmit.php index 55b46c06..1f5747ea 100644 --- a/src/Core/FrontSubmit.php +++ b/src/Core/FrontSubmit.php @@ -81,30 +81,57 @@ class FrontSubmit { return $this->user_can_cap($cap, $user_id); } + public static function upload_limit_bytes(string $kind): int { + $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; + $configured_mb = match ($kind) { + 'image' => max(1, min(20, (int) ($settings['submit_image_max_size'] ?? 5))), + 'video' => max(1, min(200, (int) ($settings['submit_video_max_size'] ?? 30))), + 'resource' => max(1, min(200, (int) ($settings['resource_max_size'] ?? 50))), + default => 0, + }; + if ($configured_mb === 0) { + return 0; + } + + return min($configured_mb * MB_IN_BYTES, (int) wp_max_upload_size()); + } + public function rest_upload_attachment(\WP_REST_Request $request) { $kind = (string) $request->get_param('kind'); $user_id = get_current_user_id(); $files = $request->get_file_params(); $file = $files['file'] ?? null; - if (!is_array($file) || empty($file['tmp_name']) || !empty($file['error'])) { + if (!is_array($file)) { return new \WP_Error('aksrt_upload_missing_file', __('请选择要上传的文件。', 'aksrt'), ['status' => 400]); } - $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; - $max_mb = match ($kind) { - 'resource' => max(1, min(200, (int) ($settings['resource_max_size'] ?? 50))), - 'video' => max(1, min(200, (int) ($settings['submit_video_max_size'] ?? 30))), - default => max(1, min(20, (int) ($settings['submit_image_max_size'] ?? 5))), - }; - $max_size = $max_mb * MB_IN_BYTES; + $max_size = self::upload_limit_bytes($kind); + $upload_error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE); + if ($upload_error === UPLOAD_ERR_INI_SIZE || $upload_error === UPLOAD_ERR_FORM_SIZE) { + return new \WP_Error( + 'aksrt_upload_too_large', + sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), + ['status' => 413] + ); + } + if ($upload_error === UPLOAD_ERR_PARTIAL) { + return new \WP_Error('aksrt_upload_partial', __('文件未完整上传,请重新选择。', 'aksrt'), ['status' => 400]); + } + if ($upload_error === UPLOAD_ERR_NO_FILE || empty($file['tmp_name'])) { + return new \WP_Error('aksrt_upload_missing_file', __('请选择要上传的文件。', 'aksrt'), ['status' => 400]); + } + if ($upload_error !== UPLOAD_ERR_OK) { + return new \WP_Error('aksrt_upload_failed', __('服务器无法保存上传文件。', 'aksrt'), ['status' => 500]); + } + $size = (int) ($file['size'] ?? 0); if ($size <= 0 || $size > $max_size) { return new \WP_Error( 'aksrt_upload_too_large', - sprintf(__('文件大小不能超过 %dMB。', 'aksrt'), $max_mb), - ['status' => 400] + sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), + ['status' => 413] ); } diff --git a/templates/pages/page-submit.php b/templates/pages/page-submit.php index 51496750..8841778f 100644 --- a/templates/pages/page-submit.php +++ b/templates/pages/page-submit.php @@ -79,10 +79,9 @@ if (file_exists($js)) { true ); } - $attachment_settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : []; - $submit_image_max_size = max(1, min(20, (int) ($attachment_settings['submit_image_max_size'] ?? 5))) * MB_IN_BYTES; - $submit_video_max_size = max(1, min(200, (int) ($attachment_settings['submit_video_max_size'] ?? 30))) * MB_IN_BYTES; - $resource_max_size = max(1, min(200, (int) ($attachment_settings['resource_max_size'] ?? 50))) * MB_IN_BYTES; + $submit_image_max_size = \AKSRT\Core\FrontSubmit::upload_limit_bytes('image'); + $submit_video_max_size = \AKSRT\Core\FrontSubmit::upload_limit_bytes('video'); + $resource_max_size = \AKSRT\Core\FrontSubmit::upload_limit_bytes('resource'); wp_localize_script('aksrt-front-submit', 'aksrtSubmitData', [ 'ajaxUrl' => admin_url('admin-ajax.php'), diff --git a/tests/widget-system/front-submit-editor-contract.test.cjs b/tests/widget-system/front-submit-editor-contract.test.cjs index e1cc488c..ad4ba31d 100644 --- a/tests/widget-system/front-submit-editor-contract.test.cjs +++ b/tests/widget-system/front-submit-editor-contract.test.cjs @@ -16,6 +16,22 @@ test('front submit enqueues the WordPress editor before wp_head is rendered', () assert.ok(enqueueAt < headerAt, 'editor assets must be queued before get_header()/wp_head'); }); +test('front submit uploads use the effective WordPress server limit', () => { + const controller = read('src/Core/FrontSubmit.php'); + const page = read('templates/pages/page-submit.php'); + + assert.match(controller, /function upload_limit_bytes\(string \$kind\): int/); + assert.match(controller, /min\(\$configured_mb \* MB_IN_BYTES, \(int\) wp_max_upload_size\(\)\)/); + assert.match(controller, /UPLOAD_ERR_INI_SIZE/); + assert.match(controller, /UPLOAD_ERR_FORM_SIZE/); + assert.match(controller, /UPLOAD_ERR_PARTIAL/); + assert.match(controller, /\['status' => 413\]/); + assert.match(page, /FrontSubmit::upload_limit_bytes\('image'\)/); + assert.match(page, /FrontSubmit::upload_limit_bytes\('video'\)/); + assert.match(page, /FrontSubmit::upload_limit_bytes\('resource'\)/); + assert.doesNotMatch(page, /\$attachment_settings\s*=/); +}); + test('published post content reaches the front submit editor unchanged', () => { const context = read('templates/parts/submit/context.php'); const editor = read('templates/parts/submit/editor-main.php'); diff --git a/tests/widget-system/pay-card-contract.test.cjs b/tests/widget-system/pay-card-contract.test.cjs index c26271ec..3870ec06 100644 --- a/tests/widget-system/pay-card-contract.test.cjs +++ b/tests/widget-system/pay-card-contract.test.cjs @@ -100,6 +100,9 @@ test('the article cashier uses the reference modal contract and one direct asset assert.match(css, /\.aksrt-dl-modal \.modal-colorful-header/); assert.match(css, /\.aksrt-dl-modal \.payment-method-radio/); assert.match(css, /\.aksrt-dl-modal \.but\.initiate-pay/); + assert.match(css, /\.aksrt-dl-modal\.is-cashier \.aksrt-dl-modal__body\s*\{[^}]*padding-top:\s*120px;/s); + assert.match(css, /\.aksrt-dl-modal \.modal-pay-body\s*\{[^}]*margin-top:\s*0;/s); + assert.doesNotMatch(css, /\.aksrt-dl-modal \.modal-pay-body\s*\{[^}]*margin-top:\s*120px;/s); assert.match(assets, /assets\/css\/frontend\/cms-download\.css/); assert.match(assets, /filemtime\(\$cms_download_css_path\)/);