diff --git a/assets/admin/src/constants/sections/permissions.js b/assets/admin/src/constants/sections/permissions.js
index dccb2621..1d198c39 100644
--- a/assets/admin/src/constants/sections/permissions.js
+++ b/assets/admin/src/constants/sections/permissions.js
@@ -7,37 +7,15 @@ export const permissionsSection = {
route: '/permissions',
children: [
customPage({
- key: 'permissions-comment',
- name: 'PermissionsComment',
- label: '评论权限',
- route: '/permissions/comment',
- component: 'CommentPermissions',
- title: '评论权限',
- desc: '游客评论、频率限制、敏感词和审核策略。',
+ key: 'permissions-capabilities',
+ name: 'PermissionsCapabilities',
+ label: '基本权限',
+ route: '/permissions/capabilities',
+ component: 'PermissionsCapabilities',
+ title: '基本权限 / 能力矩阵',
+ desc: '统一管理用户、投稿、评论、上传和下载的准入能力。',
canonicalDomain: 'permissions',
- section: 'comment',
- }),
- customPage({
- key: 'permissions-attachment',
- name: 'PermissionsAttachment',
- label: '附件权限',
- route: '/permissions/attachment',
- component: 'AttachmentPermissions',
- title: '附件权限',
- desc: '上传角色、文件类型、大小限制和审核状态。',
- canonicalDomain: 'permissions',
- section: 'attachment',
- }),
- customPage({
- key: 'permissions-download',
- name: 'PermissionsDownload',
- label: '下载权限',
- route: '/permissions/download',
- component: 'DownloadPermissions',
- title: '下载权限',
- desc: '登录要求、积分要求、角色要求和 VIP 特权。',
- canonicalDomain: 'permissions',
- section: 'download',
+ section: 'capabilities',
}),
schemaPage({
key: 'permissions-login',
@@ -52,23 +30,23 @@ export const permissionsSection = {
schemaPage({
key: 'permissions-moderation',
name: 'PermissionsModeration',
- label: '审核和风控',
+ label: '内容审核',
route: '/permissions/moderation',
- title: '审核和风控',
+ title: '内容审核',
desc: '评论审核、反垃圾、限流与风控配置。',
canonicalDomain: 'permissions',
section: 'moderation',
}),
customPage({
- key: 'permissions-capabilities',
- name: 'PermissionsCapabilities',
- label: '基本权限',
- route: '/permissions/capabilities',
- component: 'PermissionsCapabilities',
- title: '基本权限 / 能力矩阵',
- desc: '统一管理用户治理、投稿和评论的角色能力矩阵。',
+ key: 'permissions-attachment',
+ name: 'PermissionsAttachment',
+ label: '附件配置',
+ route: '/permissions/attachment',
+ component: 'AttachmentPermissions',
+ title: '附件配置',
+ desc: '管理文件大小、类型、安全限制和审核策略。',
canonicalDomain: 'permissions',
- section: 'capabilities',
+ section: 'attachment',
}),
],
}
diff --git a/assets/admin/src/pages/ResourceDownload.vue b/assets/admin/src/pages/ResourceDownload.vue
index 427132d5..fc26b6be 100644
--- a/assets/admin/src/pages/ResourceDownload.vue
+++ b/assets/admin/src/pages/ResourceDownload.vue
@@ -6,7 +6,7 @@
>
- 下载权限
+ 基本权限
收款接口
@@ -18,15 +18,9 @@
当前联动摘要
-
- 登录门槛:{{ form.download_login ? '已启用' : '已关闭' }}
-
积分购买模式:{{ form.download_credit ? '已启用' : '已关闭' }}
-
- 下载验证码:{{ form.download_captcha ? '已启用' : '已关闭' }}
-
默认积分价:{{ form.download_default_cost }} {{ currencyName }}
@@ -44,10 +38,6 @@
下载门槛
-
-
-
-
@@ -55,10 +45,6 @@
-
-
-
-
@@ -140,9 +126,7 @@ const creditSnapshot = ref({})
const overview = ref({})
const form = reactive({
- download_login: true,
download_credit: true,
- download_captcha: false,
download_default_cost: 0,
payment_paid_post_types: 'post',
payment_vip_free_download: false,
@@ -185,9 +169,7 @@ async function load() {
creditSnapshot.value = creditData
overview.value = extractData(overviewRes)
- form.download_login = featureData.download_login !== false
form.download_credit = featureData.download_credit !== false
- form.download_captcha = !!featureData.download_captcha
form.download_default_cost = Number(creditData.download_default_cost || 0)
form.payment_paid_post_types = schemaData.payment_paid_post_types || 'post'
form.payment_vip_free_download = !!schemaData.payment_vip_free_download
@@ -206,9 +188,7 @@ async function handleSave() {
try {
const nextFeatures = {
...featureSnapshot.value,
- download_login: form.download_login,
download_credit: form.download_credit,
- download_captcha: form.download_captcha,
}
const nextCredits = {
...creditSnapshot.value,
diff --git a/assets/admin/src/pages/SiteSettings.vue b/assets/admin/src/pages/SiteSettings.vue
index 3b53488b..bc069acd 100644
--- a/assets/admin/src/pages/SiteSettings.vue
+++ b/assets/admin/src/pages/SiteSettings.vue
@@ -24,12 +24,6 @@
-
-
- 是
- 否
-
-
@@ -123,7 +117,6 @@ const saving = ref(false)
// 后端 /settings/basic 返回基础字段;auth 相关字段由 /settings/auth 独立维护。
const basic = reactive({
comment_enabled: true,
- comment_requires_registration: false,
icp_number: '',
police_number: '',
lazyload_enabled: true,
@@ -139,7 +132,6 @@ async function loadAll() {
if (basicRes?.code === 'success' && basicRes.data) {
const d = basicRes.data
basic.comment_enabled = d.comment_enabled !== false
- basic.comment_requires_registration = !!d.comment_requires_registration
basic.icp_number = d.icp_number || ''
basic.police_number = d.police_number || ''
basic.lazyload_enabled = d.lazyload_enabled !== false
@@ -159,7 +151,6 @@ async function handleSave() {
try {
const basicPayload = {
comment_enabled: basic.comment_enabled,
- comment_requires_registration: basic.comment_requires_registration,
icp_number: basic.icp_number,
police_number: basic.police_number,
lazyload_enabled: basic.lazyload_enabled,
diff --git a/assets/admin/src/pages/permissions/AttachmentPermissions.vue b/assets/admin/src/pages/permissions/AttachmentPermissions.vue
index 2867dc52..c0703126 100644
--- a/assets/admin/src/pages/permissions/AttachmentPermissions.vue
+++ b/assets/admin/src/pages/permissions/AttachmentPermissions.vue
@@ -2,11 +2,10 @@
- 下载权限
审核策略
@@ -34,31 +33,10 @@
-
-
-
- 关闭后用户使用项目内置默认头像,且不能自行更换。
-
-
-
-
-
-
- 控制用户中心公开主页封面图上传入口。
-
-
-
-
-
-
- 控制用户中心公开可用的背景图上传入口。
-
-
-
-
+
@@ -70,24 +48,10 @@
内容上传
-
-
-
- 前台投稿编辑器中允许插入图片。
-
-
-
-
-
-
- 评论工具栏中允许插入图片链接或上传图片。
-
-
-
@@ -100,13 +64,6 @@
资源附件
-
-
-
- 后台资源编辑器中允许上传附件文件。
-
-
-
@@ -158,27 +115,19 @@ const saving = ref(false)
const moderationSnapshot = ref({})
const form = reactive({
- allow_avatar_upload: true,
- allow_cover_upload: true,
- allow_background_upload: false,
avatar_max_size: 2,
- allow_submit_images: true,
submit_image_max_size: 5,
- allow_comment_images: false,
comment_image_max_size: 2,
- allow_resource_upload: true,
resource_max_size: 50,
ai_moderate_background: true,
ai_moderate_submit: false,
})
const summaryItems = computed(() => [
- { key: 'avatar', icon: 'fas fa-user-circle', label: '头像上传', enabled: form.allow_avatar_upload },
- { key: 'cover', icon: 'fas fa-image', label: '封面上传', enabled: form.allow_cover_upload },
- { key: 'background', icon: 'fas fa-photo-video', label: '背景图上传', enabled: form.allow_background_upload },
- { key: 'submit', icon: 'fas fa-edit', label: '投稿图片', enabled: form.allow_submit_images },
- { key: 'comment', icon: 'fas fa-comment-dots', label: '评论图片', enabled: form.allow_comment_images },
- { key: 'resource', icon: 'fas fa-file-archive', label: '资源附件', enabled: form.allow_resource_upload },
+ { key: 'profile', icon: 'fas fa-id-card', label: '资料卡图片', enabled: true },
+ { key: 'submit', icon: 'fas fa-edit', label: '投稿图片', enabled: true },
+ { key: 'comment', icon: 'fas fa-comment-dots', label: '评论图片', enabled: true },
+ { key: 'resource', icon: 'fas fa-file-archive', label: '资源附件', enabled: true },
])
function go(path) {
@@ -199,15 +148,9 @@ async function load() {
const moderation = moderationRes?.data ?? moderationRes ?? {}
// Map permission fields
- form.allow_avatar_upload = permission.allow_avatar_upload !== false
- form.allow_cover_upload = permission.allow_cover_upload !== false
- form.allow_background_upload = !!permission.allow_background_upload
form.avatar_max_size = Number(permission.avatar_max_size || 2)
- form.allow_submit_images = permission.allow_submit_images !== false
form.submit_image_max_size = Number(permission.submit_image_max_size || 5)
- form.allow_comment_images = !!permission.allow_comment_images
form.comment_image_max_size = Number(permission.comment_image_max_size || 2)
- form.allow_resource_upload = permission.allow_resource_upload !== false
form.resource_max_size = Number(permission.resource_max_size || 50)
form.ai_moderate_background = moderation.ai_moderate_background !== false
@@ -226,15 +169,9 @@ async function handleSave() {
try {
await Promise.all([
api.savePermissionsAttachmentSettings({
- allow_avatar_upload: form.allow_avatar_upload,
- allow_cover_upload: form.allow_cover_upload,
- allow_background_upload: form.allow_background_upload,
avatar_max_size: form.avatar_max_size,
- allow_submit_images: form.allow_submit_images,
submit_image_max_size: form.submit_image_max_size,
- allow_comment_images: form.allow_comment_images,
comment_image_max_size: form.comment_image_max_size,
- allow_resource_upload: form.allow_resource_upload,
resource_max_size: form.resource_max_size,
}),
api.savePermissionsModerationSettings({
diff --git a/assets/admin/src/pages/permissions/CommentPermissions.vue b/assets/admin/src/pages/permissions/CommentPermissions.vue
deleted file mode 100644
index 45a9d030..00000000
--- a/assets/admin/src/pages/permissions/CommentPermissions.vue
+++ /dev/null
@@ -1,154 +0,0 @@
-
-
-
-
-
- 人机验证
- 审核策略
-
-
-
-
-
-
-
-
-
- 评论验证码:{{ status.commentCaptchaEnabled ? '已启用' : '未启用' }}
-
-
- 审核链路:{{ status.moderationEnabled ? '已启用' : '未启用' }}
-
-
- 评论限流:{{ status.rateLimitEnabled ? '已启用' : '未启用' }}
-
-
- 敏感词:{{ status.keywordRuleCount }} 条
-
-
-
-
-
-
-
-
-
-
- 开启后,游客只能浏览评论,不能直接提交评论。
-
-
-
-
-
-
- 关闭后,前台评论列表将不再展示回复按钮。
-
-
-
-
-
-
-
- 评论排序、分页方式、头像大小和空状态文案继续在“文章&列表 -> 评论设置”中维护;本页只负责准入与治理联动。
-
-
-
-
- 保存配置
-
-
-
-
-
-
diff --git a/assets/admin/src/pages/permissions/DownloadPermissions.vue b/assets/admin/src/pages/permissions/DownloadPermissions.vue
deleted file mode 100644
index 173b1d00..00000000
--- a/assets/admin/src/pages/permissions/DownloadPermissions.vue
+++ /dev/null
@@ -1,161 +0,0 @@
-
-
-
-
-
- 资源下载页
- 人机验证
-
-
-
-
-
-
-
-
-
- 登录要求:{{ form.download_login ? '已启用' : '已关闭' }}
-
-
- 积分购买模式:{{ form.download_credit ? '已启用' : '已关闭' }}
-
-
- 下载验证码:{{ form.download_captcha ? '已启用' : '已关闭' }}
-
- 默认积分价参考:{{ defaultCost }}
- Token TTL:10 分钟
-
-
-
-
-
-
-
-
-
- 关闭后,单个资源的登录要求仍可通过资源元字段单独覆盖。
-
-
-
-
-
-
- 开启后,文章 pay_modo=积分 可走收银台一次购买解锁;下载 token 不再按次扣积分。关闭时积分模式资源按免费/需登录策略处理。
-
-
-
-
-
-
- 具体验证码服务商与场景白名单仍在「人机验证」页维护。
-
-
-
-
-
-
-
- 实际价格以文章「付费内容设置」为准;本页只维护全局访问门槛。VIP 白名单与收银台策略见「支付 & 付费」相关页面。
-
-
-
-
- 保存配置
-
-
-
-
-
-
diff --git a/assets/admin/src/pages/permissions/PermissionsCapabilities.vue b/assets/admin/src/pages/permissions/PermissionsCapabilities.vue
index 015d4a1e..56ac1966 100644
--- a/assets/admin/src/pages/permissions/PermissionsCapabilities.vue
+++ b/assets/admin/src/pages/permissions/PermissionsCapabilities.vue
@@ -12,8 +12,7 @@
- 转账/提现(balance_transfer、points_transfer、balance_withdraw)默认 exclude all,与 Zibll 一致:未在矩阵中为角色/等级/VIP/认证打开前,前台入口会隐藏或拒绝。
- medal_manually_set 控制谁可授予徽章;管理员 manage_options 始终可通过。
+ 每项能力都通过“所有人、已登录、等级、VIP、已认证”五列治理;管理员(manage_options)始终可通过。
diff --git a/assets/admin/src/pages/users/UsersFeatures.vue b/assets/admin/src/pages/users/UsersFeatures.vue
index 1b256bc7..569e67c6 100644
--- a/assets/admin/src/pages/users/UsersFeatures.vue
+++ b/assets/admin/src/pages/users/UsersFeatures.vue
@@ -1,49 +1,30 @@
-
+
+ 基本权限
公开主页
- 附件权限
-
+
-
-
- 固定入口:个人信息 / 账号设置 / 消息通知
-
- 背景图编辑位:{{ form.user_cover_upload ? '已展示' : '已隐藏' }}
-
-
- 背景图上传权限:{{ backgroundUploadAllowed ? '已开放' : '未开放' }}
-
-
- 公开主页侧栏:{{ homeSummary.sidebar ? '开启' : '关闭' }}
-
+
+
+
+ 个人资料固定为昵称、个人签名、隐私设置、性别、个人网站、QQ、微信和 GitHub。公开资料由访问者身份过滤,原始社交资料不会下发给无权访客。
+
+
+ 背景图操作入口始终显示在资料卡右下角的三点菜单中;“上传头像”和“上传用户资料卡背景图”由基本权限矩阵控制。
+
-
+
-
-
- 个人信息
- 账号设置
- 消息通知
-
-
-
@@ -53,40 +34,10 @@
账户余额 / 签到
-
- “个人信息”始终保留在首位;积分面板仅在积分系统启用时显示。
-
-
-
-
-
-
-
- 这里只控制前台入口显示;真正的上传权限仍由“附件权限”页治理。
-
-
-
-
-
-
- 关闭后,用户中心联系方式编辑区和公开主页联系卡片会一起隐藏。
-
-
-
-
-
-
-
- 公开主页的封面、侧栏、关注按钮和公开 Tab 仍在“页面&显示 / 用户主页”维护;本页负责“用户中心能否编辑哪些内容”。
-
-
-
保存配置
@@ -103,61 +54,22 @@ import PageHeader from '@/components/PageHeader.vue'
const router = useRouter()
const message = useMessage()
-
const loading = ref(true)
const saving = ref(false)
-const backgroundUploadAllowed = ref(false)
-const homeSummary = reactive({
- tabs: [],
- sidebar: true,
-})
-
-const form = reactive({
- dynamicTabs: ['posts', 'comments', 'favorites'],
- user_cover_upload: true,
- user_social_links: true,
-})
-
-function go(path) {
- router.push(path)
-}
-
-function extractData(response) {
- return response?.data ?? response ?? {}
-}
+const form = reactive({ dynamicTabs: ['posts', 'comments', 'favorites'] })
+function go(path) { router.push(path) }
+function extractData(response) { return response?.data ?? response ?? {} }
function parseTabs(value) {
- const items = Array.isArray(value)
- ? value
- : String(value || '')
- .split(/[\s,]+/)
- .filter(Boolean)
-
- return items
- .map((item) => String(item).trim())
- .filter((item) => ['posts', 'comments', 'favorites', 'credits'].includes(item))
+ const values = Array.isArray(value) ? value : String(value || '').split(/[\s,]+/)
+ return values.map((value) => String(value).trim()).filter((value) => ['posts', 'comments', 'favorites', 'credits'].includes(value))
}
async function load() {
loading.value = true
try {
- const [featureRes, homeRes, permissionRes] = await Promise.all([
- api.getSchemaPage('users', 'features'),
- api.getSchemaPage('page_display', 'user-home'),
- api.getPermissionsAttachmentSettings(),
- ])
-
- const featureData = extractData(featureRes)
- const homeData = extractData(homeRes)
- const permissionData = extractData(permissionRes)
-
- form.dynamicTabs = parseTabs(featureData.user_center_tabs || 'profile,posts,comments,favorites')
- form.user_cover_upload = featureData.user_cover_upload !== false
- form.user_social_links = featureData.user_social_links !== false
-
- homeSummary.tabs = parseTabs(homeData.user_home_tabs || 'posts,comments,favorites')
- homeSummary.sidebar = homeData.user_home_sidebar !== false
- backgroundUploadAllowed.value = !!permissionData.allow_background_upload
+ const data = extractData(await api.getSchemaPage('users', 'features'))
+ form.dynamicTabs = parseTabs(data.user_center_tabs || 'profile,posts,comments,favorites')
} catch (error) {
message.error(error.message || '加载用户功能配置失败')
} finally {
@@ -168,11 +80,7 @@ async function load() {
async function handleSave() {
saving.value = true
try {
- await api.saveSchemaPage('users', 'features', {
- user_center_tabs: ['profile', ...form.dynamicTabs].join(','),
- user_cover_upload: form.user_cover_upload,
- user_social_links: form.user_social_links,
- })
+ await api.saveSchemaPage('users', 'features', { user_center_tabs: ['profile', ...form.dynamicTabs].join(',') })
message.success('用户功能配置已保存')
} catch (error) {
message.error(error.message || '保存失败')
diff --git a/assets/admin/src/router/component-loaders/core.js b/assets/admin/src/router/component-loaders/core.js
index ce03ba10..8ab0a3a7 100644
--- a/assets/admin/src/router/component-loaders/core.js
+++ b/assets/admin/src/router/component-loaders/core.js
@@ -10,10 +10,8 @@ export const corePageLoaders = {
BannerManager: () => import('@/pages/content/BannerManager.vue'),
PostPageSettings: () => import('@/pages/post-list/PostPageSettings.vue'),
CommentSettings: () => import('@/pages/post-list/CommentSettings.vue'),
- CommentPermissions: () => import('@/pages/permissions/CommentPermissions.vue'),
PermissionsCapabilities: () => import('@/pages/permissions/PermissionsCapabilities.vue'),
AttachmentPermissions: () => import('@/pages/permissions/AttachmentPermissions.vue'),
- DownloadPermissions: () => import('@/pages/permissions/DownloadPermissions.vue'),
CmsCredits: () => import('@/pages/CmsCredits.vue'),
ResourceDownload: () => import('@/pages/ResourceDownload.vue'),
ThemeInfoPage: () => import('@/pages/theme/ThemeInfoPage.vue'),
diff --git a/assets/admin/src/schema/sections/payment/order.js b/assets/admin/src/schema/sections/payment/order.js
index c6217a54..8c609fc2 100644
--- a/assets/admin/src/schema/sections/payment/order.js
+++ b/assets/admin/src/schema/sections/payment/order.js
@@ -15,7 +15,6 @@ export const orderSchema = {
payment_order_max_minutes: 30,
pay_mark: '付费',
pay_box_position: 'top',
- pay_free_logged_show: false,
payment_show_ip: false,
payment_show_device: false,
payment_show_order_no: true,
@@ -58,7 +57,6 @@ export const orderSchema = {
fields: [
{ key: 'pay_mark', label: '付费标记', type: 'input', default: '付费' },
{ key: 'pay_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '文章框顶部(封面后)', value: 'box_top' }, { label: '正文顶部', value: 'top' }, { label: '正文底部', value: 'bottom' }, { label: '文章框底部(相关前)', value: 'box_bottom' }] },
- { key: 'pay_free_logged_show', label: '登录用户显示免费', type: 'switch', default: false },
{ key: 'payment_show_ip', label: '显示用户IP', type: 'switch', default: false },
{ key: 'payment_show_device', label: '显示设备信息', type: 'switch', default: false },
{ key: 'payment_show_order_no', label: '显示订单号', type: 'switch', default: true },
diff --git a/assets/admin/src/schema/sections/payment/paid.js b/assets/admin/src/schema/sections/payment/paid.js
index e32eca95..f33ebe38 100644
--- a/assets/admin/src/schema/sections/payment/paid.js
+++ b/assets/admin/src/schema/sections/payment/paid.js
@@ -16,7 +16,6 @@ export const paidSchema = {
pay_box_position: 'top',
payment_download_enable: false,
payment_down_alone_page: false,
- pay_free_logged_show: false,
payment_download_remote_s: true,
payment_download_verify_nonce: true,
payment_download_user_log: 50,
@@ -56,7 +55,6 @@ export const paidSchema = {
{ key: 'pay_show_user', label: '显示购买用户', type: 'switch', default: true },
{ key: 'pay_show_paycount', label: '显示购买次数', type: 'switch', default: true },
{ key: 'pay_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '文章框顶部(封面后)', value: 'box_top' }, { label: '正文顶部', value: 'top' }, { label: '正文底部', value: 'bottom' }, { label: '文章框底部(相关前)', value: 'box_bottom' }] },
- { key: 'pay_free_logged_show', label: '免费资源需登录后下载', type: 'switch', default: false, help: '开启后,价格为 0 的下载资源也要求登录才可下载(对齐 zibll pay_free_logged_show)。' },
],
},
{
diff --git a/assets/admin/src/schema/sections/permissions/index.js b/assets/admin/src/schema/sections/permissions/index.js
index 734971fc..9494f45e 100644
--- a/assets/admin/src/schema/sections/permissions/index.js
+++ b/assets/admin/src/schema/sections/permissions/index.js
@@ -209,35 +209,6 @@ export const permissionsSchemas = {
],
},
- /**
- * Comment permissions
- */
- comment: {
- key: 'permissions.comment',
- title: '评论权限',
- icon: 'fas fa-comment-dots',
- description: '控制评论功能的开关和权限要求。',
- defaults: {
- comment_allow: true,
- comment_require_login: false,
- comment_nested_depth: 3,
- comment_allow_reply: true,
- },
- groups: [
- {
- key: 'basic',
- title: '评论基础',
- icon: 'fas fa-cog',
- fields: [
- { key: 'comment_allow', label: '允许评论', type: 'switch', help: '全局开启评论功能' },
- { key: 'comment_require_login', label: '需登录评论', type: 'switch', help: '只有登录用户才能评论' },
- { key: 'comment_nested_depth', label: '嵌套层级', type: 'slider', min: 1, max: 5, default: 3 },
- { key: 'comment_allow_reply', label: '允许回复', type: 'switch' },
- ],
- },
- ],
- },
-
/**
* Attachment permissions
*/
@@ -247,27 +218,18 @@ export const permissionsSchemas = {
icon: 'fas fa-paperclip',
description: '控制文件上传和附件下载权限。',
defaults: {
- allow_avatar_upload: true,
- allow_cover_upload: true,
- allow_background_upload: false,
avatar_max_size: 2,
- allow_submit_images: true,
submit_image_max_size: 5,
- allow_comment_images: false,
comment_image_max_size: 2,
- allow_resource_upload: true,
resource_max_size: 50,
},
groups: [
{
key: 'profile',
- title: '头像与封面',
+ title: '用户资料卡背景图',
icon: 'fas fa-user-circle',
fields: [
- { key: 'allow_avatar_upload', label: '允许用户上传头像', type: 'switch' },
- { key: 'allow_cover_upload', label: '允许用户上传封面图', type: 'switch' },
- { key: 'allow_background_upload', label: '允许用户上传背景图', type: 'switch' },
- { key: 'avatar_max_size', label: '头像/封面大小上限 (MB)', type: 'number', min: 1, max: 10 },
+ { key: 'avatar_max_size', label: '头像/用户资料卡背景图大小上限 (MB)', type: 'number', min: 1, max: 10 },
],
},
{
@@ -275,9 +237,7 @@ export const permissionsSchemas = {
title: '内容上传',
icon: 'fas fa-images',
fields: [
- { key: 'allow_submit_images', label: '允许投稿插入图片', type: 'switch' },
{ key: 'submit_image_max_size', label: '投稿图片大小上限 (MB)', type: 'number', min: 1, max: 20 },
- { key: 'allow_comment_images', label: '允许评论插入图片', type: 'switch' },
{ key: 'comment_image_max_size', label: '评论图片大小上限 (MB)', type: 'number', min: 1, max: 10 },
],
},
@@ -286,23 +246,10 @@ export const permissionsSchemas = {
title: '资源附件',
icon: 'fas fa-file-archive',
fields: [
- { key: 'allow_resource_upload', label: '允许资源附件上传', type: 'switch' },
{ key: 'resource_max_size', label: '资源附件大小上限 (MB)', type: 'number', min: 1, max: 200 },
],
},
],
},
- /**
- * Download permissions — live UI is DownloadPermissions.vue via cms_features.
- * Keep empty schema so SchemaPage cannot mis-write aksrt_settings.
- */
- download: {
- key: 'permissions.download',
- title: '下载权限',
- icon: 'fas fa-download',
- description: '下载门槛由 DownloadPermissions 页维护(cms_features);人机验证服务商在「人机验证」页。',
- defaults: {},
- groups: [],
- },
};
diff --git a/assets/admin/src/schema/sections/post-list/comment.js b/assets/admin/src/schema/sections/post-list/comment.js
index 209158aa..49405a1e 100644
--- a/assets/admin/src/schema/sections/post-list/comment.js
+++ b/assets/admin/src/schema/sections/post-list/comment.js
@@ -11,14 +11,11 @@ export const commentSchema = {
comment_paging_ajax_ias_max: 3,
comment_nested_depth: 3,
comment_avatar_size: 48,
- comment_require_login: false,
- comment_allow_reply: true,
comment_close_comments: false,
comment_edit_window: 10,
comment_smilie: true,
comment_code: true,
comment_img: true,
- comment_upload_img: false,
comment_like_s: true,
comment_hot_threshold: 10,
comment_quick_s: true,
@@ -56,11 +53,9 @@ export const commentSchema = {
},
{
key: 'permissions',
- title: '评论权限',
+ title: '评论设置',
icon: 'fas fa-lock',
fields: [
- { key: 'comment_require_login', label: '评论需要登录', type: 'switch', default: false },
- { key: 'comment_allow_reply', label: '允许回复', type: 'switch', default: true },
{ key: 'comment_close_comments', label: '关闭评论', type: 'switch', default: false },
{ key: 'comment_edit_window', label: '作者自助编辑时限 (分钟)', type: 'number', default: 10, min: 0, max: 1440 },
],
diff --git a/assets/admin/src/schema/sections/users/features.js b/assets/admin/src/schema/sections/users/features.js
index 81031cea..05342b6c 100644
--- a/assets/admin/src/schema/sections/users/features.js
+++ b/assets/admin/src/schema/sections/users/features.js
@@ -5,8 +5,6 @@ export const featuresSchema = {
description: '配置用户中心和个人主页的功能模块。',
defaults: {
user_center_tabs: 'profile,level,posts,comments,favorites,vip,credits,revenue,auth',
- user_cover_upload: true,
- user_social_links: true,
user_desc_std: '这家伙很懒…',
user_city_s: true,
user_city_type: 'province',
@@ -25,8 +23,6 @@ export const featuresSchema = {
icon: 'fas fa-user-shield',
fields: [
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,level,comments,favorites,vip,credits,revenue,auth', help: '逗号分隔。level=我的等级;creator=创作中心;revenue=推广收益中心;auth=官方认证;等级、VIP、订单、创作中心、收益中心和认证会按功能开关自动注入' },
- { key: 'user_cover_upload', label: '封面上传', type: 'switch', default: true },
- { key: 'user_social_links', label: '社交链接', type: 'switch', default: true },
{ key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' },
{ key: 'user_city_s', label: '显示城市(IP 归属地)', type: 'switch', default: true, help: '根据用户登录 IP 在用户主页与用户中心显示归属地;需同时在「扩展 → IP归属地」启用并勾选对应展示位置' },
{
diff --git a/assets/css/routes/profile/layout.css b/assets/css/routes/profile/layout.css
index e30ee729..e772e37e 100644
--- a/assets/css/routes/profile/layout.css
+++ b/assets/css/routes/profile/layout.css
@@ -96,34 +96,65 @@
pointer-events: none;
}
-.uc-header__cover-upload {
+.aksrt-cover-menu {
position: absolute;
bottom: 12px;
right: 12px;
z-index: 2;
+}
+.aksrt-cover-menu__trigger {
display: inline-flex;
align-items: center;
- gap: 4px;
- padding: 4px 10px;
+ justify-content: center;
+ width: 34px;
+ height: 34px;
+ padding: 0;
border: none;
- border-radius: 6px;
+ border-radius: 50%;
background: rgba(0, 0, 0, 0.25);
color: #fff;
- font-size: 12px;
cursor: pointer;
backdrop-filter: blur(4px);
- transition: background 0.2s;
+ transition: background .2s;
}
-.uc-header__cover-upload:hover {
+.aksrt-cover-menu__trigger:hover,
+.aksrt-cover-menu__trigger:focus-visible {
background: rgba(0, 0, 0, 0.45);
+ outline: 2px solid #fff;
+ outline-offset: 2px;
}
-.uc-header__cover-upload:disabled {
- opacity: 0.6;
- cursor: not-allowed;
+.aksrt-cover-menu__panel {
+ position: absolute;
+ right: 0;
+ bottom: calc(100% + 8px);
+ display: grid;
+ min-width: 140px;
+ padding: 6px;
+ border-radius: 10px;
+ background: rgba(20, 24, 31, .94);
+ box-shadow: 0 12px 32px rgba(0,0,0,.22);
+ backdrop-filter: blur(12px);
}
-.uc-header__cover-upload i {
- font-size: 11px;
+.aksrt-cover-menu__panel [role="menuitem"] {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 36px;
+ padding: 0 10px;
+ border: 0;
+ border-radius: 6px;
+ color: #fff;
+ background: transparent;
+ font: inherit;
+ font-size: 13px;
+ text-align: left;
+ text-decoration: none;
+ cursor: pointer;
}
+.aksrt-cover-menu__panel [role="menuitem"]:hover,
+.aksrt-cover-menu__panel [role="menuitem"]:focus-visible { background: rgba(255,255,255,.13); outline: none; }
+.profile-form__fields { display: grid; gap: 14px; }
+.profile-form__fields label { display: grid; gap: 6px; font-size: 13px; color: var(--color-ink, #24292f); }
.uc-header__content {
display: flex;
diff --git a/assets/css/routes/user-home.css b/assets/css/routes/user-home.css
index ef1fdbb1..2735a088 100644
--- a/assets/css/routes/user-home.css
+++ b/assets/css/routes/user-home.css
@@ -44,7 +44,7 @@
.aksrt-author-hero__cover-metas {
position: absolute;
- right: 14px;
+ right: 58px;
bottom: 12px;
z-index: 2;
display: flex;
@@ -589,8 +589,27 @@
flex-wrap: wrap;
align-items: center;
gap: 6px;
- margin-top: .35rem;
+ margin-top: .15rem;
}
.aksrt-author-hero__identity .aksrt-ip-geo-badge {
- margin: 2px 0;
+ display: inline-flex;
+ align-items: center;
+ height: 26px;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0 8px;
+ border-radius: 999px;
+ font-size: 12px;
+ line-height: 26px;
}
+.aksrt-author-hero__identity .aksrt-medal-entry,
+.aksrt-author-hero__identity .badg { height: 26px; box-sizing: border-box; margin: 0; padding: 0 8px; border-radius: 999px; font-size: 12px; line-height: 26px; }
+.aksrt-profile-details-modal { position: fixed; inset: 0; z-index: 10001; display: grid; place-items: center; padding: 20px; }
+.aksrt-profile-details-modal__backdrop { position: absolute; inset: 0; background: rgba(10, 15, 22, .52); }
+.aksrt-profile-details-modal__dialog { position: relative; width: min(100%, 420px); padding: 24px; border-radius: 16px; background: var(--color-card, #fff); box-shadow: 0 24px 64px rgba(0,0,0,.24); }
+.aksrt-profile-details-modal__dialog h2 { margin: 0 28px 16px 0; font-size: 18px; }
+.aksrt-profile-details-modal__dialog dl { display: grid; gap: 12px; margin: 0; }
+.aksrt-profile-details-modal__dialog dl div { display: grid; grid-template-columns: 84px 1fr; gap: 12px; }
+.aksrt-profile-details-modal__dialog dt { color: var(--color-muted, #6b7280); }
+.aksrt-profile-details-modal__dialog dd { margin: 0; overflow-wrap: anywhere; }
+.aksrt-profile-details-modal__close { position: absolute; top: 12px; right: 12px; width: 32px; height: 32px; border: 0; border-radius: 50%; background: transparent; font-size: 24px; cursor: pointer; }
diff --git a/assets/js/author.js b/assets/js/author.js
index f42a30de..89db5a0c 100644
--- a/assets/js/author.js
+++ b/assets/js/author.js
@@ -258,8 +258,50 @@
});
}
+ function initCoverMenus() {
+ document.querySelectorAll('[data-aksrt-menu]').forEach(function (menu) {
+ if (menu.dataset.aksrtMenuBound === '1') return;
+ menu.dataset.aksrtMenuBound = '1';
+ var trigger = menu.querySelector('.aksrt-cover-menu__trigger');
+ var panel = menu.querySelector('.aksrt-cover-menu__panel');
+ if (!trigger || !panel) return;
+ function close() { panel.hidden = true; trigger.setAttribute('aria-expanded', 'false'); }
+ function open() {
+ document.querySelectorAll('[data-aksrt-menu]').forEach(function (otherMenu) {
+ if (otherMenu === menu) return;
+ var otherPanel = otherMenu.querySelector('.aksrt-cover-menu__panel');
+ var otherTrigger = otherMenu.querySelector('.aksrt-cover-menu__trigger');
+ if (otherPanel) otherPanel.hidden = true;
+ if (otherTrigger) otherTrigger.setAttribute('aria-expanded', 'false');
+ });
+ panel.hidden = false;
+ trigger.setAttribute('aria-expanded', 'true');
+ var first = panel.querySelector('[role="menuitem"]');
+ if (first) first.focus();
+ }
+ trigger.addEventListener('click', function () { panel.hidden ? open() : close(); });
+ menu.addEventListener('keydown', function (event) { if (event.key === 'Escape') { close(); trigger.focus(); } });
+ document.addEventListener('pointerdown', function (event) { if (!menu.contains(event.target)) close(); });
+ });
+ }
+
+ function initProfileDetailsModal() {
+ var modal = document.getElementById('aksrt-profile-details-modal');
+ if (!modal) return;
+ var dialog = modal.querySelector('.aksrt-profile-details-modal__dialog');
+ var returnFocus = null;
+ function close() { modal.hidden = true; if (returnFocus) returnFocus.focus(); }
+ document.querySelectorAll('[data-aksrt-profile-details]').forEach(function (button) {
+ button.addEventListener('click', function () { returnFocus = button; modal.hidden = false; if (dialog) dialog.focus(); });
+ });
+ modal.querySelectorAll('[data-aksrt-modal-close]').forEach(function (button) { button.addEventListener('click', close); });
+ document.addEventListener('keydown', function (event) { if (event.key === 'Escape' && !modal.hidden) close(); });
+ }
+
function init() {
initPublicDashboards();
+ initCoverMenus();
+ initProfileDetailsModal();
initCoverUpload();
initAvatarUpload();
}
diff --git a/assets/js/front-submit.js b/assets/js/front-submit.js
index 60f3c77b..8769eb2d 100644
--- a/assets/js/front-submit.js
+++ b/assets/js/front-submit.js
@@ -390,27 +390,41 @@
}
},
- openMedia: function (opts, cb) {
- opts = opts || {};
- if (!window.wp || !wp.media) {
- alert('Media library unavailable');
- return;
+ uploadFile: function (kind, file) {
+ var upload = (cfg.uploads || {})[kind] || null;
+ if (!upload || !upload.url || !file) {
+ return Promise.reject(new Error(i18n.error || 'Upload unavailable'));
}
- var frame = wp.media({
- title: opts.title || i18n.pickImage || 'Select',
- button: { text: opts.button || 'OK' },
- multiple: !!opts.multiple,
- library: opts.library || { type: 'image' },
- });
- frame.on('select', function () {
- var selection = frame.state().get('selection');
- var files = selection.map(function (att) {
- var a = att.toJSON();
- return { id: a.id, url: a.url, title: a.title || '' };
+ if (upload.maxSize && file.size > upload.maxSize) {
+ return Promise.reject(new Error('File is too large'));
+ }
+ var body = new FormData();
+ body.append('file', file);
+ return fetch(upload.url, {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: { 'X-WP-Nonce': cfg.restNonce || '' },
+ body: body,
+ }).then(function (response) {
+ return response.json().catch(function () { return {}; }).then(function (payload) {
+ if (!response.ok || !payload || !payload.success) {
+ throw new Error((payload && payload.message) || i18n.error || 'Upload failed');
+ }
+ return payload;
});
- cb(opts.multiple ? files : files[0]);
});
- frame.open();
+ },
+
+ pickUpload: function (kind, accept) {
+ return new Promise(function (resolve) {
+ var input = document.createElement('input');
+ input.type = 'file';
+ input.accept = accept;
+ input.addEventListener('change', function () {
+ resolve(input.files && input.files[0] ? input.files[0] : null);
+ }, { once: true });
+ input.click();
+ });
},
bindFeatured: function () {
@@ -431,24 +445,21 @@
return;
}
if (act === 'image') {
- self.openMedia({ title: i18n.pickImage, multiple: false }, function (file) {
- if (file) {
- self.setFeatured('image', { url: file.url, id: file.id });
- }
- });
+ self.pickUpload('image', 'image/*').then(function (file) {
+ if (!file) return null;
+ return self.uploadFile('image', file);
+ }).then(function (file) {
+ if (file) self.setFeatured('image', { url: file.url, id: file.attachment_id });
+ }).catch(function (error) { alert(error.message || i18n.error || 'Upload failed'); });
return;
}
if (act === 'slide') {
- self.openMedia({ title: i18n.pickImage, multiple: true }, function (files) {
- if (files && files.length) {
- self.setFeatured(
- 'slide',
- files.map(function (f) {
- return { url: f.url, id: f.id };
- })
- );
- }
- });
+ self.pickUpload('image', 'image/*').then(function (file) {
+ if (!file) return null;
+ return self.uploadFile('image', file);
+ }).then(function (file) {
+ if (file) self.setFeatured('slide', [{ url: file.url, id: file.attachment_id }]);
+ }).catch(function (error) { alert(error.message || i18n.error || 'Upload failed'); });
return;
}
if (act === 'video') {
@@ -456,9 +467,12 @@
if (!url) {
return;
}
- self.openMedia({ title: i18n.pickVideo || 'Poster', multiple: false }, function (file) {
+ self.pickUpload('image', 'image/*').then(function (file) {
+ if (!file) return null;
+ return self.uploadFile('image', file);
+ }).then(function (file) {
self.setFeatured('video', { url: url, pic: file ? file.url : '' });
- });
+ }).catch(function (error) { alert(error.message || i18n.error || 'Upload failed'); });
}
});
},
@@ -520,6 +534,32 @@
list.appendChild(node);
});
}
+ var uploadBtn = qs('[data-pay-download-upload]', box);
+ if (uploadBtn && list && tpl) {
+ uploadBtn.addEventListener('click', function () {
+ App.pickUpload('resource', '*/*').then(function (file) {
+ if (!file) return null;
+ uploadBtn.disabled = true;
+ return App.uploadFile('resource', file);
+ }).then(function (file) {
+ if (!file) return;
+ var node = tpl.content.cloneNode(true);
+ var idx = list.querySelectorAll('[data-pay-download-item]').length;
+ qsa('[data-name]', node).forEach(function (input) {
+ input.name = 'aksrt_pay[downloads][' + idx + '][' + input.getAttribute('data-name') + ']';
+ });
+ var url = qs('[data-pay-download-url]', node);
+ var label = qs('[data-pay-download-label]', node);
+ if (url) url.value = file.url || '';
+ if (label) label.value = file.name || '';
+ list.appendChild(node);
+ }).catch(function (error) {
+ alert(error.message || i18n.error || 'Upload failed');
+ }).finally(function () {
+ uploadBtn.disabled = false;
+ });
+ });
+ }
if (list) {
list.addEventListener('click', function (e) {
var rm = e.target.closest('[data-pay-download-remove]');
@@ -541,6 +581,15 @@
return;
}
e.preventDefault();
+ if (btn.hasAttribute('data-insert-upload-image')) {
+ App.pickUpload('image', 'image/*').then(function (file) {
+ if (!file) return null;
+ return App.uploadFile('image', file);
+ }).then(function (file) {
+ if (file && file.url) insertEditorContent('
');
+ }).catch(function (error) { alert(error.message || i18n.error || 'Upload failed'); });
+ return;
+ }
var type = btn.getAttribute('data-insert-shortcode');
if (type === 'hide') {
insertEditorContent('[hidecontent type="login"]' + (i18n.hidePlaceholder || '隐藏内容') + '[/hidecontent]');
diff --git a/assets/js/routes/user-center.js b/assets/js/routes/user-center.js
index ce9bdd4c..5b97001e 100644
--- a/assets/js/routes/user-center.js
+++ b/assets/js/routes/user-center.js
@@ -194,40 +194,6 @@
var form = document.getElementById('profile-form');
if (!form) return;
- var iconOptions = ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + ''
- + '';
-
- var addBtn = document.getElementById('add-contact-btn');
- if (addBtn) {
- addBtn.addEventListener('click', function() {
- var list = document.getElementById('contacts-list');
- var idx = list.children.length;
- var div = document.createElement('div');
- div.className = 'contact-row';
- div.dataset.index = idx;
- div.innerHTML =
- '' +
- '' +
- '' +
- '';
- list.appendChild(div);
- });
- }
-
form.addEventListener('submit', function(e) {
e.preventDefault();
var btn = document.getElementById('profile-save-btn');
diff --git a/inc/helpers/aksrt-avatar.php b/inc/helpers/aksrt-avatar.php
index fb2377ce..3831c891 100644
--- a/inc/helpers/aksrt-avatar.php
+++ b/inc/helpers/aksrt-avatar.php
@@ -149,8 +149,7 @@ if (!function_exists('aksrt_is_avatar_upload_enabled')) {
* Whether local avatar uploads are enabled by attachment policy.
*/
function aksrt_is_avatar_upload_enabled(): bool {
- $settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
- return !isset($settings['allow_avatar_upload']) || !empty($settings['allow_avatar_upload']);
+ return function_exists('aksrt_user_can') && aksrt_user_can('upload_avatar', get_current_user_id());
}
}
diff --git a/inc/helpers/aksrt-comments.php b/inc/helpers/aksrt-comments.php
index 2f9bdf1a..5365bcc7 100644
--- a/inc/helpers/aksrt-comments.php
+++ b/inc/helpers/aksrt-comments.php
@@ -76,7 +76,6 @@ if (!function_exists('aksrt_comments_build_context')) :
'order' => 'desc',
'nested_depth' => 3,
'require_login' => false,
- 'allow_reply' => true,
'avatar_size' => 48,
'empty_text' => __('暂无评论,来做第一个评论的人吧~', 'aksrt'),
];
@@ -109,7 +108,6 @@ if (!function_exists('aksrt_comments_build_context')) :
'post_author_id' => (int) get_the_author_meta('ID'),
'avatar_size' => $avatar_size,
'max_depth' => max(1, (int) ($settings['nested_depth'] ?? 3)),
- 'allow_reply' => !empty($settings['allow_reply']),
'empty_text' => (string) ($settings['empty_text'] ?? __('暂无评论,来做第一个评论的人吧~', 'aksrt')),
'toolbar' => $toolbar,
'upload_enabled' => !empty($toolbar['upload_img']),
diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php
index adcb9ec0..1001bb35 100644
--- a/inc/helpers/aksrt-pay.php
+++ b/inc/helpers/aksrt-pay.php
@@ -775,13 +775,8 @@ if (!function_exists('aksrt_can_download')) {
return new WP_Error('aksrt_not_downloadable', __('该内容不是可下载资源。', 'aksrt'));
}
- $paid_settings = class_exists('\AKSRT\Settings\Settings')
- ? \AKSRT\Settings\Settings::get_paid_content_settings()
- : ['download_login' => true];
-
$uid = $user_id > 0 ? $user_id : get_current_user_id();
-
- if (!empty($paid_settings['download_login']) && $uid <= 0) {
+ if (!function_exists('aksrt_user_can') || !aksrt_user_can('resource_download', $uid, (int) $p->ID)) {
$login_url = wp_login_url(get_permalink($p));
return new WP_Error('aksrt_need_login', __('请先登录。', 'aksrt'), ['login_url' => $login_url]);
}
diff --git a/inc/helpers/aksrt-resource.php b/inc/helpers/aksrt-resource.php
index 42498fa2..337ce591 100644
--- a/inc/helpers/aksrt-resource.php
+++ b/inc/helpers/aksrt-resource.php
@@ -403,11 +403,7 @@ if (!function_exists('aksrt_can_download')) {
}
$uid = $user_id > 0 ? $user_id : get_current_user_id();
- $paid_settings = class_exists('\AKSRT\Settings\Settings')
- ? \AKSRT\Settings\Settings::get_paid_content_settings()
- : ['download_login' => true];
-
- if (!empty($paid_settings['download_login']) && $uid <= 0) {
+ if (!function_exists('aksrt_user_can') || !aksrt_user_can('resource_download', $uid, (int) $p->ID)) {
return new WP_Error('aksrt_need_login', __('请先登录后再下载。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($p)),
diff --git a/inc/helpers/aksrt-user-center.php b/inc/helpers/aksrt-user-center.php
index 46b390ff..2bb65e36 100644
--- a/inc/helpers/aksrt-user-center.php
+++ b/inc/helpers/aksrt-user-center.php
@@ -7,41 +7,48 @@
defined('ABSPATH') || exit;
-if (!function_exists('aksrt_normalize_user_contacts')) {
+if (!function_exists('aksrt_get_user_profile_details')) {
/**
- * Sanitize the editable public contact list.
+ * Read the fixed profile details. Social fields are deliberately kept in one
+ * structured meta value so public rendering can filter them server-side.
*
- * @param mixed $raw_contacts Submitted contact rows.
- * @return array
+ * @return array{privacy:string,gender:string,website:string,qq:string,wechat:string,github:string}
*/
- function aksrt_normalize_user_contacts($raw_contacts): array {
- $contacts = [];
- if (!is_array($raw_contacts)) {
- return $contacts;
+ function aksrt_get_user_profile_details(int $user_id): array {
+ $raw = get_user_meta($user_id, 'aksrt_profile_details', true);
+ $raw = is_array($raw) ? $raw : [];
+ $privacy = (string) ($raw['privacy'] ?? 'private');
+ if (!in_array($privacy, ['private', 'public', 'members'], true)) {
+ $privacy = 'private';
+ }
+ $gender = (string) ($raw['gender'] ?? 'secret');
+ if (!in_array($gender, ['secret', 'male', 'female'], true)) {
+ $gender = 'secret';
}
- foreach ($raw_contacts as $contact) {
- if (!is_array($contact)) {
- continue;
- }
+ return [
+ 'privacy' => $privacy,
+ 'gender' => $gender,
+ 'website' => esc_url_raw((string) ($raw['website'] ?? '')),
+ 'qq' => sanitize_text_field((string) ($raw['qq'] ?? '')),
+ 'wechat' => sanitize_text_field((string) ($raw['wechat'] ?? '')),
+ 'github' => esc_url_raw((string) ($raw['github'] ?? '')),
+ ];
+ }
+}
- $url = isset($contact['url']) ? esc_url_raw(wp_unslash($contact['url'])) : '';
- $icon = isset($contact['icon']) ? sanitize_key(wp_unslash($contact['icon'])) : '';
- $icon = preg_replace('/^(fab|fas|far)[\s-]*/', '', $icon);
- $icon = preg_replace('/^fa[\s-]*/', '', (string) $icon);
- $icon = trim(preg_replace('/^[\s-]+/', '', (string) $icon), '- ');
- if (!$url || !$icon) {
- continue;
- }
-
- $contacts[] = [
- 'name' => aksrt_icon_key_to_name($icon),
- 'url' => $url,
- 'icon' => $icon,
- ];
- }
-
- return $contacts;
+if (!function_exists('aksrt_get_visible_user_profile_details')) {
+ /** @return array{gender:string,website:string,qq:string,wechat:string,github:string} */
+ function aksrt_get_visible_user_profile_details(int $user_id, int $viewer_id = 0): array {
+ $details = aksrt_get_user_profile_details($user_id);
+ $visible = $viewer_id === $user_id || $details['privacy'] === 'public' || ($details['privacy'] === 'members' && $viewer_id > 0);
+ return [
+ 'gender' => $details['gender'] === 'secret' ? '' : $details['gender'],
+ 'website' => $visible ? $details['website'] : '',
+ 'qq' => $visible ? $details['qq'] : '',
+ 'wechat' => $visible ? $details['wechat'] : '',
+ 'github' => $visible ? $details['github'] : '',
+ ];
}
}
@@ -79,6 +86,30 @@ if (!function_exists('aksrt_save_user_center_profile')) {
}
}
+ $privacy = isset($input['privacy']) ? sanitize_key(wp_unslash($input['privacy'])) : 'private';
+ if (!in_array($privacy, ['private', 'public', 'members'], true)) {
+ $errors[] = __('无效的隐私设置。', 'aksrt');
+ }
+ $gender = isset($input['gender']) ? sanitize_key(wp_unslash($input['gender'])) : 'secret';
+ if (!in_array($gender, ['secret', 'male', 'female'], true)) {
+ $errors[] = __('无效的性别设置。', 'aksrt');
+ }
+ $details = [
+ 'privacy' => $privacy,
+ 'gender' => $gender,
+ 'website' => isset($input['website']) ? esc_url_raw(wp_unslash($input['website'])) : '',
+ 'qq' => isset($input['qq']) ? sanitize_text_field(wp_unslash($input['qq'])) : '',
+ 'wechat' => isset($input['wechat']) ? sanitize_text_field(wp_unslash($input['wechat'])) : '',
+ 'github' => isset($input['github']) ? esc_url_raw(wp_unslash($input['github'])) : '',
+ ];
+ foreach (['website', 'github'] as $field) {
+ if (!empty($input[$field]) && $details[$field] === '') {
+ $errors[] = sprintf(__('无效的%s链接。', 'aksrt'), $field === 'website' ? __('个人网站', 'aksrt') : 'GitHub');
+ }
+ }
+ if (mb_strlen($details['qq']) > 50 || mb_strlen($details['wechat']) > 100) {
+ $errors[] = __('社交资料长度超过限制。', 'aksrt');
+ }
if ($errors) {
return new WP_Error('aksrt_profile_invalid', implode('
', $errors));
}
@@ -89,8 +120,7 @@ if (!function_exists('aksrt_save_user_center_profile')) {
return $result;
}
}
-
- update_user_meta($user_id, 'aksrt_user_contacts', aksrt_normalize_user_contacts($input['contacts'] ?? []));
+ update_user_meta($user_id, 'aksrt_profile_details', $details);
return true;
@@ -388,6 +418,8 @@ if (!function_exists('aksrt_user_center_cover_upload_config')) {
if (!$allow_bg_upload) {
return [];
}
+ $settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
+ $max_size = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2))) * MB_IN_BYTES;
return [
'enabled' => true,
'restUrl' => rest_url('aksrt/v1/user/background'),
@@ -396,11 +428,11 @@ if (!function_exists('aksrt_user_center_cover_upload_config')) {
'inputId' => 'aksrt-bg-file-input',
'imageId' => 'aksrt-cover-img',
'fieldName' => 'background',
- 'maxSize' => 2097152,
+ 'maxSize' => $max_size,
'fallbackImage' => AKSRT_URI . '/assets/img/person_background.png',
- 'idleText' => __('编辑', 'aksrt'),
+ 'idleText' => __('修改背景图', 'aksrt'),
'loadingText' => __('上传中…', 'aksrt'),
- 'tooLargeText' => __('图片大小不能超过 2MB', 'aksrt'),
+ 'tooLargeText' => sprintf(__('图片大小不能超过 %dMB', 'aksrt'), (int) ($max_size / MB_IN_BYTES)),
'successText' => __('背景图已更新', 'aksrt'),
'errorText' => __('上传失败', 'aksrt'),
'networkErrorText' => __('网络错误,请重试', 'aksrt'),
@@ -418,10 +450,7 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
$user_center_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_user_center_settings()
: [
- 'tabs' => ['profile', 'comments', 'favorites'],
- 'avatar_upload' => true,
- 'cover_upload' => true,
- 'social_links' => true,
+ 'tabs' => ['profile', 'comments', 'favorites'],
];
$tabs = aksrt_user_center_resolve_tabs();
@@ -439,11 +468,9 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
$comment_count = (int) ($dashboard['comment_count'] ?? get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']));
$favorite_post_ids = $dashboard['favorite_post_ids'] ?? [];
$favorite_count = (int) ($dashboard['favorite_count'] ?? count($favorite_post_ids));
- $user_contacts = $dashboard['user_contacts'] ?? get_user_meta($user_id, 'aksrt_user_contacts', true);
- $user_contacts = is_array($user_contacts) ? $user_contacts : [];
$user_background_url = (string) ($dashboard['background_url'] ?? get_user_meta($user_id, 'aksrt_background_image', true));
- $allow_avatar_upload = !empty($user_center_settings['avatar_upload']);
- $allow_bg_upload = !empty($user_center_settings['cover_upload']) && class_exists('\AKSRT\Settings\Settings') && aksrt_get_setting('allow_background_upload', false);
+ $allow_avatar_upload = function_exists('aksrt_user_can') && aksrt_user_can('upload_avatar', $user_id);
+ $allow_bg_upload = function_exists('aksrt_user_can') && aksrt_user_can('upload_profile_background', $user_id);
$messages_enabled = !class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::message_enabled();
$follower_count = class_exists('\AKSRT\User\Follow')
? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id)
@@ -501,7 +528,7 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
'favorite_count' => $favorite_count,
'follower_count' => $follower_count,
'following_count' => $following_count,
- 'user_contacts' => $user_contacts,
+ 'profile_details' => aksrt_get_user_profile_details($user_id),
'profile_signals' => $dashboard['profile_signals'] ?? [],
'user_background_url' => $user_background_url,
'allow_bg_upload' => $allow_bg_upload,
diff --git a/inc/helpers/aksrt-user-dashboard.php b/inc/helpers/aksrt-user-dashboard.php
index fbaa85ba..17b06340 100644
--- a/inc/helpers/aksrt-user-dashboard.php
+++ b/inc/helpers/aksrt-user-dashboard.php
@@ -23,7 +23,7 @@ defined('ABSPATH') || exit;
* @type int $comment_count Approved comment count.
* @type int $favorite_count Favorited post count.
* @type array $favorite_post_ids Array of favorited post IDs.
- * @type array $user_contacts Social/contact links from user meta.
+ * @type array $profile_details Server-filtered fixed public profile fields.
* @type array $profile_signals Identity chips (verified, level, badges).
* @type array $profile_notice Ban/restriction notice, if any.
* @type string $background_url Cover image URL.
@@ -62,14 +62,9 @@ function aksrt_get_user_dashboard_view(int $user_id, array $args = []): array {
])
: [];
- $user_contacts = get_user_meta($user_id, 'aksrt_user_contacts', true) ?: [];
- if (!is_array($user_contacts)) {
- $user_contacts = [];
- }
-
- $user_center_settings = class_exists('\AKSRT\Settings\Settings')
- ? \AKSRT\Settings\Settings::get_user_center_settings()
- : ['social_links' => true];
+ $profile_details = function_exists('aksrt_get_visible_user_profile_details')
+ ? aksrt_get_visible_user_profile_details($user_id, $viewer_id)
+ : ['gender' => '', 'website' => '', 'qq' => '', 'wechat' => '', 'github' => ''];
$background_url = get_user_meta($user_id, 'aksrt_background_image', true) ?: '';
@@ -98,8 +93,7 @@ function aksrt_get_user_dashboard_view(int $user_id, array $args = []): array {
'favorite_count' => $favorite_count,
'favorite_post_ids'=> $favorite_post_ids,
'recent_posts' => $recent_posts,
- 'user_contacts' => $user_contacts,
- 'social_links_on' => !empty($user_center_settings['social_links']),
+ 'profile_details' => $profile_details,
'profile_signals' => $profile_signals,
'profile_notice' => $profile_notice,
'show_report' => $show_report,
diff --git a/inc/helpers/aksrt-user-home.php b/inc/helpers/aksrt-user-home.php
index 49c2fb25..eb8d7023 100644
--- a/inc/helpers/aksrt-user-home.php
+++ b/inc/helpers/aksrt-user-home.php
@@ -34,15 +34,11 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
'posts_per_page' => 10,
];
- $user_center_settings = class_exists('\AKSRT\Settings\Settings')
- ? \AKSRT\Settings\Settings::get_user_center_settings()
- : ['social_links' => true];
-
$post_count = (int) ($dashboard['post_count'] ?? 0);
$comment_count = (int) ($dashboard['comment_count'] ?? 0);
$favorite_post_ids = $dashboard['favorite_post_ids'] ?? [];
$favorite_count = (int) ($dashboard['favorite_count'] ?? 0);
- $user_contacts = $dashboard['user_contacts'] ?? [];
+ $profile_details = $dashboard['profile_details'] ?? [];
$per_page = max(1, (int) ($user_home_settings['posts_per_page'] ?? 10));
$credits_enabled = !empty($dashboard['credits_enabled']);
$credit_balance = $dashboard['credit_balance'] ?? 0.0;
@@ -118,8 +114,7 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
'dashboard' => $dashboard,
'is_own_profile' => $is_own_profile,
'user_home_settings' => $user_home_settings,
- 'user_center_settings' => $user_center_settings,
- 'user_contacts' => $user_contacts,
+ 'profile_details' => $profile_details,
'post_count' => $post_count,
'comment_count' => $comment_count,
'favorite_post_ids' => $favorite_post_ids,
diff --git a/inc/helpers/comments/item.php b/inc/helpers/comments/item.php
index 31e3b28c..43d1e513 100644
--- a/inc/helpers/comments/item.php
+++ b/inc/helpers/comments/item.php
@@ -151,7 +151,6 @@ if (!function_exists('aksrt_get_comment_footer_html')) :
*/
function aksrt_get_comment_footer_html(WP_Comment $comment, array $args, int $depth, array $settings = []): string {
$is_pending = '0' === (string) $comment->comment_approved;
- $allow_reply = !empty($settings['allow_reply']);
// Time ago.
$absolute = date_i18n('Y-m-d H:i:s', strtotime((string) $comment->comment_date));
@@ -181,7 +180,7 @@ if (!function_exists('aksrt_get_comment_footer_html')) :
// Reply.
$reply_html = '';
- if ($allow_reply && !$is_pending && $depth > 0) {
+ if (!$is_pending && $depth > 0) {
$max_depth = (int) ($args['max_depth'] ?? get_option('thread_comments_depth', 5));
$link = get_comment_reply_link(
array_merge(
diff --git a/inc/helpers/user-center-renderers/form-contacts.php b/inc/helpers/user-center-renderers/form-contacts.php
deleted file mode 100644
index 6fbe0485..00000000
--- a/inc/helpers/user-center-renderers/form-contacts.php
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
diff --git a/inc/helpers/user-center-renderers/profile-form.php b/inc/helpers/user-center-renderers/profile-form.php
index 96b3088f..d694fd59 100644
--- a/inc/helpers/user-center-renderers/profile-form.php
+++ b/inc/helpers/user-center-renderers/profile-form.php
@@ -9,6 +9,7 @@ defined('ABSPATH') || exit;
$context = is_array($args['context'] ?? null) ? $args['context'] : [];
extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
+$profile_details = isset($profile_details) && is_array($profile_details) ? $profile_details : aksrt_get_user_profile_details((int) $user_id);
?>
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inc/helpers/user-center-renderers/profile-header.php b/inc/helpers/user-center-renderers/profile-header.php
index e41d298a..f28ab6e9 100644
--- a/inc/helpers/user-center-renderers/profile-header.php
+++ b/inc/helpers/user-center-renderers/profile-header.php
@@ -14,13 +14,18 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
diff --git a/src/Admin/ExtensionController.php b/src/Admin/ExtensionController.php
index dd77b3fe..919b20a1 100644
--- a/src/Admin/ExtensionController.php
+++ b/src/Admin/ExtensionController.php
@@ -183,8 +183,8 @@ class ExtensionController extends ControllerBase {
'provider' => (string) ($captcha['provider'] ?? ''),
'configured' => !empty($captcha['configured']),
'scenes' => $scenes,
- 'download_required' => !empty($features['download_captcha']),
- 'download_active' => !empty($features['download_captcha']) && !empty($scenes['download']),
+ 'download_required' => !empty($scenes['download']),
+ 'download_active' => !empty($scenes['download']),
],
'content_audit' => [
'enabled' => !empty($services['audit']['enabled']),
diff --git a/src/Admin/GlobalController.php b/src/Admin/GlobalController.php
index 81813d42..c55ec940 100644
--- a/src/Admin/GlobalController.php
+++ b/src/Admin/GlobalController.php
@@ -182,9 +182,7 @@ class GlobalController extends ControllerBase {
$params = $this->get_json_params($request);
$allowed = [
'resource_cpt_enabled' => 'bool',
- 'download_login' => 'bool',
'download_credit' => 'bool',
- 'download_captcha' => 'bool',
'search_include_resource' => 'bool',
];
$sanitized = $this->sanitize_fields($params, $allowed);
diff --git a/src/Admin/PermissionsController.php b/src/Admin/PermissionsController.php
index ace31228..ff9e80e4 100644
--- a/src/Admin/PermissionsController.php
+++ b/src/Admin/PermissionsController.php
@@ -46,7 +46,7 @@ class PermissionsController extends ControllerBase {
'groups' => $this->build_capability_groups($defs, $caps, $columns),
'summary' => [
'cap_count' => count($defs),
- 'group_count' => 3,
+ 'group_count' => 5,
'column_count' => count($columns),
],
],
@@ -114,6 +114,16 @@ class PermissionsController extends ControllerBase {
'title' => __('评论权限', 'aksrt'),
'items' => [],
],
+ 'upload' => [
+ 'key' => 'upload',
+ 'title' => __('上传权限', 'aksrt'),
+ 'items' => [],
+ ],
+ 'download' => [
+ 'key' => 'download',
+ 'title' => __('下载权限', 'aksrt'),
+ 'items' => [],
+ ],
];
foreach ($defs as $cap_id => $def) {
@@ -122,10 +132,13 @@ class PermissionsController extends ControllerBase {
$bucket_key = 'comment';
} elseif (str_starts_with($cap_id, 'new_post_')) {
$bucket_key = 'post';
+ } elseif (str_starts_with($cap_id, 'upload_')) {
+ $bucket_key = 'upload';
+ } elseif (str_starts_with($cap_id, 'resource_')) {
+ $bucket_key = 'download';
}
$excludes = $def['exclude_roles'] ?? [];
- $only_roles = $def['roles'] ?? null;
$values = $caps[$cap_id] ?? [];
$visible_columns = [];
@@ -133,9 +146,6 @@ class PermissionsController extends ControllerBase {
if (in_array($column, $excludes, true)) {
continue;
}
- if ($only_roles !== null && !in_array($column, $only_roles, true) && !in_array($column, ['all', 'logged', 'level', 'vip', 'auth'], true)) {
- continue;
- }
$visible_columns[] = $column;
}
@@ -157,9 +167,6 @@ class PermissionsController extends ControllerBase {
'level' => __('等级≥', 'aksrt'),
'vip' => __('VIP≥', 'aksrt'),
'auth' => __('已认证', 'aksrt'),
- 'moderator' => __('版主', 'aksrt'),
- 'plate_author' => __('超级版主', 'aksrt'),
- 'cat_moderator' => __('分区版主', 'aksrt'),
];
}
}
diff --git a/src/Admin/Schema/SchemaPayment.php b/src/Admin/Schema/SchemaPayment.php
index e4723736..9b137a4b 100644
--- a/src/Admin/Schema/SchemaPayment.php
+++ b/src/Admin/Schema/SchemaPayment.php
@@ -40,7 +40,6 @@ class SchemaPayment {
['label' => '正文底部', 'value' => 'bottom'],
['label' => '文章框底部', 'value' => 'box_bottom'],
]],
- ['key' => 'pay_free_logged_show', 'label' => '免费资源需登录后下载', 'type' => 'switch', 'default' => false],
]],
['key' => 'download', 'title' => '下载能力', 'fields' => [
['key' => 'payment_download_enable', 'label' => '启用下载能力', 'type' => 'switch', 'default' => false],
@@ -308,7 +307,6 @@ class SchemaPayment {
['label' => '正文底部', 'value' => 'bottom'],
['label' => '文章框底部', 'value' => 'box_bottom'],
]],
- ['key' => 'pay_free_logged_show', 'label' => '登录用户显示免费', 'type' => 'switch', 'default' => false],
['key' => 'payment_show_ip', 'label' => '显示用户IP', 'type' => 'switch', 'default' => false],
['key' => 'payment_show_device', 'label' => '显示设备信息', 'type' => 'switch', 'default' => false],
['key' => 'payment_show_order_no', 'label' => '显示订单号', 'type' => 'switch', 'default' => true],
diff --git a/src/Admin/Schema/SchemaPermissions.php b/src/Admin/Schema/SchemaPermissions.php
index b33360fd..cc6f8011 100644
--- a/src/Admin/Schema/SchemaPermissions.php
+++ b/src/Admin/Schema/SchemaPermissions.php
@@ -11,9 +11,9 @@ class SchemaPermissions {
public static function build(): array {
return [
- 'domain' => 'permissions',
- 'title' => '功能&权限',
- 'sections' => [
+ 'domain' => 'permissions',
+ 'title' => '功能与权限',
+ 'sections' => [
'login' => [
'key' => 'login',
'title' => '人机验证',
@@ -54,94 +54,24 @@ class SchemaPermissions {
]],
],
],
- 'comment' => [
- 'key' => 'comment',
- 'title' => '评论权限',
- 'version' => '1.0.0',
- 'option_key' => 'aksrt_settings',
- 'groups' => [
- ['key' => 'basic', 'title' => '基础权限', 'fields' => [
- ['key' => 'comment_enabled', 'label' => '全局评论开关', 'type' => 'switch', 'default' => true, 'help' => '关闭后全站禁用评论功能'],
- ['key' => 'comment_guest_allowed', 'label' => '允许游客评论', 'type' => 'switch', 'default' => true],
- ['key' => 'comment_min_length', 'label' => '评论最少字数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 100],
- ['key' => 'comment_max_length', 'label' => '评论最多字数', 'type' => 'number', 'default' => 1000, 'min' => 50, 'max' => 10000],
- ['key' => 'comment_cool_down_seconds', 'label' => '评论冷却时间(秒)', 'type' => 'number', 'default' => 15, 'min' => 0, 'max' => 300],
- ['key' => 'comment_max_links', 'label' => '最大链接数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 10],
- ['key' => 'comment_max_images', 'label' => '最大图片数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 20],
- ]],
- ['key' => 'sensitive', 'title' => '敏感词', 'fields' => [
- ['key' => 'comment_sensitive_words', 'label' => '敏感词列表', 'type' => 'textarea', 'default' => ''],
- ['key' => 'comment_sensitive_action', 'label' => '敏感词处理', 'type' => 'radio', 'default' => 'reject', 'options' => [
- ['label' => '拒绝评论', 'value' => 'reject'],
- ['label' => '替换为***', 'value' => 'replace'],
- ['label' => '转入审核', 'value' => 'pending'],
- ]],
- ['key' => 'comment_sensitive_log', 'label' => '记录敏感词日志', 'type' => 'switch', 'default' => true],
- ]],
- ['key' => 'anti-spam', 'title' => '反垃圾', 'fields' => [
- ['key' => 'comment_link_filter', 'label' => '链接过滤', 'type' => 'switch', 'default' => true, 'help' => '评论包含链接自动进入审核'],
- ['key' => 'comment_flood_protection', 'label' => '防刷评论', 'type' => 'switch', 'default' => true],
- ['key' => 'comment_flood_interval', 'label' => '防刷间隔(秒)', 'type' => 'number', 'default' => 10, 'min' => 3, 'max' => 60],
- ['key' => 'comment_max_per_hour', 'label' => '每小时最大评论数', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
- ]],
- ['key' => 'display', 'title' => '展示设置', 'fields' => [
- ['key' => 'comment_avatar_required', 'label' => '要求设置头像', 'type' => 'switch', 'default' => false],
- ['key' => 'comment_nickname_required', 'label' => '要求填写昵称', 'type' => 'switch', 'default' => true],
- ['key' => 'comment_email_required', 'label' => '要求填写邮箱', 'type' => 'switch', 'default' => true],
- ]],
- ],
- ],
'attachment' => [
'key' => 'attachment',
- 'title' => '附件权限',
- 'version' => '1.0.0',
+ 'title' => '附件配置',
+ 'version' => '1.1.0',
'option_key' => 'aksrt_settings',
'groups' => [
- ['key' => 'profile', 'title' => '头像与封面', 'fields' => [
- ['key' => 'allow_avatar_upload', 'label' => '允许用户上传头像', 'type' => 'switch', 'default' => true],
- ['key' => 'allow_cover_upload', 'label' => '允许用户上传封面图', 'type' => 'switch', 'default' => true],
- ['key' => 'allow_background_upload', 'label' => '允许用户上传背景图', 'type' => 'switch', 'default' => false],
- ['key' => 'avatar_max_size', 'label' => '头像/封面大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
+ ['key' => 'profile', 'title' => '用户资料卡背景图', 'fields' => [
+ ['key' => 'avatar_max_size', 'label' => '头像/用户资料卡背景图大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
]],
- ['key' => 'content', 'title' => '内容上传', 'fields' => [
- ['key' => 'allow_submit_images', 'label' => '允许投稿插入图片', 'type' => 'switch', 'default' => true],
+ ['key' => 'content', 'title' => '内容上传限制', 'fields' => [
['key' => 'submit_image_max_size', 'label' => '投稿图片大小上限 (MB)', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 20],
- ['key' => 'allow_comment_images', 'label' => '允许评论插入图片', 'type' => 'switch', 'default' => false],
['key' => 'comment_image_max_size', 'label' => '评论图片大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
]],
- ['key' => 'resource', 'title' => '资源附件', 'fields' => [
- ['key' => 'allow_resource_upload', 'label' => '允许资源附件上传', 'type' => 'switch', 'default' => true],
+ ['key' => 'resource', 'title' => '资源附件限制', 'fields' => [
['key' => 'resource_max_size', 'label' => '资源附件大小上限 (MB)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 200],
]],
],
],
- 'download' => [
- 'key' => 'download',
- 'title' => '下载权限',
- 'version' => '1.0.0',
- 'option_key' => 'aksrt_settings',
- 'groups' => [
- ['key' => 'basic', 'title' => '下载设置', 'fields' => [
- ['key' => 'download_require_login', 'label' => '下载需要登录', 'type' => 'switch', 'default' => true],
- ['key' => 'download_require_credits', 'label' => '允许积分购买模式', 'type' => 'switch', 'default' => false, 'help' => '兼容字段;运行时以 cms_features.download_credit 为准。开启表示 pay_modo=积分 可收银台一次购买,不是按次扣费。'],
- ['key' => 'download_credits_cost', 'label' => '默认积分价参考', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100, 'help' => '仅运营参考/兼容;实际价格以文章付费设置为准。'],
- ['key' => 'download_daily_limit', 'label' => '每日下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为不限制'],
- ['key' => 'download_link_expire', 'label' => '下载链接有效期(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
- ]],
- ['key' => 'vip', 'title' => 'VIP 特权', 'fields' => [
- ['key' => 'download_vip_free', 'label' => 'VIP 免费下载', 'type' => 'switch', 'default' => false],
- ['key' => 'download_vip_daily_extra', 'label' => 'VIP每日额外次数', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 999],
- ]],
- ['key' => 'role', 'title' => '角色权限', 'fields' => [
- ['key' => 'download_role_limits', 'label' => '角色下载限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|daily_limit,每行一条'],
- ['key' => 'download_role_size', 'label' => '角色文件大小限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|max_size_mb,每行一条'],
- ]],
- ['key' => 'captcha', 'title' => '安全设置', 'fields' => [
- ['key' => 'download_captcha_hint', 'label' => '下载验证码', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '下载验证码门槛在「功能&权限 → 下载权限」页(cms_features.download_captcha)维护;服务商与场景在「人机验证」。'],
- ['key' => 'download_ip_limit', 'label' => 'IP下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '同一IP每小时最大下载次数,0为不限制'],
- ]],
- ],
- ],
'moderation' => [
'key' => 'moderation',
'title' => '内容审核',
@@ -159,9 +89,9 @@ class SchemaPermissions {
]],
['key' => 'ai', 'title' => 'AI 审核', 'fields' => [
['key' => 'ai_enabled', 'label' => '启用 AI 审核', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'enabled', 'value' => true]],
- ['key' => 'ai_api_hint', 'label' => '审核 API 配置', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => 'API 接入(百度/阿里云等)在「扩展 → API内容审核」中统一配置,此处仅控制审核范围和策略。', 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
+ ['key' => 'ai_api_hint', 'label' => '审核 API 配置', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => 'API 接入在「扩展 → API 内容审核」中统一配置;此处仅控制审核范围和策略。', 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'ai_moderate_comments', 'label' => '用户评论', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
- ['key' => 'ai_moderate_background', 'label' => '用户资料背景', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
+ ['key' => 'ai_moderate_background', 'label' => '用户资料卡背景图', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'auto_approve_low_risk', 'label' => '低风险自动通过', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'auto_reject_high_risk', 'label' => '高风险自动拒绝', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'low_risk_max_score', 'label' => '低风险上限', 'type' => 'number', 'default' => 35, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
@@ -175,21 +105,13 @@ class SchemaPermissions {
['key' => 'rate_limit_min_interval', 'label' => '最小间隔(秒)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 300, 'dependsOn' => ['key' => 'rate_limit_enabled', 'value' => true]],
['key' => 'rate_limit_max_per_hour', 'label' => '每小时上限', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 200, 'dependsOn' => ['key' => 'rate_limit_enabled', 'value' => true]],
]],
- ['key' => 'geoip', 'title' => 'GeoIP 拦截', 'fields' => [
- ['key' => 'geoip_enabled', 'label' => '启用 GeoIP 拦截', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'enabled', 'value' => true]],
- ['key' => 'geoip_hint', 'label' => 'IP 数据源', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '复用「扩展 → IP归属地」中配置的数据源和 API Key。', 'dependsOn' => ['key' => 'geoip_enabled', 'value' => true]],
- ['key' => 'geoip_block_countries', 'label' => '屏蔽国家/地区', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array', 'dependsOn' => ['key' => 'geoip_enabled', 'value' => true]],
- ]],
],
],
'capabilities' => [
'key' => 'capabilities',
'title' => '基本权限',
- 'version' => '1.0.0',
+ 'version' => '1.1.0',
'option_key' => 'aksrt_settings',
- // Live editing uses PermissionsCapabilities.vue + /permissions/capabilities.
- // Keep an empty schema contract here so domain build() does not fatally call
- // a missing build_cap_matrix_groups() helper when loading login/moderation.
'groups' => [],
],
],
diff --git a/src/Admin/Schema/SchemaPostList.php b/src/Admin/Schema/SchemaPostList.php
index ab95dbb1..2c38ee72 100644
--- a/src/Admin/Schema/SchemaPostList.php
+++ b/src/Admin/Schema/SchemaPostList.php
@@ -253,8 +253,6 @@ class SchemaPostList {
['key' => 'comment_avatar_size', 'label' => '头像大小', 'type' => 'number', 'default' => 48, 'min' => 24, 'max' => 96],
]],
['key' => 'permissions', 'title' => '评论权限', 'fields' => [
- ['key' => 'comment_require_login', 'label' => '评论需要登录', 'type' => 'switch', 'default' => false],
- ['key' => 'comment_allow_reply', 'label' => '允许回复', 'type' => 'switch', 'default' => true],
['key' => 'comment_close_comments', 'label' => '关闭评论', 'type' => 'switch', 'default' => false],
['key' => 'comment_edit_window', 'label' => '作者自助编辑时限(分钟)', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1440],
]],
@@ -262,7 +260,6 @@ class SchemaPostList {
['key' => 'comment_smilie', 'label' => 'Emoji表情', 'type' => 'switch', 'default' => true],
['key' => 'comment_code', 'label' => '代码插入', 'type' => 'switch', 'default' => true],
['key' => 'comment_img', 'label' => '图片上传', 'type' => 'switch', 'default' => true],
- ['key' => 'comment_upload_img', 'label' => '评论图片上传', 'type' => 'switch', 'default' => false],
]],
['key' => 'interaction', 'title' => '互动功能', 'fields' => [
['key' => 'comment_hot_threshold', 'label' => '神评点赞阈值', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 999],
diff --git a/src/Admin/Schema/SchemaUsers.php b/src/Admin/Schema/SchemaUsers.php
index 2ef286cf..c2ffecac 100644
--- a/src/Admin/Schema/SchemaUsers.php
+++ b/src/Admin/Schema/SchemaUsers.php
@@ -602,8 +602,6 @@ class SchemaUsers {
'groups' => [
['key' => 'center', 'title' => '用户中心', 'fields' => [
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,level,comments,favorites,vip,credits,revenue,auth', 'help' => '逗号分隔。level=我的等级;creator=创作中心;revenue=推广收益中心;auth=官方认证;等级、VIP、订单、创作中心、收益中心和认证会按功能开关自动注入'],
- ['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
- ['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
['key' => 'user_city_s', 'label' => '显示城市(IP 归属地)', 'type' => 'switch', 'default' => true],
['key' => 'user_city_type', 'label' => '位置精度', 'type' => 'radio', 'default' => 'province', 'options' => [
diff --git a/src/Admin/UserController.php b/src/Admin/UserController.php
index 07e02238..2ff4737f 100644
--- a/src/Admin/UserController.php
+++ b/src/Admin/UserController.php
@@ -659,6 +659,9 @@ class UserController extends ControllerBase {
if ($user_id <= 0) {
return $this->error(__('请先登录', 'aksrt'), 401);
}
+ if (!function_exists('aksrt_user_can') || !aksrt_user_can('upload_comment_image', $user_id)) {
+ return $this->error(__('当前账号没有上传评论图片的权限', 'aksrt'), 403);
+ }
if (empty($_FILES['image']) && empty($_FILES['file'])) {
return $this->error(__('请选择要上传的图片', 'aksrt'), 400);
}
@@ -667,10 +670,49 @@ class UserController extends ControllerBase {
require_once ABSPATH . 'wp-admin/includes/image.php';
$file_key = !empty($_FILES['image']) ? 'image' : 'file';
- $attachment_id = media_handle_upload($file_key, 0);
+ $file = $_FILES[$file_key];
+ $upload_error = isset($file['error']) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE;
+ if ($upload_error !== UPLOAD_ERR_OK) {
+ 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)));
+ $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);
+ }
+
+ $mimes = [
+ 'jpg|jpeg|jpe' => 'image/jpeg',
+ 'png' => 'image/png',
+ 'gif' => 'image/gif',
+ 'webp' => 'image/webp',
+ ];
+ $checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $mimes);
+ $image_size = wp_getimagesize($file['tmp_name']);
+ if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type']) || !$image_size || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
+ return $this->error(__('仅支持有效的 JPG、PNG、GIF 或 WebP 图片', 'aksrt'), 415);
+ }
+ if (class_exists('\AKSRT\Extensions\ContentAudit')) {
+ $audit = \AKSRT\Extensions\ContentAudit::get_instance()->audit_image((string) $file['tmp_name']);
+ if (empty($audit['passed'])) {
+ return $this->error(__('图片未通过内容审核', 'aksrt'), 422);
+ }
+ }
+
+ $attachment_id = media_handle_upload($file_key, 0, [], [
+ 'test_form' => false,
+ 'mimes' => $mimes,
+ ]);
if (is_wp_error($attachment_id)) {
return $this->error($attachment_id->get_error_message(), 400);
}
+ wp_update_post(['ID' => (int) $attachment_id, 'post_author' => $user_id]);
+ if (!wp_attachment_is_image((int) $attachment_id)) {
+ wp_delete_attachment((int) $attachment_id, true);
+ return $this->error(__('上传文件不是有效图片', 'aksrt'), 415);
+ }
$url = wp_get_attachment_url((int) $attachment_id);
if (!$url) {
return $this->error(__('上传失败', 'aksrt'), 500);
diff --git a/src/Admin/UserControllerProfileMediaOperations.php b/src/Admin/UserControllerProfileMediaOperations.php
index db96ca5d..e5a2fbfe 100644
--- a/src/Admin/UserControllerProfileMediaOperations.php
+++ b/src/Admin/UserControllerProfileMediaOperations.php
@@ -54,14 +54,11 @@ trait UserControllerProfileMediaOperations {
return $this->error(__('请先登录', 'aksrt'), 401);
}
- $center_settings = class_exists('\AKSRT\Settings\Settings')
- ? \AKSRT\Settings\Settings::get_user_center_settings()
- : ['avatar_upload' => true, 'cover_upload' => true];
- if ($is_avatar && empty($center_settings['avatar_upload'])) {
- return $this->error(__('头像上传功能未开启', 'aksrt'), 403);
- }
- if (!$is_avatar && (empty($center_settings['cover_upload']) || !aksrt_get_setting('allow_background_upload', false))) {
- return $this->error(__('背景图上传功能未开启', 'aksrt'), 403);
+ $capability = $is_avatar ? 'upload_avatar' : 'upload_profile_background';
+ if (!function_exists('aksrt_user_can') || !aksrt_user_can($capability, $user_id)) {
+ return $this->error($is_avatar
+ ? __('当前账号没有上传头像的权限', 'aksrt')
+ : __('当前账号没有上传用户资料卡背景图的权限', 'aksrt'), 403);
}
$file_key = '';
@@ -107,6 +104,12 @@ trait UserControllerProfileMediaOperations {
if (!$image_size || empty($image_size[0]) || empty($image_size[1]) || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
return $this->error(__('图片无效或尺寸过大', 'aksrt'), 415);
}
+ if (!$is_avatar && class_exists('\AKSRT\Extensions\ContentAudit')) {
+ $audit = \AKSRT\Extensions\ContentAudit::get_instance()->audit_image((string) $file['tmp_name']);
+ if (empty($audit['passed'])) {
+ return $this->error(__('图片未通过内容审核', 'aksrt'), 422);
+ }
+ }
$attachment_id = media_handle_upload($file_key, 0, [], [
'test_form' => false,
diff --git a/src/Admin/UserControllerReportOperations.php b/src/Admin/UserControllerReportOperations.php
index f1477d55..0c024f09 100644
--- a/src/Admin/UserControllerReportOperations.php
+++ b/src/Admin/UserControllerReportOperations.php
@@ -451,8 +451,21 @@ trait UserControllerReportOperations {
return $this->error('无效的举报类型');
}
+ $reporter_id = get_current_user_id();
+ if ($reporter_id <= 0) {
+ return $this->error(__('请先登录', 'aksrt'), 401);
+ }
+ if ($target_type === 'user') {
+ if ($target_id === $reporter_id) {
+ return $this->error(__('不能举报自己', 'aksrt'), 400);
+ }
+ if (!function_exists('aksrt_user_can') || !aksrt_user_can('user_report', $reporter_id, $target_id)) {
+ return $this->error(__('当前账号没有举报用户的权限', 'aksrt'), 403);
+ }
+ }
+
$report_id = \AKSRT\Messages\ReportCenter::get_instance()->submit(
- get_current_user_id(),
+ $reporter_id,
$target_type,
$target_id,
$reason,
diff --git a/src/Auth/Moderation.php b/src/Auth/Moderation.php
index 25ae140c..722cc740 100644
--- a/src/Auth/Moderation.php
+++ b/src/Auth/Moderation.php
@@ -81,6 +81,15 @@ class Moderation {
*/
public function process(array $commentdata): array {
+ $actor = get_current_user_id();
+ if (!function_exists('aksrt_user_can') || !aksrt_user_can('comment_add', $actor, (int) ($commentdata['comment_post_ID'] ?? 0))) {
+ wp_die(
+ esc_html__('当前账号没有发表评论的权限。', 'aksrt'),
+ esc_html__('评论提交失败', 'aksrt'),
+ ['response' => $actor > 0 ? 403 : 401, 'back_link' => true]
+ );
+ }
+
// 人机验证:覆盖 REST 与原生 wp-comments-post 两条提交合同(与审核开关无关)
if (function_exists('aksrt_require_captcha')) {
$payload = is_array($_POST ?? null) ? wp_unslash($_POST) : [];
diff --git a/src/Content/Download.php b/src/Content/Download.php
index 5fd70714..715f4337 100644
--- a/src/Content/Download.php
+++ b/src/Content/Download.php
@@ -91,6 +91,15 @@ class Download {
}
public function check_rest_nonce(\WP_REST_Request $request) {
+ // The capability matrix can explicitly allow resource downloads for
+ // everyone. A WordPress REST nonce only exists for authenticated
+ // sessions, so requiring one from visitors would make the "all"
+ // column ineffective. Anonymous requests still pass the unified
+ // download gate (capability, payment and captcha) in rest_request().
+ if (get_current_user_id() <= 0) {
+ return true;
+ }
+
$nonce = $request->get_header('x-wp-nonce');
if (!$nonce) {
return new \WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]);
@@ -109,7 +118,6 @@ class Download {
}
$meta = function_exists('aksrt_get_download_box_meta') ? aksrt_get_download_box_meta($post) : [];
- $paid_settings = \AKSRT\Settings\Settings::get_paid_content_settings();
$cost = aksrt_resource_download_cost($post);
$uid = get_current_user_id();
$priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($post, $uid) : null;
@@ -122,10 +130,7 @@ class Download {
? Visibility::get_post_access_state($post, $uid)
: [];
- $need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']);
- if ($need_captcha && function_exists('aksrt_is_captcha_enabled_for') && !aksrt_is_captcha_enabled_for('download')) {
- $need_captcha = false;
- }
+ $need_captcha = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
return new \WP_REST_Response([
'post_id' => $post_id,
@@ -134,7 +139,7 @@ class Download {
'pay_modo' => $pay_modo,
'pay_limit' => (int) ($meta['pay_limit'] ?? 0),
'cost' => $cost,
- 'require_login' => !empty($paid_settings['download_login']) || !empty($meta['require_login']) || (!empty($paid_settings['free_logged_show']) && $cost <= 0 && $display_price <= 0),
+ 'require_login' => !empty($access['need_login']),
'require_captcha' => $need_captcha,
'logged_in' => $uid > 0,
'balance' => $uid > 0 && class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::balance($uid) : 0,
@@ -181,7 +186,6 @@ class Download {
$gate = aksrt_can_download($post);
if (is_wp_error($gate)) return $gate;
- $paid_settings = \AKSRT\Settings\Settings::get_paid_content_settings();
$uid = get_current_user_id();
$cost = aksrt_resource_download_cost($post);
@@ -193,8 +197,7 @@ class Download {
}
}
- $need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']);
- // 下载双开关:cms_features.download_captcha(或单资源强制)且场景未停用。
+ $need_captcha = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
if ($need_captcha && function_exists('aksrt_require_captcha')) {
$cap_payload = isset($params['captcha']) && is_array($params['captcha']) ? $params['captcha'] : [];
// 场景未启用时 require 直接放行;启用时强制校验 token。
diff --git a/src/Content/Visibility.php b/src/Content/Visibility.php
index 3ecc3aaf..664a0a36 100644
--- a/src/Content/Visibility.php
+++ b/src/Content/Visibility.php
@@ -47,14 +47,6 @@ class Visibility {
: (($pay_modo === 'points')
? (float) ($pay_meta['points_price'] ?? 0)
: (float) ($pay_meta['price'] ?? 0));
- $paid_settings = class_exists('\AKSRT\Settings\Settings')
- ? \AKSRT\Settings\Settings::get_paid_content_settings()
- : [
- 'download_login' => true,
- 'download_credit' => true,
- 'download_captcha' => false,
- ];
-
$state = [
'post_id' => $resource_post instanceof \WP_Post ? (int) $resource_post->ID : 0,
'pay_type' => $pay_type,
@@ -134,12 +126,13 @@ class Visibility {
if ($allowed_member && $price <= 0) {
$download_cost = 0;
}
+ $can_download = function_exists('aksrt_user_can') && aksrt_user_can('resource_download', $uid, (int) $resource_post->ID);
$state['download_cost'] = max(0, $download_cost);
- $state['need_login'] = $uid <= 0;
+ $state['need_login'] = !$can_download && $uid <= 0;
$state['need_purchase'] = $allowed_member && $price > 0 && !$has_paid_order;
$state['has_paid_access'] = $allowed_member && ($has_paid_order || $price <= 0);
$state['can_view'] = true;
- $state['can_download'] = $allowed_member && ($uid > 0) && ($has_paid_order || $price <= 0);
+ $state['can_download'] = $can_download && $allowed_member && ($has_paid_order || $price <= 0);
if (!$allowed_member) {
$state['can_download'] = false;
$state['has_paid_access'] = false;
@@ -172,16 +165,14 @@ class Visibility {
}
$state['download_cost'] = max(0, $download_cost);
- $free_logged_show = !empty($paid_settings['free_logged_show']);
- $state['need_login'] = !empty($paid_settings['download_login'])
- || !empty($pay_meta['require_login'])
- || ($free_logged_show && $price <= 0);
- $state['require_captcha'] = !empty($paid_settings['download_captcha']) || !empty($pay_meta['require_captcha']);
+ $can_download = function_exists('aksrt_user_can') && aksrt_user_can('resource_download', $uid, (int) $resource_post->ID);
+ $state['need_login'] = !$can_download && $uid <= 0;
+ $state['require_captcha'] = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
$state['need_purchase'] = !$has_paid_order && $price > 0;
$state['has_paid_access'] = $has_paid_order || $price <= 0 || $has_free_access;
$state['can_view'] = true;
- $state['can_download'] = (!$state['need_login'] || $uid > 0) && ($has_paid_order || $price <= 0);
+ $state['can_download'] = $can_download && ($has_paid_order || $price <= 0);
return $state;
}
@@ -227,6 +218,13 @@ class Visibility {
$state = self::get_post_access_state($resource_post, $user_id);
$uid = $user_id > 0 ? $user_id : get_current_user_id();
+ if (!function_exists('aksrt_user_can') || !aksrt_user_can('resource_download', $uid, (int) $resource_post->ID)) {
+ return new \WP_Error('aksrt_download_forbidden', __('当前账号没有下载资源的权限。', 'aksrt'), [
+ 'status' => $uid > 0 ? 403 : 401,
+ 'login_url' => wp_login_url(get_permalink($resource_post)),
+ ]);
+ }
+
if (($state['pay_type'] ?? '') !== \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD) {
return new \WP_Error('aksrt_not_downloadable', __('该内容未启用下载能力。', 'aksrt'));
}
@@ -238,19 +236,7 @@ class Visibility {
]);
}
- // Free download + pay_free_logged_show (belt-and-suspenders with state.need_login).
$cost = (int) ($state['download_cost'] ?? 0);
- $paid_settings = class_exists('\AKSRT\Settings\Settings')
- ? \AKSRT\Settings\Settings::get_paid_content_settings()
- : ['download_credit' => true, 'free_logged_show' => false];
-
- if ($cost <= 0 && !empty($paid_settings['free_logged_show']) && $uid <= 0) {
- return new \WP_Error('aksrt_need_login', __('免费资源,请登录后下载。', 'aksrt'), [
- 'status' => 401,
- 'login_url' => wp_login_url(get_permalink($resource_post)),
- ]);
- }
-
// Member-only (pay_limit).
if (!empty($state['need_vip']) && empty($state['has_paid_access']) && empty($state['can_download'])) {
$need_level = (int) ($state['pay_limit'] ?? 1);
diff --git a/src/Core/CommentActions.php b/src/Core/CommentActions.php
index f544e48b..c1c85b5f 100644
--- a/src/Core/CommentActions.php
+++ b/src/Core/CommentActions.php
@@ -266,6 +266,15 @@ class CommentActions {
], 403);
}
+ $user_id = get_current_user_id();
+ if (!function_exists('aksrt_user_can') || !aksrt_user_can('comment_add', $user_id, $post_id)) {
+ return new \WP_REST_Response([
+ 'success' => false,
+ 'code' => 'comment_forbidden',
+ 'message' => __('当前账号没有发表评论的权限。', 'aksrt'),
+ ], $user_id > 0 ? 403 : 401);
+ }
+
$nonce = (string) ($params['_aksrt_comment_nonce'] ?? $params['_wpnonce'] ?? '');
if ($nonce !== '' && !wp_verify_nonce($nonce, 'aksrt_comment_nonce') && !wp_verify_nonce($nonce, 'wp_rest')) {
return new \WP_REST_Response([
diff --git a/src/Core/FrontSubmit.php b/src/Core/FrontSubmit.php
index 1a561bba..bfabfe48 100644
--- a/src/Core/FrontSubmit.php
+++ b/src/Core/FrontSubmit.php
@@ -36,12 +36,124 @@ class FrontSubmit {
// Guest path (when enabled).
add_action('wp_ajax_nopriv_aksrt_submit_post', [$this, 'ajax_submit_post']);
+ add_action('rest_api_init', [$this, 'register_rest_routes']);
+
add_filter('body_class', [$this, 'body_class']);
add_filter('wp_robots', [$this, 'robots_noindex']);
add_filter('aksrt_floating_buttons_enabled', [$this, 'disable_chrome_on_submit']);
add_filter('aksrt_footer_tabbar_enabled', [$this, 'disable_chrome_on_submit']);
}
+ public function register_rest_routes(): void {
+ register_rest_route('aksrt/v1', '/front-submit/upload/(?Pimage|resource)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'rest_upload_attachment'],
+ 'permission_callback' => [$this, 'can_upload_attachment'],
+ 'args' => [
+ 'kind' => [
+ 'validate_callback' => static function ($value): bool {
+ return in_array((string) $value, ['image', 'resource'], true);
+ },
+ ],
+ ],
+ ]);
+ }
+
+ public function can_upload_attachment(\WP_REST_Request $request): bool {
+ $user_id = get_current_user_id();
+ if ($user_id <= 0) {
+ return false;
+ }
+ if (!$this->can_submit($user_id)) {
+ return false;
+ }
+
+ $kind = (string) $request->get_param('kind');
+ $cap = $kind === 'resource' ? 'upload_resource_attachment' : 'upload_submit_image';
+ return $this->user_can_cap($cap, $user_id);
+ }
+
+ 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'])) {
+ return new \WP_Error('aksrt_upload_missing_file', __('请选择要上传的文件。', 'aksrt'), ['status' => 400]);
+ }
+
+ $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
+ $max_mb = $kind === 'resource'
+ ? max(1, min(200, (int) ($settings['resource_max_size'] ?? 50)))
+ : max(1, min(20, (int) ($settings['submit_image_max_size'] ?? 5)));
+ $max_size = $max_mb * MB_IN_BYTES;
+ $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]
+ );
+ }
+
+ $filename = sanitize_file_name((string) ($file['name'] ?? ''));
+ $checked = wp_check_filetype_and_ext((string) $file['tmp_name'], $filename, get_allowed_mime_types());
+ $mime = (string) ($checked['type'] ?? '');
+ if ($filename === '' || $mime === '') {
+ return new \WP_Error('aksrt_upload_invalid_type', __('不支持的文件类型。', 'aksrt'), ['status' => 400]);
+ }
+
+ if ($kind === 'image') {
+ $image_info = @getimagesize((string) $file['tmp_name']);
+ if ($image_info === false || strpos($mime, 'image/') !== 0) {
+ return new \WP_Error('aksrt_upload_invalid_image', __('请上传有效的图片文件。', 'aksrt'), ['status' => 400]);
+ }
+ if (((int) $image_info[0] * (int) $image_info[1]) > 40000000) {
+ return new \WP_Error('aksrt_upload_image_too_large', __('图片尺寸过大。', 'aksrt'), ['status' => 400]);
+ }
+ if (class_exists('\AKSRT\Extensions\ContentAudit')) {
+ $audit = \AKSRT\Extensions\ContentAudit::get_instance()->audit_image((string) $file['tmp_name']);
+ if (empty($audit['passed'])) {
+ return new \WP_Error('aksrt_upload_image_rejected', __('图片未通过内容审核。', 'aksrt'), ['status' => 422]);
+ }
+ }
+ }
+
+ if (!function_exists('media_handle_upload')) {
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ }
+
+ $_FILES['aksrt_front_submit_file'] = $file;
+ $attachment_id = media_handle_upload('aksrt_front_submit_file', 0, [], ['test_form' => false]);
+ unset($_FILES['aksrt_front_submit_file']);
+ if (is_wp_error($attachment_id)) {
+ return new \WP_Error('aksrt_upload_failed', $attachment_id->get_error_message(), ['status' => 400]);
+ }
+
+ $attachment_id = (int) $attachment_id;
+ wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
+ update_post_meta($attachment_id, '_aksrt_front_submit_upload_type', $kind);
+ update_post_meta($attachment_id, '_aksrt_front_submit_upload_user_id', $user_id);
+
+ $url = wp_get_attachment_url($attachment_id);
+ if (!$url) {
+ wp_delete_attachment($attachment_id, true);
+ return new \WP_Error('aksrt_upload_failed', __('上传后无法读取文件。', 'aksrt'), ['status' => 500]);
+ }
+
+ return new \WP_REST_Response([
+ 'success' => true,
+ 'attachment_id' => $attachment_id,
+ 'url' => $url,
+ 'name' => get_the_title($attachment_id),
+ 'mime' => $mime,
+ ], 201);
+ }
+
/**
* @return array
*/
diff --git a/src/Core/FrontSubmitFeatured.php b/src/Core/FrontSubmitFeatured.php
index d76a6838..6d78d422 100644
--- a/src/Core/FrontSubmitFeatured.php
+++ b/src/Core/FrontSubmitFeatured.php
@@ -48,9 +48,12 @@ trait FrontSubmitFeatured {
}
$video = esc_url_raw((string) ($payload['url'] ?? $payload['video'] ?? ''));
$pic = esc_url_raw((string) ($payload['pic'] ?? $payload['poster'] ?? $payload['cover'] ?? ''));
+ if ($pic !== '' && !$this->user_can_cap('upload_submit_image', $user_id)) {
+ $pic = '';
+ }
$post_meta['featured_video'] = $video;
$post_meta['featured_slide'] = '';
- if ($pic !== '') {
+ if ($pic !== '' && $this->can_use_front_submit_image_url($pic, $user_id)) {
$post_meta['thumbnail_url'] = $pic;
$post_meta['cover_images'] = $pic;
$this->maybe_set_thumbnail_url($post_id, $pic);
@@ -70,22 +73,26 @@ trait FrontSubmitFeatured {
foreach ($ids as $item) {
if (is_array($item)) {
$u = esc_url_raw((string) ($item['url'] ?? ''));
- if ($u !== '') {
- $urls[] = $u;
- } elseif (!empty($item['id'])) {
- $src = wp_get_attachment_image_url((int) $item['id'], 'full');
+ $attachment_id = absint($item['id'] ?? 0);
+ if ($attachment_id > 0 && $this->can_use_front_submit_image($attachment_id, $user_id)) {
+ $src = wp_get_attachment_image_url($attachment_id, 'full');
if ($src) {
$urls[] = $src;
}
+ } elseif ($u !== '' && $this->user_can_cap('upload_submit_image', $user_id)) {
+ $urls[] = $u;
}
} elseif (is_numeric($item)) {
- $src = wp_get_attachment_image_url((int) $item, 'full');
+ $attachment_id = (int) $item;
+ $src = $this->can_use_front_submit_image($attachment_id, $user_id)
+ ? wp_get_attachment_image_url($attachment_id, 'full')
+ : false;
if ($src) {
$urls[] = $src;
}
} else {
$u = esc_url_raw((string) $item);
- if ($u !== '') {
+ if ($u !== '' && $this->user_can_cap('upload_submit_image', $user_id)) {
$urls[] = $u;
}
}
@@ -93,7 +100,7 @@ trait FrontSubmitFeatured {
}
$post_meta['featured_slide'] = implode(',', $urls);
$post_meta['featured_video'] = '';
- if (!empty($urls[0])) {
+ if (!empty($urls[0]) && $this->can_use_front_submit_image_url((string) $urls[0], $user_id)) {
$post_meta['thumbnail_url'] = $urls[0];
$post_meta['cover_images'] = $urls[0];
$this->maybe_set_thumbnail_url($post_id, $urls[0]);
@@ -106,6 +113,9 @@ trait FrontSubmitFeatured {
}
$url = esc_url_raw((string) ($payload['url'] ?? $payload['image'] ?? ''));
$id = absint($payload['id'] ?? 0);
+ if ($id > 0 && !$this->can_use_front_submit_image($id, $user_id)) {
+ return;
+ }
if ($url === '' && $id > 0) {
$src = wp_get_attachment_image_url($id, 'full');
if ($src) {
@@ -114,12 +124,12 @@ trait FrontSubmitFeatured {
}
$post_meta['featured_video'] = '';
$post_meta['featured_slide'] = '';
- if ($url !== '') {
+ if ($url !== '' && $this->can_use_front_submit_image_url($url, $user_id)) {
$post_meta['thumbnail_url'] = $url;
$post_meta['cover_images'] = $url;
if ($id > 0) {
set_post_thumbnail($post_id, $id);
- } else {
+ } elseif ($this->user_can_cap('upload_submit_image', $user_id)) {
$this->maybe_set_thumbnail_url($post_id, $url);
}
}
@@ -154,4 +164,20 @@ trait FrontSubmitFeatured {
}
$this->set_thumbnail_from_url($post_id, $url);
}
+
+ private function can_use_front_submit_image(int $attachment_id, int $user_id): bool {
+ if ($attachment_id <= 0 || !$this->user_can_cap('upload_submit_image', $user_id)) {
+ return false;
+ }
+ $attachment = get_post($attachment_id);
+ return $attachment instanceof \WP_Post
+ && $attachment->post_type === 'attachment'
+ && (int) $attachment->post_author === $user_id
+ && wp_attachment_is_image($attachment_id);
+ }
+
+ private function can_use_front_submit_image_url(string $url, int $user_id): bool {
+ $attachment_id = attachment_url_to_postid($url);
+ return $attachment_id > 0 && $this->can_use_front_submit_image($attachment_id, $user_id);
+ }
}
diff --git a/src/Core/FrontSubmitPay.php b/src/Core/FrontSubmitPay.php
index 63207823..88e15d4a 100644
--- a/src/Core/FrontSubmitPay.php
+++ b/src/Core/FrontSubmitPay.php
@@ -55,7 +55,7 @@ trait FrontSubmitPay {
}
}
- $downloads = $this->normalize_front_downloads($raw['downloads'] ?? $data['pay_download'] ?? []);
+ $downloads = $this->normalize_front_downloads($raw['downloads'] ?? $data['pay_download'] ?? [], $user_id);
if (!empty($downloads) && $this->user_can_cap('new_post_pay_download', $user_id)) {
$pay_type = DomainModel::PAY_TYPE_DOWNLOAD;
$meta['downloads'] = $downloads;
@@ -163,7 +163,7 @@ trait FrontSubmitPay {
* @param mixed $raw
* @return array>
*/
- private function normalize_front_downloads($raw): array {
+ private function normalize_front_downloads($raw, int $user_id): array {
if (!is_array($raw)) {
return [];
}
@@ -178,16 +178,31 @@ trait FrontSubmitPay {
if ($url === '') {
continue;
}
+ $attachment_id = attachment_url_to_postid($url);
+ if ($attachment_id > 0 && !$this->can_use_front_submit_resource($attachment_id, $user_id)) {
+ continue;
+ }
$out[] = [
'label' => sanitize_text_field((string) ($row['label'] ?? $row['name'] ?? '')),
'url' => $url,
- 'type' => 'external',
+ 'type' => $attachment_id > 0 ? 'direct' : 'external',
'extract_code' => sanitize_text_field((string) ($row['extract_code'] ?? $row['copy_val'] ?? $row['more'] ?? '')),
];
}
return DomainModel::sanitize_downloads($out);
}
+ private function can_use_front_submit_resource(int $attachment_id, int $user_id): bool {
+ if ($attachment_id <= 0 || !$this->user_can_cap('upload_resource_attachment', $user_id)) {
+ return false;
+ }
+ $attachment = get_post($attachment_id);
+ return $attachment instanceof \WP_Post
+ && $attachment->post_type === 'attachment'
+ && (int) $attachment->post_author === $user_id
+ && get_post_meta($attachment_id, '_aksrt_front_submit_upload_type', true) === 'resource';
+ }
+
/**
* @param array $meta
*/
diff --git a/src/Core/FrontSubmitSave.php b/src/Core/FrontSubmitSave.php
index 8eb72f69..c0262324 100644
--- a/src/Core/FrontSubmitSave.php
+++ b/src/Core/FrontSubmitSave.php
@@ -183,8 +183,12 @@ trait FrontSubmitSave {
// Legacy thumbnail URL fallback.
$thumbnail_url = esc_url_raw((string) ($data['thumbnail'] ?? ''));
- if ($thumbnail_url !== '' && empty($data['featured_data'])) {
- $this->set_thumbnail_from_url((int) $in_id, $thumbnail_url);
+ if ($thumbnail_url !== '' && empty($data['featured_data']) && $this->user_can_cap('upload_submit_image', $user_id)) {
+ $attachment_id = attachment_url_to_postid($thumbnail_url);
+ $attachment = $attachment_id > 0 ? get_post($attachment_id) : null;
+ if ($attachment instanceof \WP_Post && (int) $attachment->post_author === $user_id && wp_attachment_is_image($attachment_id)) {
+ set_post_thumbnail((int) $in_id, $attachment_id);
+ }
}
$post = get_post((int) $in_id);
diff --git a/src/Settings/AuthReader.php b/src/Settings/AuthReader.php
index e0880abe..1c5376f4 100644
--- a/src/Settings/AuthReader.php
+++ b/src/Settings/AuthReader.php
@@ -17,7 +17,12 @@ trait AuthReader {
/** 是否要求注册用户才能评论 */
public static function comment_requires_registration(): bool {
- return !empty(self::get_value('comment_require_login', false)) && self::comment_enabled();
+ if (!self::comment_enabled()) {
+ return true;
+ }
+
+ $user_id = get_current_user_id();
+ return !class_exists(Capabilities::class) || !Capabilities::user_can('comment_add', $user_id);
}
/** 是否允许注册 */
diff --git a/src/Settings/Capabilities.php b/src/Settings/Capabilities.php
index 6b2bc6ba..3cb776e1 100644
--- a/src/Settings/Capabilities.php
+++ b/src/Settings/Capabilities.php
@@ -12,7 +12,7 @@ class Capabilities {
/**
* Capability definitions registry.
*
- * @return array}>
+ * @return array}>
*/
public static function cap_definitions(): array {
return [
@@ -20,16 +20,11 @@ class Capabilities {
'balance_transfer' => ['label' => __('余额转账给其他用户', 'aksrt'), 'exclude_roles' => ['all']],
'points_transfer' => ['label' => __('积分转账给其他用户', 'aksrt'), 'exclude_roles' => ['all']],
'balance_withdraw' => ['label' => __('余额提现', 'aksrt'), 'exclude_roles' => ['all']],
- 'set_user_ban' => ['label' => __('设置用户封禁状态', 'aksrt'), 'roles' => ['cat_moderator'], 'default' => ['cat_moderator' => true]],
- 'medal_manually_set' => ['label' => __('为用户授予徽章', 'aksrt'), 'roles' => ['cat_moderator'], 'default' => ['cat_moderator' => true]],
-
'new_post_add' => ['label' => __('发布新文章', 'aksrt'), 'default' => ['logged' => true]],
'new_post_audit_no' => ['label' => __('投稿无需审核直接发布', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_audit_no_manual' => ['label' => __('无需人工审核直接发布', 'aksrt'), 'exclude_roles' => ['all']],
- 'new_post_upload_img' => ['label' => __('投稿允许上传图片', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_upload_video' => ['label' => __('投稿允许上传视频', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_iframe_video' => ['label' => __('投稿允许插入 iframe 视频', 'aksrt'), 'exclude_roles' => ['all'], 'default' => ['logged' => true]],
- 'new_post_upload_file' => ['label' => __('投稿允许上传文件', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_hide' => ['label' => __('投稿允许发布隐藏内容', 'aksrt'), 'default' => ['logged' => true]],
'new_post_image_cover' => ['label' => __('发帖允许设置图片封面', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_slide_cover' => ['label' => __('允许设置幻灯片封面', 'aksrt'), 'exclude_roles' => ['all']],
@@ -40,18 +35,26 @@ class Capabilities {
'new_post_delete' => ['label' => __('删除自己发布的投稿', 'aksrt'), 'exclude_roles' => ['all']],
'comment_view' => ['label' => __('查看评论', 'aksrt'), 'default' => ['all' => true]],
+ 'comment_add' => ['label' => __('发表评论', 'aksrt'), 'default' => ['logged' => true]],
'comment_edit' => ['label' => __('修改自己发布的评论', 'aksrt'), 'exclude_roles' => ['all'], 'default' => ['logged' => true]],
'comment_delete' => ['label' => __('删除自己发布的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_audit_no' => ['label' => __('评论无需审核直接发布', 'aksrt'), 'exclude_roles' => ['all']],
'comment_audit_no_manual' => ['label' => __('评论无需人工审核', 'aksrt'), 'exclude_roles' => ['all']],
- 'comment_set_topping_my_post' => ['label' => __('置顶自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all'], 'default' => ['moderator' => true, 'plate_author' => true, 'cat_moderator' => true]],
- 'comment_set_topping_other' => ['label' => __('置顶管理帖子下的评论', 'aksrt'), 'roles' => ['moderator', 'plate_author', 'cat_moderator'], 'default' => ['cat_moderator' => true]],
+ 'comment_set_topping_my_post' => ['label' => __('置顶自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
+ 'comment_set_topping_other' => ['label' => __('置顶其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_edit_my_post' => ['label' => __('修改自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
- 'comment_edit_other' => ['label' => __('修改管理帖子下的评论', 'aksrt'), 'roles' => ['moderator', 'plate_author', 'cat_moderator'], 'default' => ['cat_moderator' => true]],
- 'comment_audit_my_post' => ['label' => __('审核自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all'], 'default' => ['moderator' => true, 'plate_author' => true, 'cat_moderator' => true]],
- 'comment_audit_other' => ['label' => __('审核管理帖子下的评论', 'aksrt'), 'roles' => ['moderator', 'plate_author', 'cat_moderator'], 'default' => ['moderator' => true, 'plate_author' => true, 'cat_moderator' => true]],
+ 'comment_edit_other' => ['label' => __('修改其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
+ 'comment_audit_my_post' => ['label' => __('审核自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
+ 'comment_audit_other' => ['label' => __('审核其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_delete_my_post' => ['label' => __('删除自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
- 'comment_delete_other' => ['label' => __('删除管理帖子下的评论', 'aksrt'), 'roles' => ['moderator', 'plate_author', 'cat_moderator'], 'default' => ['cat_moderator' => true]],
+ 'comment_delete_other' => ['label' => __('删除其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
+
+ 'upload_avatar' => ['label' => __('上传头像', 'aksrt'), 'default' => ['logged' => true]],
+ 'upload_profile_background' => ['label' => __('上传用户资料卡背景图', 'aksrt'), 'default' => ['logged' => true]],
+ 'upload_submit_image' => ['label' => __('投稿图片上传', 'aksrt'), 'exclude_roles' => ['all']],
+ 'upload_comment_image' => ['label' => __('评论图片上传', 'aksrt'), 'exclude_roles' => ['all']],
+ 'upload_resource_attachment'=> ['label' => __('资源附件上传', 'aksrt'), 'exclude_roles' => ['all']],
+ 'resource_download' => ['label' => __('资源下载', 'aksrt'), 'default' => ['logged' => true]],
];
}
@@ -59,7 +62,7 @@ class Capabilities {
* All role columns in the capability matrix.
*/
public static function cap_role_columns(): array {
- return ['all', 'logged', 'level', 'vip', 'auth', 'moderator', 'plate_author', 'cat_moderator'];
+ return ['all', 'logged', 'level', 'vip', 'auth'];
}
/**
@@ -77,14 +80,9 @@ class Capabilities {
foreach ($defs as $cap_id => $def) {
$defaults = $def['default'] ?? [];
$excludes = $def['exclude_roles'] ?? [];
- $only_roles = $def['roles'] ?? null;
$row = [];
foreach ($cols as $col) {
- if ($only_roles !== null && !in_array($col, $only_roles, true) && !in_array($col, ['all', 'logged', 'level', 'vip', 'auth'], true)) {
- $row[$col] = false;
- continue;
- }
if (in_array($col, $excludes, true)) {
$row[$col] = false;
continue;
diff --git a/src/Settings/CreditsPaymentGroups.php b/src/Settings/CreditsPaymentGroups.php
index 934f8138..ef6a86ac 100644
--- a/src/Settings/CreditsPaymentGroups.php
+++ b/src/Settings/CreditsPaymentGroups.php
@@ -121,9 +121,7 @@ trait CreditsPaymentGroups {
* Unified paid-content and download policy settings.
*
* @return array{
- * download_login:bool,
* download_credit:bool,
- * download_captcha:bool,
* default_cost:int,
* paid_post_types:string[],
* vip_free_download:bool,
@@ -194,7 +192,7 @@ trait CreditsPaymentGroups {
/**
* 获取订单设置
*
- * @return array{prefix:string,expiry:int,show_user:bool,show_paycount:bool,no_logged_in:bool,cookie_day:int,no_logged_remind:bool,wait_float:bool,order_max_minutes:int,mark:string,box_position:string,free_logged_show:bool,show_ip:bool,show_device:bool,show_order_no:bool,show_create_time:bool,show_pay_time:bool,status_texts:array,service:array}
+ * @return array{prefix:string,expiry:int,show_user:bool,show_paycount:bool,no_logged_in:bool,cookie_day:int,no_logged_remind:bool,wait_float:bool,order_max_minutes:int,mark:string,box_position:string,show_ip:bool,show_device:bool,show_order_no:bool,show_create_time:bool,show_pay_time:bool,status_texts:array,service:array}
*/
public static function get_order_settings(): array {
return PaymentRuntime::get_order_settings();
diff --git a/src/Settings/HomeCms.php b/src/Settings/HomeCms.php
index 94ef9169..5ee454d9 100644
--- a/src/Settings/HomeCms.php
+++ b/src/Settings/HomeCms.php
@@ -137,9 +137,7 @@ class HomeCms {
public static function get_cms_features(): array {
$defaults = [
'resource_cpt_enabled' => true,
- 'download_login' => true,
'download_credit' => true,
- 'download_captcha' => false,
'search_include_resource' => true,
];
$s = Settings::get_all();
diff --git a/src/Settings/PaymentRuntime.php b/src/Settings/PaymentRuntime.php
index 116dfef6..a7f75f06 100644
--- a/src/Settings/PaymentRuntime.php
+++ b/src/Settings/PaymentRuntime.php
@@ -27,9 +27,7 @@ class PaymentRuntime {
}
return [
- 'download_login' => !empty($features['download_login']),
'download_credit' => !empty($features['download_credit']) && Settings::credits_enabled(),
- 'download_captcha' => !empty($features['download_captcha']),
'default_cost' => Settings::download_default_cost(),
'paid_post_types' => self::parse_slug_list((string) ($settings['payment_paid_post_types'] ?? 'post'), ['post']),
'vip_free_download' => !empty($settings['payment_vip_free_download']),
@@ -37,7 +35,6 @@ class PaymentRuntime {
'catalog_note' => $catalog_note,
'manual_delivery_note' => $manual_note !== '' ? $manual_note : __('付费或人工确认内容请在订单备注中记录交付方式。', 'aksrt'),
'no_logged_in' => !isset($settings['pay_no_logged_in']) || !empty($settings['pay_no_logged_in']),
- 'free_logged_show' => !empty($settings['pay_free_logged_show']),
'down_alone_page' => !empty($settings['payment_down_alone_page']),
'display' => [
'mark' => sanitize_text_field((string) ($settings['pay_mark'] ?? '付费')),
@@ -375,7 +372,6 @@ class PaymentRuntime {
'order_max_minutes' => $max_minutes,
'mark' => sanitize_text_field((string) ($s['pay_mark'] ?? '付费')),
'box_position' => $box_position,
- 'free_logged_show' => !empty($s['pay_free_logged_show']),
'show_ip' => !empty($s['payment_show_ip']),
'show_device' => !empty($s['payment_show_device']),
'show_order_no' => !isset($s['payment_show_order_no']) || !empty($s['payment_show_order_no']),
diff --git a/src/Settings/PostDisplay.php b/src/Settings/PostDisplay.php
index 2e653f7c..f1056a11 100644
--- a/src/Settings/PostDisplay.php
+++ b/src/Settings/PostDisplay.php
@@ -461,14 +461,13 @@ class PostDisplay {
'per_page' => max(5, min(100, (int) ($settings['comment_per_page'] ?? 20))),
'nested_depth' => max(1, min(10, (int) ($settings['comment_nested_depth'] ?? 3))),
'require_login' => Settings::comment_requires_registration(),
- 'allow_reply' => !isset($settings['comment_allow_reply']) || !empty($settings['comment_allow_reply']),
'avatar_size' => max(24, min(96, (int) ($settings['comment_avatar_size'] ?? 48))),
'empty_text' => $empty_text !== '' ? $empty_text : __('暂无评论,来做第一个评论的人吧~', 'aksrt'),
'toolbar' => [
'smilie' => !isset($settings['comment_smilie']) || !empty($settings['comment_smilie']),
'code' => !isset($settings['comment_code']) || !empty($settings['comment_code']),
'image' => !isset($settings['comment_img']) || !empty($settings['comment_img']),
- 'upload_img' => !empty($settings['comment_upload_img']),
+ 'upload_img' => function_exists('aksrt_user_can') && aksrt_user_can('upload_comment_image', get_current_user_id()),
],
'like_enabled' => !isset($settings['comment_like_s']) || !empty($settings['comment_like_s']),
'author_tag' => !isset($settings['comment_author_tag']) || !empty($settings['comment_author_tag']),
diff --git a/src/Settings/Settings.php b/src/Settings/Settings.php
index edf517e3..7e3da7bd 100644
--- a/src/Settings/Settings.php
+++ b/src/Settings/Settings.php
@@ -573,7 +573,7 @@ class Settings {
/**
* Private user-center display settings.
*
- * @return array{tabs:string[],avatar_upload:bool,cover_upload:bool,social_links:bool}
+ * @return array{tabs:string[]}
*/
public static function get_user_center_settings(): array {
return UserRuntime::get_user_center_settings();
@@ -705,7 +705,7 @@ class Settings {
/**
* Comment display and behavior settings.
*
- * @return array{order:string,pagination_mode:string,nested_depth:int,require_login:bool,allow_reply:bool,avatar_size:int,empty_text:string}
+ * @return array{order:string,pagination_mode:string,nested_depth:int,require_login:bool,avatar_size:int,empty_text:string}
*/
public static function get_comment_display_settings(): array {
return PostDisplay::get_comment_display_settings();
diff --git a/src/Settings/UserRuntime.php b/src/Settings/UserRuntime.php
index dd9ef691..7463de34 100644
--- a/src/Settings/UserRuntime.php
+++ b/src/Settings/UserRuntime.php
@@ -80,7 +80,7 @@ class UserRuntime {
/**
* Private user-center display settings.
*
- * @return array{tabs:string[],avatar_upload:bool,cover_upload:bool,social_links:bool}
+ * @return array{tabs:string[]}
*/
public static function get_user_center_settings(): array {
$settings = Settings::get_all();
@@ -133,9 +133,6 @@ class UserRuntime {
return [
'tabs' => $normalized_tabs,
- 'avatar_upload' => !isset($settings['allow_avatar_upload']) || !empty($settings['allow_avatar_upload']),
- 'cover_upload' => !isset($settings['user_cover_upload']) || !empty($settings['user_cover_upload']),
- 'social_links' => !isset($settings['user_social_links']) || !empty($settings['user_social_links']),
];
}
diff --git a/src/User/Badges.php b/src/User/Badges.php
index d037cec2..3fe400b3 100644
--- a/src/User/Badges.php
+++ b/src/User/Badges.php
@@ -409,9 +409,6 @@ class Badges {
if (user_can($actor, 'manage_options')) {
return true;
}
- if (class_exists('\AKSRT\Settings\Capabilities')) {
- return \AKSRT\Settings\Capabilities::user_can('medal_manually_set', $actor);
- }
return false;
}
diff --git a/src/User/Center.php b/src/User/Center.php
index d6df27d1..ab78601c 100644
--- a/src/User/Center.php
+++ b/src/User/Center.php
@@ -113,14 +113,11 @@ class Center {
* 注册默认面板
*/
private function register_default_panels(): void {
- $settings = class_exists('\AKSRT\Settings\Settings')
- ? \AKSRT\Settings\Settings::get_user_center_settings()
- : [
- 'tabs' => ['profile', 'comments', 'favorites'],
- 'avatar_upload' => true,
- 'cover_upload' => true,
- 'social_links' => true,
- ];
+ $settings = class_exists('\AKSRT\Settings\Settings')
+ ? \AKSRT\Settings\Settings::get_user_center_settings()
+ : [
+ 'tabs' => ['profile', 'comments', 'favorites'],
+ ];
$definitions = [
'profile' => [
diff --git a/templates/pages/page-download-file.php b/templates/pages/page-download-file.php
index 1e390870..c3f61746 100644
--- a/templates/pages/page-download-file.php
+++ b/templates/pages/page-download-file.php
@@ -56,14 +56,6 @@ $need_vip = $gate_code === 'aksrt_need_vip';
$priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($post) : null;
$price = is_array($priced) ? (float) ($priced['amount'] ?? 0) : (float) ($pay_meta['price'] ?? 0);
$pay_modo = is_array($priced) ? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0') : ((($pay_meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0');
-$order_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_order_settings() : [];
-$free_logged_show = !empty($order_settings['free_logged_show']);
-if ($price <= 0 && $free_logged_show && !is_user_logged_in()) {
- $can_dl = false;
- $need_login = true;
- $has_access = true; // free "access" but must login to download
-}
-
$currency = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
$cash_symbol = '¥';
if (class_exists('\AKSRT\Settings\Settings')) {
diff --git a/templates/pages/page-download.php b/templates/pages/page-download.php
index d1c8d107..4fd746b6 100644
--- a/templates/pages/page-download.php
+++ b/templates/pages/page-download.php
@@ -60,8 +60,7 @@ if ($is_logged_in) {
}
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
-$paid_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_paid_content_settings() : [];
-$dl_requires_login = !empty($paid_settings['download_login']);
+$dl_requires_login = function_exists('aksrt_user_can') && !aksrt_user_can('resource_download', get_current_user_id());
?>
diff --git a/templates/pages/page-submit.php b/templates/pages/page-submit.php
index d4c96c3b..69e4a819 100644
--- a/templates/pages/page-submit.php
+++ b/templates/pages/page-submit.php
@@ -68,9 +68,9 @@ if (file_exists($js)) {
true
);
}
- if (!empty($ctx['can_upload_img']) || !empty($ctx['can_cover_image']) || !empty($ctx['can_cover_slide'])) {
- wp_enqueue_media();
- }
+ $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;
+ $resource_max_size = max(1, min(200, (int) ($attachment_settings['resource_max_size'] ?? 50))) * MB_IN_BYTES;
wp_localize_script('aksrt-front-submit', 'aksrtSubmitData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
@@ -83,6 +83,16 @@ if (file_exists($js)) {
'summary' => !empty($ctx['ai_available']),
'tags' => !empty($ctx['ai_available']),
],
+ 'uploads' => [
+ 'image' => !empty($ctx['can_upload_img']) ? [
+ 'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/image')),
+ 'maxSize' => $submit_image_max_size,
+ ] : null,
+ 'resource' => !empty($ctx['can_upload_file']) ? [
+ 'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/resource')),
+ 'maxSize' => $resource_max_size,
+ ] : null,
+ ],
'i18n' => [
'confirmLeave' => __('你有未保存的更改,确定要离开吗?', 'aksrt'),
'saving' => __('保存中...', 'aksrt'),
diff --git a/templates/parts/resource-download-box.php b/templates/parts/resource-download-box.php
index 05433dc8..e27cacab 100644
--- a/templates/parts/resource-download-box.php
+++ b/templates/parts/resource-download-box.php
@@ -43,7 +43,7 @@ $gate_data = $gate_blocked ? (array) $gate->get_error_data() : [];
$paid_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_paid_content_settings()
- : ['download_credit' => true, 'download_captcha' => false];
+ : ['download_credit' => true];
$access = class_exists('\AKSRT\Content\Visibility')
? \AKSRT\Content\Visibility::get_post_access_state($post_id)
@@ -56,10 +56,7 @@ $pay_modo = is_array($priced)
$user_amount = is_array($priced) ? (float) ($priced['amount'] ?? 0) : (float) ($meta['price'] ?? 0);
$cost = function_exists('aksrt_resource_download_cost') ? (int) aksrt_resource_download_cost($post_id) : 0;
-$captcha_active = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']);
-if ($captcha_active && function_exists('aksrt_is_captcha_enabled_for') && !aksrt_is_captcha_enabled_for('download')) {
- $captcha_active = false;
-}
+$captcha_active = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
$balance = (int) ($gate_data['balance'] ?? (is_user_logged_in() && class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::balance(get_current_user_id()) : 0));
$login_url = (string) ($gate_data['login_url'] ?? wp_login_url(get_permalink($post_id)));
$vip_url = function_exists('aksrt_get_user_center_tab_url')
@@ -84,13 +81,7 @@ if ($is_need_login && $allow_guest_buy) {
$gate_blocked = true;
}
-// Zibll pay_free_logged_show: free downloads still require login.
-$free_logged_show = !empty($paid_settings['free_logged_show']);
$price_amount = (float) ($price_label['amount'] ?? $user_amount);
-if ($price_amount <= 0 && $free_logged_show && !is_user_logged_in()) {
- $is_need_login = true;
- $gate_blocked = true;
-}
// Zibll down_alone_page: single entry to independent download page.
$alone_page = !empty($paid_settings['down_alone_page']) || !empty($paid_settings['download']['alone_page']);
diff --git a/templates/parts/submit/context.php b/templates/parts/submit/context.php
index 6cf3fe2a..7bc62654 100644
--- a/templates/parts/submit/context.php
+++ b/templates/parts/submit/context.php
@@ -213,9 +213,9 @@ return [
'can_cover_image' => $cap('new_post_image_cover'),
'can_cover_slide' => $cap('new_post_slide_cover'),
'can_cover_video' => $cap('new_post_video_cover'),
- 'can_upload_img' => $cap('new_post_upload_img'),
+ 'can_upload_img' => $cap('upload_submit_image'),
'can_upload_video' => $cap('new_post_upload_video'),
- 'can_upload_file' => $cap('new_post_upload_file'),
+ 'can_upload_file' => $cap('upload_resource_attachment'),
'can_hide' => $cap('new_post_hide'),
'can_iframe' => $cap('new_post_iframe_video'),
];
diff --git a/templates/parts/submit/editor-main.php b/templates/parts/submit/editor-main.php
index 60c34306..95432af2 100644
--- a/templates/parts/submit/editor-main.php
+++ b/templates/parts/submit/editor-main.php
@@ -44,14 +44,11 @@ $is_can_new = !empty($ctx['is_can_new']);
'post_content',
'textarea_rows' => 20,
'editor_height' => wp_is_mobile() ? 460 : 470,
- 'media_buttons' => !empty($ctx['can_upload_img']) || !empty($ctx['can_upload_video']) || !empty($ctx['can_upload_file']),
+ 'media_buttons' => false,
'default_editor' => 'tinymce',
'quicktags' => false,
'teeny' => false,
@@ -66,8 +63,13 @@ $is_can_new = !empty($ctx['is_can_new']);
?>
-
+
@@ -125,13 +138,6 @@ $pm_url = ($pm_enabled && $messages_url !== '')
-
-
-
-
-
-
-
@@ -176,19 +182,24 @@ $pm_url = ($pm_enabled && $messages_url !== '')
-
-
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/uninstall.php b/uninstall.php
index 5840fa14..5c616bf9 100644
--- a/uninstall.php
+++ b/uninstall.php
@@ -59,7 +59,6 @@ if ($options) {
// ── Delete AKSRT user meta ──
$meta_keys = [
- 'aksrt_user_contacts',
'aksrt_avatar_attachment_id',
'aksrt_background_attachment_id',
'aksrt_background_image',