feat(attachment): 按参考主题重构后台上传配置套件
- 新增 AKSRT\Attachment\UploadLimits:按用户类型解析图片/视频/文件 上传上限(取最大值+默认兜底,钳制到 PHP 上限)、允许/禁止格式过滤、 上传自动重命名、非管理员附件页 404;不含论坛角色 - 后台「附件配置」重做:大小限制/格式限制/上传行为三组配置, PHP Schema 与 SPA Schema 逐字段镜像(v1.2.0) - 定制页 AttachmentPermissions.vue 改为标准 schemaPage,清理失效的 API 包装、组件加载器与旧配置键(submit_image_max_size 等) - 投稿/评论上传、批量上传数量全部切换到新链路;AksField 数字组件 支持单位展示 - 更新 captcha harness 与 front-submit 契约测试;421 项测试通过
This commit is contained in:
@@ -3,14 +3,6 @@ export const permissionsApiMethods = {
|
||||
* 功能&权限 (Permissions)
|
||||
* ================================================================ */
|
||||
|
||||
getPermissionsAttachmentSettings() { return this.getSchemaPage('permissions', 'attachment') },
|
||||
savePermissionsAttachmentSettings(data) { return this.saveSchemaPage('permissions', 'attachment', data) },
|
||||
|
||||
getPermissionsLoginSettings() { return this.getSchemaPage('permissions', 'login') },
|
||||
savePermissionsLoginSettings(data) { return this.saveSchemaPage('permissions', 'login', data) },
|
||||
|
||||
getPermissionsModerationSettings() { return this.getSchemaPage('permissions', 'moderation') },
|
||||
|
||||
getCapabilities() { return this.request('/permissions/capabilities') },
|
||||
saveCapabilities(user_caps) { return this.request('/permissions/capabilities', { method: 'POST', body: JSON.stringify({ user_caps }) }) },
|
||||
}
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
/>
|
||||
|
||||
<!-- Number -->
|
||||
<div v-else-if="schema.type === 'number' || schema.type === 'spinner'" class="aksrt-field__number">
|
||||
<n-input-number
|
||||
v-else-if="schema.type === 'number' || schema.type === 'spinner'"
|
||||
:value="modelValue"
|
||||
:min="schema.min"
|
||||
:max="schema.max"
|
||||
@@ -71,6 +71,8 @@
|
||||
style="width: 100%"
|
||||
@update:value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
<span v-if="schema.unit" class="aksrt-field__unit">{{ schema.unit }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Select -->
|
||||
<div v-else-if="schema.type === 'select'" class="aksrt-field__ordered-select">
|
||||
@@ -1210,6 +1212,19 @@ function confirmRepeaterModal() {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.aksrt-field__number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.aksrt-field__unit {
|
||||
color: var(--aksrt-color-muted);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-field__between-sep {
|
||||
color: var(--aksrt-color-border-strong);
|
||||
font-size: 14px;
|
||||
|
||||
@@ -37,14 +37,13 @@ export const permissionsSection = {
|
||||
canonicalDomain: 'permissions',
|
||||
section: 'moderation',
|
||||
}),
|
||||
customPage({
|
||||
schemaPage({
|
||||
key: 'permissions-attachment',
|
||||
name: 'PermissionsAttachment',
|
||||
label: '附件配置',
|
||||
route: '/permissions/attachment',
|
||||
component: 'AttachmentPermissions',
|
||||
title: '附件配置',
|
||||
desc: '管理文件大小、类型、安全限制和审核策略。',
|
||||
desc: '头像图片、上传大小、上传格式与上传行为配置。',
|
||||
canonicalDomain: 'permissions',
|
||||
section: 'attachment',
|
||||
}),
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader
|
||||
title="附件治理"
|
||||
desc="管理各类上传文件的大小、安全限制和审核策略;准入权限请在“基本权限”中配置。"
|
||||
>
|
||||
<template #actions>
|
||||
<n-space>
|
||||
<n-button quaternary @click="goModeration">审核策略</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-spin :show="loading" style="width:100%; min-height:200px;">
|
||||
<!-- Summary -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>上传策略总览</h3>
|
||||
</div>
|
||||
<n-grid :cols="3" :x-gap="12" :y-gap="12">
|
||||
<n-grid-item v-for="item in summaryItems" :key="item.key">
|
||||
<div class="aksrt-attach__summary">
|
||||
<FaIcon :icon="item.icon" :size="16" :color="item.enabled ? '#52c41a' : 'var(--aksrt-color-muted)'" />
|
||||
<div>
|
||||
<strong>{{ item.label }}</strong>
|
||||
<p>{{ item.enabled ? '已启用' : '已关闭' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<!-- Avatar & Cover -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>用户资料卡背景图</h3>
|
||||
</div>
|
||||
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
|
||||
<n-form-item label="头像/用户资料卡背景图大小上限 (MB)">
|
||||
<n-input-number v-model:value="form.avatar_max_size" :min="1" :max="10" style="max-width:160px;" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- Content uploads -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>内容上传</h3>
|
||||
</div>
|
||||
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
|
||||
<n-form-item label="投稿图片大小上限 (MB)">
|
||||
<n-input-number v-model:value="form.submit_image_max_size" :min="1" :max="20" style="max-width:160px;" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="评论图片大小上限 (MB)">
|
||||
<n-input-number v-model:value="form.comment_image_max_size" :min="1" :max="10" style="max-width:160px;" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- Resource attachments -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>资源附件</h3>
|
||||
</div>
|
||||
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
|
||||
<n-form-item label="资源附件大小上限 (MB)">
|
||||
<n-input-number v-model:value="form.resource_max_size" :min="1" :max="200" style="max-width:160px;" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<div class="aksrt-form-actions">
|
||||
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
|
||||
</div>
|
||||
</n-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import FaIcon from '@/components/form/FaIcon.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
avatar_max_size: 2,
|
||||
submit_image_max_size: 5,
|
||||
comment_image_max_size: 2,
|
||||
resource_max_size: 50,
|
||||
})
|
||||
|
||||
const summaryItems = computed(() => [
|
||||
{ key: 'profile', icon: 'fa fas fa-id-card', label: '资料卡图片', enabled: true },
|
||||
{ key: 'submit', icon: 'fa fas fa-edit', label: '投稿图片', enabled: true },
|
||||
{ key: 'comment', icon: 'fa fas fa-comment-dots', label: '评论图片', enabled: true },
|
||||
{ key: 'resource', icon: 'fa fas fa-file-archive', label: '资源附件', enabled: true },
|
||||
])
|
||||
|
||||
function go(path) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function goModeration() { router.push('/permissions/moderation') }
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const permissionRes = await api.getPermissionsAttachmentSettings()
|
||||
|
||||
const permission = permissionRes?.data ?? permissionRes ?? {}
|
||||
|
||||
// Map permission fields
|
||||
form.avatar_max_size = Number(permission.avatar_max_size || 2)
|
||||
form.submit_image_max_size = Number(permission.submit_image_max_size || 5)
|
||||
form.comment_image_max_size = Number(permission.comment_image_max_size || 2)
|
||||
form.resource_max_size = Number(permission.resource_max_size || 50)
|
||||
} catch (error) {
|
||||
message.error(error.message || '加载附件权限失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
await api.savePermissionsAttachmentSettings({
|
||||
avatar_max_size: form.avatar_max_size,
|
||||
submit_image_max_size: form.submit_image_max_size,
|
||||
comment_image_max_size: form.comment_image_max_size,
|
||||
resource_max_size: form.resource_max_size,
|
||||
})
|
||||
message.success('附件治理配置已保存')
|
||||
} catch (error) {
|
||||
message.error(error.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-attach__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.aksrt-attach__summary strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--aksrt-color-text);
|
||||
}
|
||||
|
||||
.aksrt-attach__summary p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--aksrt-color-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,6 @@ export const corePageLoaders = {
|
||||
PostPageSettings: () => import('@/pages/post-list/PostPageSettings.vue'),
|
||||
CommentSettings: () => import('@/pages/post-list/CommentSettings.vue'),
|
||||
PermissionsCapabilities: () => import('@/pages/permissions/PermissionsCapabilities.vue'),
|
||||
AttachmentPermissions: () => import('@/pages/permissions/AttachmentPermissions.vue'),
|
||||
CmsCredits: () => import('@/pages/CmsCredits.vue'),
|
||||
ThemeInfoPage: () => import('@/pages/theme/ThemeInfoPage.vue'),
|
||||
ThemeAuthorizationPage: () => import('@/pages/theme/ThemeAuthorizationPage.vue'),
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
* Section keys must stay aligned with the router and PHP schema provider.
|
||||
*/
|
||||
|
||||
/**
|
||||
* User type options shared by the upload limit repeaters.
|
||||
* Mirrors AKSRT\Attachment\UploadLimits::user_type_options() (levels 2-10, VIP 1-2).
|
||||
*/
|
||||
const uploadUserTypeOptions = [
|
||||
{ label: '最小默认值(所有人)', value: 'default' },
|
||||
{ label: '已登录用户', value: 'logged' },
|
||||
...[2, 3, 4, 5, 6, 7, 8, 9, 10].map((level) => ({ label: `等级达到 ${level} 级`, value: `level_${level}` })),
|
||||
{ label: 'VIP1 会员', value: 'vip_1' },
|
||||
{ label: 'VIP2 会员', value: 'vip_2' },
|
||||
{ label: '已认证用户', value: 'auth' },
|
||||
{ label: '超级管理员', value: 'admin' },
|
||||
]
|
||||
|
||||
const uploadMimeUserTypeOptions = [
|
||||
{ label: '所有用户', value: 'all' },
|
||||
...uploadUserTypeOptions,
|
||||
]
|
||||
|
||||
const uploadSizeFields = [
|
||||
{ key: 'type', label: '用户类型', type: 'select', default: 'logged', options: uploadUserTypeOptions },
|
||||
{ key: 'val', label: '可上传', type: 'number', default: 30, min: 1, max: 2048 },
|
||||
]
|
||||
|
||||
export const permissionsSchemas = {
|
||||
/**
|
||||
* Login / captcha / access guard settings
|
||||
@@ -188,43 +212,136 @@ export const permissionsSchemas = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Attachment permissions
|
||||
* Attachment / upload governance
|
||||
*/
|
||||
attachment: {
|
||||
key: 'permissions.attachment',
|
||||
title: '附件权限',
|
||||
title: '附件配置',
|
||||
icon: 'fa fas fa-paperclip',
|
||||
description: '控制文件上传和附件下载权限。',
|
||||
description: '统一管理头像、上传大小、上传格式与上传行为;准入权限请在“基本权限”中配置。',
|
||||
defaults: {
|
||||
avatar_max_size: 2,
|
||||
submit_image_max_size: 5,
|
||||
comment_image_max_size: 2,
|
||||
resource_max_size: 50,
|
||||
upload_img_size: [
|
||||
{ type: 'default', val: 5 },
|
||||
{ type: 'admin', val: 20 },
|
||||
],
|
||||
image_upload_multiple: 6,
|
||||
upload_video_size: [
|
||||
{ type: 'default', val: 30 },
|
||||
{ type: 'admin', val: 2048 },
|
||||
],
|
||||
upload_file_size: [
|
||||
{ type: 'default', val: 30 },
|
||||
{ type: 'admin', val: 2048 },
|
||||
],
|
||||
upload_file_mimes: [
|
||||
{ type: 'admin', pattern: 'allow', val: 'svg=image/svg+xml' },
|
||||
{ type: 'all', pattern: 'allow', val: '' },
|
||||
],
|
||||
newfilename: false,
|
||||
newfilename_type: 'time',
|
||||
close_attachment_page: true,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'profile',
|
||||
title: '用户资料卡背景图',
|
||||
icon: 'fa fas fa-user-circle',
|
||||
title: '用户头像与资料卡图片',
|
||||
icon: 'fa fas fa-id-card',
|
||||
fields: [
|
||||
{ key: 'avatar_max_size', label: '头像/用户资料卡背景图大小上限 (MB)', type: 'number', min: 1, max: 10 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'content',
|
||||
title: '内容上传',
|
||||
icon: 'fa fas fa-image',
|
||||
key: 'size-limits',
|
||||
title: '上传大小限制',
|
||||
icon: 'fa fas fa-weight-hanging',
|
||||
description: '设置上传大小,请考虑服务器负荷、硬盘,以及服务器最大能支持的范围。实际上传大小取「主题限制」与「PHP 环境限制」的较小值,超过 PHP 环境限制时请调整 php.ini 的 upload_max_filesize 与 post_max_size。',
|
||||
fields: [
|
||||
{ key: 'submit_image_max_size', label: '投稿图片大小上限 (MB)', type: 'number', min: 1, max: 20 },
|
||||
{ key: 'comment_image_max_size', label: '评论图片大小上限 (MB)', type: 'number', min: 1, max: 10 },
|
||||
{
|
||||
key: 'upload_img_size',
|
||||
label: '前端图片上传限制',
|
||||
type: 'repeater',
|
||||
min: 1,
|
||||
addText: '添加细分用户权限',
|
||||
help: '投稿、评论等前端场景允许上传的最大图片大小(单位 MB)。同一个用户满足多个条件则取最大值,「最小默认值」为所有人兜底。',
|
||||
fields: uploadSizeFields.map((field) => ({ ...field, default: field.key === 'val' ? 5 : field.default })),
|
||||
},
|
||||
{ key: 'image_upload_multiple', label: '图片批量上传最大数量', type: 'number', min: 0, max: 100, unit: '张', help: '前端投稿、评论上传图片允许的单次批量上传数量(为 0 则不限制,为 1 则不能批量上传)。' },
|
||||
{
|
||||
key: 'upload_video_size',
|
||||
label: '前端视频上传限制',
|
||||
type: 'repeater',
|
||||
min: 1,
|
||||
addText: '添加细分用户权限',
|
||||
help: '前端允许上传的最大视频大小(单位 MB)。同一个用户满足多个条件则取最大值,必须包含最低默认值。',
|
||||
fields: uploadSizeFields,
|
||||
},
|
||||
{
|
||||
key: 'upload_file_size',
|
||||
label: '前端文件上传大小限制',
|
||||
type: 'repeater',
|
||||
min: 1,
|
||||
addText: '添加细分用户权限',
|
||||
help: '前端允许上传的其他文件(不含视频和图片,建议大于视频图片)最大大小(单位 MB)。同一个用户满足多个条件则取最大值,必须包含最低默认值。',
|
||||
fields: uploadSizeFields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'resource',
|
||||
title: '资源附件',
|
||||
icon: 'fa fas fa-file-archive',
|
||||
key: 'mime-limits',
|
||||
title: '上传格式限制',
|
||||
icon: 'fa fas fa-file-code',
|
||||
fields: [
|
||||
{ key: 'resource_max_size', label: '资源附件大小上限 (MB)', type: 'number', min: 1, max: 200 },
|
||||
{
|
||||
key: 'mime_limit_notice',
|
||||
label: '安全提示',
|
||||
type: 'notice',
|
||||
alertType: 'warning',
|
||||
alertContent: '请添加文件的扩展名和 MIME 类型,用英文逗号分割。格式示例:mp3=audio/mpeg,jpg=image/jpeg。禁止的优先级大于允许。郑重提示:请勿允许上传 php、sh、exe 等可执行文件,会极大增加服务器被入侵攻击的风险。',
|
||||
},
|
||||
{
|
||||
key: 'upload_file_mimes',
|
||||
label: '前端上传文件格式限制',
|
||||
type: 'repeater',
|
||||
min: 1,
|
||||
addText: '添加细分用户权限',
|
||||
help: '根据不同的用户类型,添加额外允许或禁止上传的文件格式(禁止的优先级大于允许)。',
|
||||
fields: [
|
||||
{ key: 'type', label: '用户类型', type: 'select', default: 'all', options: uploadMimeUserTypeOptions },
|
||||
{
|
||||
key: 'pattern',
|
||||
label: '规则',
|
||||
type: 'select',
|
||||
default: 'allow',
|
||||
options: [
|
||||
{ label: '允许', value: 'allow' },
|
||||
{ label: '禁止', value: 'prohibit' },
|
||||
],
|
||||
},
|
||||
{ key: 'val', label: '格式', type: 'textarea', default: '', rows: 1, placeholder: '如:svg=image/svg+xml,mp3=audio/mpeg' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'behavior',
|
||||
title: '上传行为',
|
||||
icon: 'fa fas fa-cogs',
|
||||
fields: [
|
||||
{ key: 'newfilename', label: '上传文件自动重命名', type: 'switch', help: '开启后上传文件按规则重命名,减少重名文件查询,提高上传效率。' },
|
||||
{
|
||||
key: 'newfilename_type',
|
||||
label: '重命名格式',
|
||||
type: 'radio',
|
||||
buttonStyle: false,
|
||||
default: 'time',
|
||||
dependsOn: { key: 'newfilename', value: true },
|
||||
options: [
|
||||
{ label: '时间 + 原文件名', value: 'time' },
|
||||
{ label: '随机字符串', value: 'random' },
|
||||
],
|
||||
},
|
||||
{ key: 'close_attachment_page', label: '非管理员屏蔽附件页面', type: 'switch', help: '开启后非管理员访问附件(图片、文件等)页面将直接显示 404,可有效提高文件的安全性。' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -144,6 +144,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
function imageBatchLimit() {
|
||||
// 0 = 不限制(沿用默认 100 的选取上限),1 = 不允许批量上传。
|
||||
var configured = Number(cfg.imageUploadMultiple);
|
||||
if (!isFinite(configured) || configured <= 0) {
|
||||
return { multiple: true, maxItems: 100 };
|
||||
}
|
||||
if (configured === 1) {
|
||||
return { multiple: false, maxItems: 1 };
|
||||
}
|
||||
return { multiple: true, maxItems: Math.min(configured, 100) };
|
||||
}
|
||||
|
||||
function mediaDefinition(type) {
|
||||
if (type === 'video') return {
|
||||
title: i18n.insertVideo || '插入视频', mineTab: i18n.mineVideo || '我的视频', uploadTab: i18n.uploadVideo || '上传视频',
|
||||
@@ -156,9 +168,10 @@
|
||||
accept: '*/*', multiple: false, maxItems: 1, canUpload: !!editorCfg.canUploadFile && !!((cfg.uploads || {}).resource), canEmbed: false,
|
||||
select: i18n.selectFile || '选择或拖入附件', hint: i18n.fileUploadHint || '上传完成后自动插入正文', externalHint: i18n.externalFileHint || '请填写附件地址:',
|
||||
};
|
||||
var batch = imageBatchLimit();
|
||||
return {
|
||||
title: i18n.insertImage || '插入图片', mineTab: i18n.mineImage || '我的图片', uploadTab: i18n.uploadImage || '上传图片', externalTab: i18n.externalImage || '外链图片',
|
||||
accept: 'image/gif,image/jpeg,image/png,image/webp', multiple: true, maxItems: 100, canUpload: !!editorCfg.canUploadImage && !!((cfg.uploads || {}).image), canEmbed: false,
|
||||
accept: 'image/gif,image/jpeg,image/png,image/webp', multiple: batch.multiple, maxItems: batch.maxItems, canUpload: !!editorCfg.canUploadImage && !!((cfg.uploads || {}).image), canEmbed: false,
|
||||
select: i18n.selectImages || '选择或拖入图片', hint: i18n.imageUploadHint || '可批量选择,上传完成后自动插入正文', externalHint: i18n.externalImageHint || '每行填写一个图片地址:',
|
||||
};
|
||||
}
|
||||
@@ -575,13 +588,14 @@
|
||||
bindCharacterCount(editor);
|
||||
|
||||
if (editorCfg.canUploadImage) {
|
||||
var imageBatch = imageBatchLimit();
|
||||
editor.addButton('aksrt_image', {
|
||||
text: '',
|
||||
icon: 'image',
|
||||
tooltip: i18n.insertImage || '插入图片',
|
||||
onclick: function () {
|
||||
window.AksrtSubmitMedia.open({
|
||||
type: 'image', multiple: true, maxItems: 100,
|
||||
type: 'image', multiple: imageBatch.multiple, maxItems: imageBatch.maxItems,
|
||||
onSelect: function (items) {
|
||||
editor.insertContent(items.map(function (item) { return mediaMarkup('image', item); }).join('') + '<p></p>');
|
||||
editor.nodeChanged();
|
||||
|
||||
@@ -301,6 +301,9 @@ if (class_exists('\AKSRT\Core\CategoryRewrite')) {
|
||||
if (class_exists('\AKSRT\Core\FrontSubmit')) {
|
||||
\AKSRT\Core\FrontSubmit::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Attachment\UploadLimits')) {
|
||||
\AKSRT\Attachment\UploadLimits::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Payment\Order')) {
|
||||
\AKSRT\Payment\Order::get_instance()->init();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,34 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaPermissions {
|
||||
|
||||
/**
|
||||
* User type options for upload size limit rows.
|
||||
*
|
||||
* @return array<int, array{label:string, value:string}>
|
||||
*/
|
||||
private static function upload_user_type_options(): array {
|
||||
$options = [];
|
||||
foreach (\AKSRT\Attachment\UploadLimits::user_type_options() as $value => $label) {
|
||||
$options[] = ['label' => $label, 'value' => $value];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* User type options for mime limit rows (adds the "all users" type).
|
||||
*
|
||||
* @return array<int, array{label:string, value:string}>
|
||||
*/
|
||||
private static function upload_mime_user_type_options(): array {
|
||||
$options = [
|
||||
['label' => __('所有用户', 'aksrt'), 'value' => 'all'],
|
||||
];
|
||||
foreach (\AKSRT\Attachment\UploadLimits::user_type_options() as $value => $label) {
|
||||
$options[] = ['label' => $label, 'value' => $value];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
public static function build(): array {
|
||||
$provider_meta = function_exists('aksrt_captcha_provider_metadata') ? aksrt_captcha_provider_metadata() : [];
|
||||
$provider_options = [['label' => '关闭', 'value' => '']];
|
||||
@@ -60,19 +88,58 @@ class SchemaPermissions {
|
||||
'attachment' => [
|
||||
'key' => 'attachment',
|
||||
'title' => '附件配置',
|
||||
'version' => '1.1.0',
|
||||
'version' => '1.2.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'profile', 'title' => '用户资料卡背景图', 'fields' => [
|
||||
['key' => 'profile', 'title' => '用户头像与资料卡图片', 'fields' => [
|
||||
['key' => 'avatar_max_size', 'label' => '头像/用户资料卡背景图大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
|
||||
]],
|
||||
['key' => 'content', 'title' => '内容上传限制', 'fields' => [
|
||||
['key' => 'submit_image_max_size', 'label' => '投稿图片大小上限 (MB)', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 20],
|
||||
['key' => 'submit_video_max_size', 'label' => '投稿视频大小上限 (MB)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 200],
|
||||
['key' => 'comment_image_max_size', 'label' => '评论图片大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
|
||||
['key' => 'size-limits', 'title' => '上传大小限制', 'fields' => [
|
||||
['key' => 'size_limit_notice', 'label' => '服务器环境提示', 'type' => 'notice', 'alertType' => 'warning', 'alertContent' => '设置上传大小,请考虑服务器负荷、硬盘,以及服务器最大能支持的范围。实际上传大小取「主题限制」与「PHP 环境限制」的较小值,超过 PHP 环境限制时请调整 php.ini 的 upload_max_filesize 与 post_max_size。'],
|
||||
['key' => 'upload_img_size', 'label' => '前端图片上传限制', 'type' => 'repeater', 'default' => [
|
||||
['type' => 'default', 'val' => 5],
|
||||
['type' => 'admin', 'val' => 20],
|
||||
], 'min' => 1, 'button_title' => '添加细分用户权限', 'help' => '投稿、评论等前端场景允许上传的最大图片大小(单位 MB)。同一个用户满足多个条件则取最大值,「最小默认值」为所有人兜底。', 'fields' => [
|
||||
['key' => 'type', 'label' => '用户类型', 'type' => 'select', 'default' => 'logged', 'options' => self::upload_user_type_options()],
|
||||
['key' => 'val', 'label' => '可上传', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 2048, 'unit' => 'M'],
|
||||
]],
|
||||
['key' => 'resource', 'title' => '资源附件限制', 'fields' => [
|
||||
['key' => 'resource_max_size', 'label' => '资源附件大小上限 (MB)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 200],
|
||||
['key' => 'image_upload_multiple', 'label' => '图片批量上传最大数量', 'type' => 'number', 'default' => 6, 'min' => 0, 'max' => 100, 'unit' => '张', 'help' => '前端投稿、评论上传图片允许的单次批量上传数量(为 0 则不限制,为 1 则不能批量上传)。'],
|
||||
['key' => 'upload_video_size', 'label' => '前端视频上传限制', 'type' => 'repeater', 'default' => [
|
||||
['type' => 'default', 'val' => 30],
|
||||
['type' => 'admin', 'val' => 2048],
|
||||
], 'min' => 1, 'button_title' => '添加细分用户权限', 'help' => '前端允许上传的最大视频大小(单位 MB)。同一个用户满足多个条件则取最大值,必须包含最低默认值。', 'fields' => [
|
||||
['key' => 'type', 'label' => '用户类型', 'type' => 'select', 'default' => 'logged', 'options' => self::upload_user_type_options()],
|
||||
['key' => 'val', 'label' => '可上传', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 2048, 'unit' => 'M'],
|
||||
]],
|
||||
['key' => 'upload_file_size', 'label' => '前端文件上传大小限制', 'type' => 'repeater', 'default' => [
|
||||
['type' => 'default', 'val' => 30],
|
||||
['type' => 'admin', 'val' => 2048],
|
||||
], 'min' => 1, 'button_title' => '添加细分用户权限', 'help' => '前端允许上传的其他文件(不含视频和图片,建议大于视频图片)最大大小(单位 MB)。同一个用户满足多个条件则取最大值,必须包含最低默认值。', 'fields' => [
|
||||
['key' => 'type', 'label' => '用户类型', 'type' => 'select', 'default' => 'logged', 'options' => self::upload_user_type_options()],
|
||||
['key' => 'val', 'label' => '可上传', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 2048, 'unit' => 'M'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'mime-limits', 'title' => '上传格式限制', 'fields' => [
|
||||
['key' => 'mime_limit_notice', 'label' => '安全提示', 'type' => 'notice', 'alertType' => 'warning', 'alertContent' => '请添加文件的扩展名和 MIME 类型,用英文逗号分割。格式示例:mp3=audio/mpeg,jpg=image/jpeg。禁止的优先级大于允许。郑重提示:请勿允许上传 php、sh、exe 等可执行文件,会极大增加服务器被入侵攻击的风险。'],
|
||||
['key' => 'upload_file_mimes', 'label' => '前端上传文件格式限制', 'type' => 'repeater', 'default' => [
|
||||
['type' => 'admin', 'pattern' => 'allow', 'val' => 'svg=image/svg+xml'],
|
||||
['type' => 'all', 'pattern' => 'allow', 'val' => ''],
|
||||
], 'min' => 1, 'button_title' => '添加细分用户权限', 'help' => '根据不同的用户类型,添加额外允许或禁止上传的文件格式(禁止的优先级大于允许)。', 'fields' => [
|
||||
['key' => 'type', 'label' => '用户类型', 'type' => 'select', 'default' => 'all', 'options' => self::upload_mime_user_type_options()],
|
||||
['key' => 'pattern', 'label' => '规则', 'type' => 'select', 'default' => 'allow', 'options' => [
|
||||
['label' => '允许', 'value' => 'allow'],
|
||||
['label' => '禁止', 'value' => 'prohibit'],
|
||||
]],
|
||||
['key' => 'val', 'label' => '格式', 'type' => 'textarea', 'default' => '', 'rows' => 1, 'placeholder' => '如:svg=image/svg+xml,mp3=audio/mpeg'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'behavior', 'title' => '上传行为', 'fields' => [
|
||||
['key' => 'newfilename', 'label' => '上传文件自动重命名', 'type' => 'switch', 'default' => false, 'help' => '开启后上传文件按规则重命名,减少重名文件查询,提高上传效率。'],
|
||||
['key' => 'newfilename_type', 'label' => '重命名格式', 'type' => 'radio', 'default' => 'time', 'dependsOn' => ['key' => 'newfilename', 'value' => true], 'options' => [
|
||||
['label' => '时间 + 原文件名', 'value' => 'time'],
|
||||
['label' => '随机字符串', 'value' => 'random'],
|
||||
]],
|
||||
['key' => 'close_attachment_page', 'label' => '非管理员屏蔽附件页面', 'type' => 'switch', 'default' => true, 'help' => '开启后非管理员访问附件(图片、文件等)页面将直接显示 404,可有效提高文件的安全性。'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -546,11 +546,12 @@ class UserController extends ControllerBase {
|
||||
return $this->error(__('图片文件上传失败,请重新选择', 'aksrt'), 400);
|
||||
}
|
||||
|
||||
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$max_size_mb = max(1, min(10, (int) ($settings['comment_image_max_size'] ?? 2)));
|
||||
$max_size = class_exists('\AKSRT\Attachment\UploadLimits')
|
||||
? \AKSRT\Attachment\UploadLimits::limit_bytes('image', $user_id)
|
||||
: (int) wp_max_upload_size();
|
||||
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
|
||||
if ($actual_size <= 0 || $actual_size > $max_size_mb * MB_IN_BYTES) {
|
||||
return $this->error(sprintf(__('图片大小必须在 1 字节至 %dMB 之间', 'aksrt'), $max_size_mb), 413);
|
||||
if ($actual_size <= 0 || $actual_size > $max_size) {
|
||||
return $this->error(sprintf(__('图片大小不能超过 %s。', 'aksrt'), size_format($max_size)), 413);
|
||||
}
|
||||
|
||||
$mimes = [
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* Upload limits, mime restrictions, auto-rename, and attachment page rules.
|
||||
*
|
||||
* Mirrors the reference theme's upload permission suite (zib-attachment.php):
|
||||
* per-user-type size limits for image / video / file uploads, allow & prohibit
|
||||
* mime lists, upload file renaming, and hiding attachment pages from guests.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Attachment;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class UploadLimits {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** Setting keys holding the per-user-type size rows for each upload kind. */
|
||||
const KIND_OPTIONS = [
|
||||
'image' => 'upload_img_size',
|
||||
'video' => 'upload_video_size',
|
||||
'file' => 'upload_file_size',
|
||||
];
|
||||
|
||||
/** Fallback limits (MB) used before the admin saves any size row. */
|
||||
const KIND_FALLBACK_MB = [
|
||||
'image' => 5.0,
|
||||
'video' => 30.0,
|
||||
'file' => 30.0,
|
||||
];
|
||||
|
||||
/** Hard caps mirroring the capability matrix (level 2..10, vip 1..2). */
|
||||
const LEVEL_MAX = 10;
|
||||
const VIP_MAX = 2;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_filter('upload_mimes', [$this, 'filter_upload_mimes']);
|
||||
add_filter('getimagesize_mimes_to_exts', [$this, 'filter_image_mimes_to_exts']);
|
||||
add_filter('wp_handle_upload_prefilter', [$this, 'rename_upload_file'], 99);
|
||||
add_action('template_redirect', [$this, 'close_attachment_page']);
|
||||
}
|
||||
|
||||
/**
|
||||
* User type options shared by the admin schema and the SPA schema.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function user_type_options(): array {
|
||||
$options = [
|
||||
'default' => __('最小默认值(所有人)', 'aksrt'),
|
||||
'logged' => __('已登录用户', 'aksrt'),
|
||||
];
|
||||
for ($i = 2; $i <= self::LEVEL_MAX; $i++) {
|
||||
$options['level_' . $i] = sprintf(__('等级达到 %d 级', 'aksrt'), $i);
|
||||
}
|
||||
for ($i = 1; $i <= self::VIP_MAX; $i++) {
|
||||
$options['vip_' . $i] = sprintf(__('VIP%d 会员', 'aksrt'), $i);
|
||||
}
|
||||
$options['auth'] = __('已认证用户', 'aksrt');
|
||||
$options['admin'] = __('超级管理员', 'aksrt');
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a user matches a config row type.
|
||||
*/
|
||||
public static function user_matches_type(string $type, int $user_id = 0): bool {
|
||||
if ($user_id <= 0) {
|
||||
$user_id = get_current_user_id();
|
||||
}
|
||||
if ($type === 'default' || $type === 'all') {
|
||||
return true;
|
||||
}
|
||||
if ($type === 'logged') {
|
||||
return $user_id > 0;
|
||||
}
|
||||
if ($type === 'admin') {
|
||||
return $user_id > 0 && is_super_admin($user_id);
|
||||
}
|
||||
if ($type === 'auth') {
|
||||
return $user_id > 0
|
||||
&& class_exists('\AKSRT\User\AuthApply')
|
||||
&& \AKSRT\User\AuthApply::is_verified($user_id);
|
||||
}
|
||||
if (str_starts_with($type, 'level_')) {
|
||||
$need = (int) substr($type, 6);
|
||||
return $user_id > 0
|
||||
&& $need > 0
|
||||
&& class_exists('\AKSRT\User\Level')
|
||||
&& \AKSRT\User\Level::get_level($user_id) >= $need;
|
||||
}
|
||||
if (str_starts_with($type, 'vip_')) {
|
||||
$need = (int) substr($type, 4);
|
||||
return $user_id > 0
|
||||
&& $need > 0
|
||||
&& function_exists('aksrt_get_user_vip_level')
|
||||
&& aksrt_get_user_vip_level($user_id) >= $need;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Size rows (user type + MB) for one upload kind.
|
||||
*
|
||||
* @return array<int, array{type:string, val:float}>
|
||||
*/
|
||||
private static function size_rows(string $option_key): array {
|
||||
if ($option_key === '' || !class_exists('\AKSRT\Settings\Settings')) {
|
||||
return [];
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
$rows = is_array($settings[$option_key] ?? null) ? $settings[$option_key] : [];
|
||||
$clean = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$type = sanitize_key((string) ($row['type'] ?? ''));
|
||||
$val = (float) ($row['val'] ?? 0);
|
||||
if ($type === '' || $val <= 0) {
|
||||
continue;
|
||||
}
|
||||
$clean[] = ['type' => $type, 'val' => $val];
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Max upload size in MB for a user.
|
||||
*
|
||||
* A user takes the maximum value among all rows whose type matches them;
|
||||
* the "default" row is the floor for everyone (reference theme rule:
|
||||
* 同一个用户满足多个条件则取最大值,必须包含最低默认值).
|
||||
*/
|
||||
public static function limit_mb(string $kind, int $user_id = 0): float {
|
||||
$option_key = self::KIND_OPTIONS[$kind] ?? '';
|
||||
if ($option_key === '') {
|
||||
return 0.0;
|
||||
}
|
||||
$rows = self::size_rows($option_key);
|
||||
if (!$rows) {
|
||||
return self::KIND_FALLBACK_MB[$kind] ?? 0.0;
|
||||
}
|
||||
$default = 0.0;
|
||||
$matched = 0.0;
|
||||
foreach ($rows as $row) {
|
||||
if ($row['type'] === 'default' || $row['type'] === 'all') {
|
||||
$default = max($default, $row['val']);
|
||||
continue;
|
||||
}
|
||||
if (self::user_matches_type($row['type'], $user_id)) {
|
||||
$matched = max($matched, $row['val']);
|
||||
}
|
||||
}
|
||||
$limit = max($default, $matched);
|
||||
// No default row and nothing matched: keep the built-in floor instead
|
||||
// of silently blocking the upload with a zero limit.
|
||||
return $limit > 0 ? $limit : (self::KIND_FALLBACK_MB[$kind] ?? 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective byte limit for a user.
|
||||
*
|
||||
* The configured limit is clamped to the PHP upload cap so the front end
|
||||
* never promises more than the server can accept in a single request.
|
||||
*/
|
||||
public static function limit_bytes(string $kind, int $user_id = 0): int {
|
||||
$mb = self::limit_mb($kind, $user_id);
|
||||
if ($mb <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int) min($mb * MB_IN_BYTES, (int) wp_max_upload_size());
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Max images allowed in one front-end batch selection (0 = unlimited).
|
||||
*/
|
||||
public static function image_batch_limit(): int {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return 6;
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
return max(0, (int) ($settings['image_upload_multiple'] ?? 6));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowed / prohibited mime maps for a user.
|
||||
*
|
||||
* Row format: type + pattern(allow|prohibit) + "ext=mime,ext=mime".
|
||||
* Prohibit wins over allow for the same extension.
|
||||
*
|
||||
* @return array{allow:array<string,string>, prohibit:array<string,string>, allow_image_mime_to_ext:array<string,string>}
|
||||
*/
|
||||
public static function mimes_limit(int $user_id = 0): array {
|
||||
$result = [
|
||||
'allow' => [],
|
||||
'prohibit' => [],
|
||||
'allow_image_mime_to_ext' => [],
|
||||
];
|
||||
if ($user_id <= 0) {
|
||||
$user_id = get_current_user_id();
|
||||
}
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return $result;
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
$rows = is_array($settings['upload_file_mimes'] ?? null) ? $settings['upload_file_mimes'] : [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$type = sanitize_key((string) ($row['type'] ?? ''));
|
||||
$pattern = ($row['pattern'] ?? 'allow') === 'prohibit' ? 'prohibit' : 'allow';
|
||||
$val = (string) ($row['val'] ?? '');
|
||||
if ($type === '' || trim($val) === '' || !self::user_matches_type($type, $user_id)) {
|
||||
continue;
|
||||
}
|
||||
foreach (explode(',', $val) as $pair) {
|
||||
$kv = array_map('trim', explode('=', (string) $pair));
|
||||
if (count($kv) !== 2 || $kv[0] === '' || $kv[1] === '') {
|
||||
continue;
|
||||
}
|
||||
$ext = strtolower($kv[0]);
|
||||
$mime = strtolower($kv[1]);
|
||||
$result[$pattern][$ext] = $mime;
|
||||
if ($pattern === 'allow' && strpos($mime, 'image/') === 0) {
|
||||
$result['allow_image_mime_to_ext'][$mime] = $ext;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply configured allow / prohibit mimes to the system mime list.
|
||||
*
|
||||
* @param array<string, string> $mimes
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function filter_upload_mimes($mimes) {
|
||||
if (!is_array($mimes)) {
|
||||
return $mimes;
|
||||
}
|
||||
$limit = self::mimes_limit();
|
||||
if (empty($limit['allow']) && empty($limit['prohibit'])) {
|
||||
return $mimes;
|
||||
}
|
||||
$mimes = array_merge($mimes, $limit['allow']);
|
||||
$mimes = array_diff($mimes, $limit['prohibit']);
|
||||
return $mimes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register extra image mime -> extension mappings (e.g. svg).
|
||||
*
|
||||
* @param array<string, string> $mimes_to_exts
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function filter_image_mimes_to_exts($mimes_to_exts) {
|
||||
if (!is_array($mimes_to_exts)) {
|
||||
return $mimes_to_exts;
|
||||
}
|
||||
$limit = self::mimes_limit();
|
||||
if (empty($limit['allow_image_mime_to_ext'])) {
|
||||
return $mimes_to_exts;
|
||||
}
|
||||
return array_merge($mimes_to_exts, $limit['allow_image_mime_to_ext']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally rename uploaded files to reduce name collisions.
|
||||
*
|
||||
* @param array<string, mixed> $file
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rename_upload_file(array $file): array {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return $file;
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
if (empty($settings['newfilename'])) {
|
||||
return $file;
|
||||
}
|
||||
$name = (string) ($file['name'] ?? '');
|
||||
if ($name === '') {
|
||||
return $file;
|
||||
}
|
||||
$info = pathinfo($name);
|
||||
$ext = empty($info['extension']) ? '' : '.' . $info['extension'];
|
||||
if (($settings['newfilename_type'] ?? 'time') === 'random') {
|
||||
$file['name'] = substr(md5($name), 0, 10) . current_time('YmdHis') . $ext;
|
||||
} else {
|
||||
$file['name'] = current_time('YmdHis') . mt_rand(10, 99) . mt_rand(0, 9) . '-' . $name;
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide attachment pages from non-admins (returns 404 instead).
|
||||
*/
|
||||
public function close_attachment_page(): void {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return;
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_all();
|
||||
if (empty($settings['close_attachment_page'])) {
|
||||
return;
|
||||
}
|
||||
if (!is_attachment() || is_super_admin()) {
|
||||
return;
|
||||
}
|
||||
global $wp_query;
|
||||
$wp_query->is_404 = true;
|
||||
}
|
||||
}
|
||||
@@ -118,18 +118,13 @@ class FrontSubmit {
|
||||
}
|
||||
|
||||
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;
|
||||
// Kind names follow the REST route; the config rows use the reference
|
||||
// theme's naming (image / video / file).
|
||||
$config_kind = $kind === 'resource' ? 'file' : $kind;
|
||||
if (class_exists('\AKSRT\Attachment\UploadLimits')) {
|
||||
return \AKSRT\Attachment\UploadLimits::limit_bytes($config_kind);
|
||||
}
|
||||
|
||||
return min($configured_mb * MB_IN_BYTES, (int) wp_max_upload_size());
|
||||
return (int) wp_max_upload_size();
|
||||
}
|
||||
|
||||
public function rest_upload_attachment(\WP_REST_Request $request) {
|
||||
|
||||
@@ -479,9 +479,9 @@ trait ScriptsPart1c {
|
||||
'commentIasAuto' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_comment_display_settings()['paging_ajax_ias'] : true,
|
||||
'commentUploadUrl' => esc_url_raw(rest_url('aksrt/v1/comment/upload-image')),
|
||||
'commentUploadMaxSize' => (function () {
|
||||
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$max_size_mb = max(1, min(10, (int) ($settings['comment_image_max_size'] ?? 2)));
|
||||
return $max_size_mb * MB_IN_BYTES;
|
||||
return class_exists('\AKSRT\Attachment\UploadLimits')
|
||||
? \AKSRT\Attachment\UploadLimits::limit_bytes('image')
|
||||
: (int) wp_max_upload_size();
|
||||
})(),
|
||||
'commentIasMax' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_comment_display_settings()['paging_ajax_ias_max'] : 3,
|
||||
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
||||
|
||||
@@ -81,9 +81,12 @@ if (file_exists($js)) {
|
||||
(string) filemtime($js),
|
||||
true
|
||||
);
|
||||
$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');
|
||||
$upload_image_max_bytes = \AKSRT\Core\FrontSubmit::upload_limit_bytes('image');
|
||||
$upload_video_max_bytes = \AKSRT\Core\FrontSubmit::upload_limit_bytes('video');
|
||||
$upload_resource_max_bytes = \AKSRT\Core\FrontSubmit::upload_limit_bytes('resource');
|
||||
$image_upload_multiple = class_exists('\AKSRT\Attachment\UploadLimits')
|
||||
? \AKSRT\Attachment\UploadLimits::image_batch_limit()
|
||||
: 6;
|
||||
|
||||
wp_localize_script('aksrt-front-submit-editor', 'aksrtSubmitData', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
@@ -101,6 +104,7 @@ if (file_exists($js)) {
|
||||
|
||||
|
||||
'leaveWarning' => !empty($ctx['settings']['leave_warning']),
|
||||
'imageUploadMultiple' => $image_upload_multiple,
|
||||
'ai' => [
|
||||
'available' => !empty($ctx['ai_available']),
|
||||
'summary' => !empty($ctx['ai_available']),
|
||||
@@ -117,15 +121,15 @@ if (file_exists($js)) {
|
||||
'uploads' => [
|
||||
'image' => !empty($ctx['can_upload_img']) ? [
|
||||
'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/image')),
|
||||
'maxSize' => $submit_image_max_size,
|
||||
'maxSize' => $upload_image_max_bytes,
|
||||
] : null,
|
||||
'video' => !empty($ctx['can_upload_video']) ? [
|
||||
'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/video')),
|
||||
'maxSize' => $submit_video_max_size,
|
||||
'maxSize' => $upload_video_max_bytes,
|
||||
] : null,
|
||||
'resource' => !empty($ctx['can_upload_file']) ? [
|
||||
'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/resource')),
|
||||
'maxSize' => $resource_max_size,
|
||||
'maxSize' => $upload_resource_max_bytes,
|
||||
] : null,
|
||||
],
|
||||
'i18n' => [
|
||||
|
||||
@@ -86,6 +86,7 @@ namespace AKSRT\Settings {
|
||||
|
||||
namespace {
|
||||
require dirname(__DIR__, 2) . '/inc/modules/captcha.php';
|
||||
require dirname(__DIR__, 2) . '/src/Attachment/UploadLimits.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaPermissions.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/SchemaProvider.php';
|
||||
|
||||
|
||||
@@ -19,19 +19,35 @@ test('front submit enqueues the WordPress editor before wp_head is rendered', ()
|
||||
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');
|
||||
const limits = read('src/Attachment/UploadLimits.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, /UploadLimits::limit_bytes\(\$config_kind\)/);
|
||||
assert.match(controller, /\$config_kind = \$kind === 'resource' \? 'file' : \$kind;/);
|
||||
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\]/);
|
||||
// The effective clamp (configured limit vs PHP upload cap) lives in the limiter.
|
||||
assert.match(limits, /min\(\$mb \* MB_IN_BYTES, \(int\) wp_max_upload_size\(\)\)/);
|
||||
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('front submit image batch selection follows the admin upload config', () => {
|
||||
const page = read('templates/pages/page-submit.php');
|
||||
const editor = read('assets/js/front-submit-editor.js');
|
||||
|
||||
assert.match(page, /UploadLimits::image_batch_limit\(\)/);
|
||||
assert.match(page, /'imageUploadMultiple' => \$image_upload_multiple,/);
|
||||
assert.match(editor, /function imageBatchLimit\(\)/);
|
||||
assert.match(editor, /Number\(cfg\.imageUploadMultiple\)/);
|
||||
assert.match(editor, /multiple: batch\.multiple, maxItems: batch\.maxItems/);
|
||||
assert.match(editor, /type: 'image', multiple: imageBatch\.multiple, maxItems: imageBatch\.maxItems,/);
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user