feat(extensions): 新增系统工具页面与前台投稿分片上传
This commit is contained in:
@@ -6,6 +6,16 @@ export const extensionsSection = {
|
||||
icon: renderFaIcon('fa fas fa-puzzle-piece'),
|
||||
route: '/extensions',
|
||||
children: [
|
||||
schemaPage({
|
||||
key: 'extensions-system',
|
||||
name: 'ExtensionsSystem',
|
||||
label: '系统工具',
|
||||
route: '/extensions/system',
|
||||
title: '系统工具',
|
||||
desc: 'WordPress 优化、上传优化与编辑器显示的系统级开关。',
|
||||
canonicalDomain: 'extensions',
|
||||
section: 'system',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'extensions-ip',
|
||||
name: 'ExtensionsIp',
|
||||
|
||||
@@ -16,7 +16,7 @@ const SECTION_DEFAULT_CHILDREN = {
|
||||
permissions: 'permissions-comment',
|
||||
users: 'users-auth',
|
||||
payment: 'payment-knowledge',
|
||||
extensions: 'extensions-ip',
|
||||
extensions: 'extensions-system',
|
||||
tools: 'tools-import-export',
|
||||
'theme-info': 'theme-info-dashboard',
|
||||
docs: 'docs-dashboard',
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
* Each key corresponds to a sub-page under the "扩展&增强" section.
|
||||
*/
|
||||
|
||||
import { systemSchema } from './system.js'
|
||||
import { ipSchema } from './ip.js'
|
||||
import { auditSchema } from './audit.js'
|
||||
import { pushSchema } from './push.js'
|
||||
import { securitySchema } from './security.js'
|
||||
|
||||
export const extensionsSchemas = {
|
||||
system: systemSchema,
|
||||
ip: ipSchema,
|
||||
audit: auditSchema,
|
||||
push: pushSchema,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
export const systemSchema = {
|
||||
key: 'extensions.system',
|
||||
title: '系统工具',
|
||||
icon: 'fa fas fa-gavel',
|
||||
description: 'WordPress 优化、上传优化与编辑器显示的系统级开关。',
|
||||
defaults: {
|
||||
hide_admin_bar: true,
|
||||
remove_emoji: true,
|
||||
remove_open_sans: true,
|
||||
remove_more_wp_head: true,
|
||||
remove_dashboard_widgets: true,
|
||||
display_wp_update: true,
|
||||
newfilename: false,
|
||||
newfilename_type: 'time',
|
||||
split_upload_s: true,
|
||||
split_minimum_size: 20,
|
||||
disabled_large_thumbnail: true,
|
||||
admin_user_del_fields: true,
|
||||
admin_frontend_set: true,
|
||||
close_gutenberg: false,
|
||||
disabled_autoembed: true,
|
||||
mce_float_toolbar: ['pc_s', 'm_s'],
|
||||
time_ago_s: true,
|
||||
time_format: 'n月j日 H:i',
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'optimize',
|
||||
title: 'WordPress 优化',
|
||||
icon: 'fa fas fa-bolt',
|
||||
description: '精简WordPress自带的冗余功能,提升前台与后台的加载速度。',
|
||||
fields: [
|
||||
{ key: 'hide_admin_bar', label: '前台关闭顶部admin_bar', type: 'switch', default: true, help: '开启后前台不显示WordPress顶部黑条' },
|
||||
{ key: 'remove_emoji', label: '删除WordPress自带Emoji表情', type: 'switch', default: true },
|
||||
{ key: 'remove_open_sans', label: '禁用Google字体', type: 'switch', default: true },
|
||||
{ key: 'remove_more_wp_head', label: '清理多余的头部Meta标签', type: 'switch', default: true },
|
||||
{ key: 'remove_dashboard_widgets', label: 'WordPress 后台加速', type: 'switch', default: true, help: '移除仪表盘的站点健康和WordPress活动等不必要功能,并禁止检测浏览器和PHP版本(这两个非常耗时),优化后台加载速度' },
|
||||
{ key: 'display_wp_update', label: '禁止WordPress检测更新', type: 'switch', default: true, help: '开启后将禁用WordPress核心、插件和主题的自动更新及更新检测' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'upload',
|
||||
title: '上传优化',
|
||||
icon: 'fa fas fa-cloud-upload-alt',
|
||||
description: '上传文件的命名与缩略图裁剪优化,减少硬盘占用并提高上传效率。',
|
||||
fields: [
|
||||
{ key: 'split_upload_s', label: '大文件分片上传', type: 'switch', default: true, help: '分片上传大文件,优化大文件上传效率(支持断点续传)' },
|
||||
{ key: 'split_minimum_size', label: '分片上传最小文件', type: 'number', default: 20, min: 4, max: 100, unit: 'M', dependsOn: { key: 'split_upload_s', value: true }, help: '大于此大小的文件才会分片上传,请根据服务器性能合理设置;较大文件直接上传会比较耗时且无法突破系统上传大小限制,分片上传可以有效解决此问题;前端上传大小限制在「功能权限 → 上传权限」中设置。注意:能否顺利上传大文件还取决于服务器性能、带宽、CDN等因素,如果网站使用了OSS/COS等第三方云储存,则不建议在网站上传大文件' },
|
||||
{ key: 'newfilename', label: '上传文件自动重命名', type: 'switch', default: false, help: '上传文件时系统会查询重名文件,开启此功能后可减少重名文件,从而提高上传效率' },
|
||||
{
|
||||
key: 'newfilename_type',
|
||||
label: '重命名格式',
|
||||
type: 'radio',
|
||||
buttonStyle: false,
|
||||
default: 'time',
|
||||
dependsOn: { key: 'newfilename', value: true },
|
||||
options: [
|
||||
{ label: '随机字串符', value: 'random' },
|
||||
{ label: '时间+原文件名', value: 'time' },
|
||||
],
|
||||
},
|
||||
{ key: 'disabled_large_thumbnail', label: '禁止WP大尺寸缩略图裁剪', type: 'switch', default: true, help: '开启后将禁用WordPress自动裁剪medium_large、1536x1536、2048x2048的缩略图,可减少服务器硬盘压力,同时提高上传效率;如果您使用的插件必须要这些尺寸的缩略图,或使用了云储存的图片处理功能,则请关闭' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'editor',
|
||||
title: '编辑器与显示',
|
||||
icon: 'fa fas fa-edit',
|
||||
description: '编辑器模式、用户资料页与前台时间显示格式的定制。',
|
||||
fields: [
|
||||
{ key: 'admin_user_del_fields', label: '关闭后台用户编辑多余的选项', type: 'switch', default: true, help: '开启后在后台编辑用户资料时将不显示无用的多余选项' },
|
||||
{ key: 'admin_frontend_set', label: '前端页面编辑', type: 'switch', default: true, help: '管理员可在前台快速修改页面、文章参数' },
|
||||
{ key: 'close_gutenberg', label: '禁用古腾堡编辑器', type: 'switch', default: false, help: '后台文章编辑器将退回使用经典编辑器;禁用后主题的编辑器增强功能将不能使用,请酌情禁用!' },
|
||||
{ key: 'disabled_autoembed', label: '禁用Auto Embed', type: 'switch', default: true, help: 'Auto Embed是WordPress自动检测并嵌入链接的一种功能,主要适配海外媒体网站,国内几乎用不上,建议禁用,可大幅度提高页面渲染速度' },
|
||||
{
|
||||
key: 'mce_float_toolbar',
|
||||
label: '经典编辑器增强浮动工具栏',
|
||||
type: 'checkbox',
|
||||
default: ['pc_s', 'm_s'],
|
||||
options: [
|
||||
{ label: 'PC端开启', value: 'pc_s' },
|
||||
{ label: '移动端开启', value: 'm_s' },
|
||||
],
|
||||
help: '开启后前端编辑器会显示浮动的工具栏,更加方便书写',
|
||||
},
|
||||
{ key: 'time_ago_s', label: '倒计时显示', type: 'switch', default: true, help: '时间格式化为:X分钟前,X小时前,X天前....' },
|
||||
{ key: 'time_format', label: '自定义时间格式', type: 'input', default: 'n月j日 H:i', dependsOn: { key: 'time_ago_s', operator: 'falsy' }, help: '关闭倒计时显示后生效,时间格式接受标准PHP时间格式,请注意控制长度!' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -238,8 +238,6 @@ export const permissionsSchemas = {
|
||||
{ type: 'admin', pattern: 'allow', val: 'svg=image/svg+xml' },
|
||||
{ type: 'all', pattern: 'allow', val: '' },
|
||||
],
|
||||
newfilename: false,
|
||||
newfilename_type: 'time',
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
@@ -314,26 +312,6 @@ export const permissionsSchemas = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'behavior',
|
||||
title: '上传行为',
|
||||
icon: 'fa fas fa-cogs',
|
||||
fields: [
|
||||
{ key: 'newfilename', label: '上传文件自动重命名', type: 'switch', help: '开启后上传文件按规则重命名,减少重名文件查询,提高上传效率。' },
|
||||
{
|
||||
key: 'newfilename_type',
|
||||
label: '重命名格式',
|
||||
type: 'radio',
|
||||
buttonStyle: false,
|
||||
default: 'time',
|
||||
dependsOn: { key: 'newfilename', value: true },
|
||||
options: [
|
||||
{ label: '时间 + 原文件名', value: 'time' },
|
||||
{ label: '随机字符串', value: 'random' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -121,27 +121,100 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
var SPLIT_CHUNK_BYTES = 2 * 1048576;
|
||||
|
||||
function splitUploadConfig() {
|
||||
var split = cfg.splitUpload || {};
|
||||
var minSize = parseInt(split.minSize, 10);
|
||||
if (!isFinite(minSize) || minSize < 4) minSize = 4;
|
||||
return { enabled: !!split.enabled, minSize: minSize };
|
||||
}
|
||||
|
||||
function requestJson(url, options) {
|
||||
var init = options || {};
|
||||
init.credentials = 'same-origin';
|
||||
init.headers = { 'X-WP-Nonce': cfg.restNonce || '' };
|
||||
return fetch(url, init).then(function (response) {
|
||||
return response.json().catch(function () { return {}; }).then(function (payload) {
|
||||
if (!response.ok) {
|
||||
throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '文件上传失败');
|
||||
}
|
||||
return payload;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 分片上传:状态(断点续传)→ 逐片上传 → 合并走标准落库管线,响应与整传一致。
|
||||
function uploadInChunks(file, type) {
|
||||
var uploadKey = type === 'file' ? 'resource' : type;
|
||||
var base = String(cfg.restUrl || '').replace(/\/$/, '');
|
||||
if (!base) return Promise.reject(new Error(i18n.uploadUnavailable || '当前账号不能上传此类文件'));
|
||||
var postId = currentPostId();
|
||||
|
||||
function chunkUrl(action) {
|
||||
var target = base + '/front-submit/' + action + '/' + uploadKey;
|
||||
if (postId) target += (target.indexOf('?') === -1 ? '?' : '&') + 'post_id=' + encodeURIComponent(String(postId));
|
||||
return target;
|
||||
}
|
||||
|
||||
var total = Math.ceil(file.size / SPLIT_CHUNK_BYTES);
|
||||
return requestJson(chunkUrl('upload-status')).catch(function () { return { uploaded: [] }; }).then(function (status) {
|
||||
var uploaded = {};
|
||||
(Array.isArray(status.uploaded) ? status.uploaded : []).forEach(function (index) { uploaded[index] = true; });
|
||||
var chain = Promise.resolve();
|
||||
for (var index = 0; index < total; index++) {
|
||||
(function (i) {
|
||||
chain = chain.then(function () {
|
||||
if (uploaded[i]) return;
|
||||
var body = new FormData();
|
||||
body.append('file', file.slice(i * SPLIT_CHUNK_BYTES, Math.min(file.size, (i + 1) * SPLIT_CHUNK_BYTES)));
|
||||
body.append('chunk', String(i));
|
||||
body.append('chunks', String(total));
|
||||
body.append('name', file.name);
|
||||
body.append('size', String(file.size));
|
||||
return requestJson(chunkUrl('upload-chunk'), { method: 'POST', body: body });
|
||||
});
|
||||
})(index);
|
||||
}
|
||||
return chain;
|
||||
}).then(function () {
|
||||
var body = new FormData();
|
||||
body.append('chunks', String(total));
|
||||
body.append('name', file.name);
|
||||
body.append('size', String(file.size));
|
||||
return requestJson(chunkUrl('upload-merge'), { method: 'POST', body: body });
|
||||
}).then(function (payload) {
|
||||
if (!payload || !payload.success || !payload.url) {
|
||||
throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '文件上传失败');
|
||||
}
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
|
||||
function uploadMedia(file, type) {
|
||||
var uploadKey = type === 'file' ? 'resource' : type;
|
||||
var upload = (cfg.uploads || {})[uploadKey] || null;
|
||||
if (!upload || !upload.url) return Promise.reject(new Error(i18n.uploadUnavailable || '当前账号不能上传此类文件'));
|
||||
if (!acceptsFile(file, type)) return Promise.reject(new Error(i18n.invalidFile || '请选择有效文件'));
|
||||
if (upload.maxSize && file.size > upload.maxSize) return Promise.reject(new Error(i18n.fileTooLarge || '文件大小超过限制'));
|
||||
var split = splitUploadConfig();
|
||||
if (split.enabled && typeof file.slice === 'function' && file.size > split.minSize * 1048576) {
|
||||
if (Math.ceil(file.size / SPLIT_CHUNK_BYTES) > 1) {
|
||||
return uploadInChunks(file, type);
|
||||
}
|
||||
}
|
||||
var body = new FormData();
|
||||
body.append('file', file);
|
||||
var uploadUrl = String(upload.url);
|
||||
if (currentPostId()) {
|
||||
uploadUrl += (uploadUrl.indexOf('?') === -1 ? '?' : '&') + 'post_id=' + encodeURIComponent(String(currentPostId()));
|
||||
}
|
||||
return fetch(uploadUrl, { 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 || !payload.url) {
|
||||
throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '文件上传失败');
|
||||
}
|
||||
return payload;
|
||||
});
|
||||
});
|
||||
return requestJson(uploadUrl, { method: 'POST', body: body }).then(function (payload) {
|
||||
if (!payload || !payload.success || !payload.url) {
|
||||
throw new Error((payload && (payload.message || (payload.data && payload.data.message))) || i18n.error || '文件上传失败');
|
||||
}
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
|
||||
function imageBatchLimit() {
|
||||
@@ -578,6 +651,120 @@
|
||||
});
|
||||
}
|
||||
|
||||
function toggleBlockFormat(editor, format) {
|
||||
editor.formatter.toggle(format);
|
||||
editor.nodeChanged();
|
||||
}
|
||||
|
||||
function removeEditorBlock(editor, node) {
|
||||
if (!node || node === editor.getBody()) return;
|
||||
editor.dom.remove(node);
|
||||
editor.nodeChanged();
|
||||
}
|
||||
|
||||
function replaceButtonIconWithSvg(button, pathData) {
|
||||
var node = button && button.$el && button.$el[0];
|
||||
var icon = node ? node.querySelector('.mce-i-c-svg') : null;
|
||||
if (!icon) return;
|
||||
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 1024 1024');
|
||||
svg.setAttribute('style', 'width:21px;height:21px;fill:currentColor;');
|
||||
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.setAttribute('d', pathData);
|
||||
svg.appendChild(path);
|
||||
icon.parentNode.replaceChild(svg, icon);
|
||||
}
|
||||
|
||||
// 经典编辑器增强浮动工具栏(后台「扩展&增强 → 系统工具」mce_float_toolbar):
|
||||
// 复刻参考主题 editextend.js 的 wptoolbar 上下文工具条。
|
||||
function registerFloatToolbar(editor) {
|
||||
editor.addButton('aksrt_h2', {
|
||||
title: 'Heading 2',
|
||||
icon: 'c-svg heading2',
|
||||
stateSelector: 'h2',
|
||||
onclick: function () { toggleBlockFormat(editor, 'h2'); },
|
||||
onPostRender: function () {
|
||||
replaceButtonIconWithSvg(this, 'M143.616 219.648v228.864h278.016V219.648h89.856V768H421.632v-242.688H143.616V768H53.76V219.648h89.856z m660.48-10.752c52.992 0 96.768 15.36 131.328 46.08 33.792 30.72 50.688 69.888 50.688 119.04 0 47.616-18.432 90.624-53.76 129.792-16.554667 17.706667-43.093333 39.082667-78.933333 64.426667l-22.613334 15.701333-12.117333 8.192c-52.309333 34.389333-85.248 64.810667-99.413333 91.178667l-2.730667 5.589333h270.336V768h-382.464c0-56.064 17.664-104.448 54.528-145.92 8.746667-10.069333 21.76-22.186667 38.912-36.352l15.786667-12.586667c5.589333-4.352 11.52-8.874667 17.834666-13.568l19.84-14.506666 21.888-15.488 11.690667-8.106667c35.328-24.576 59.904-45.312 75.264-61.44 23.808-26.88 36.096-56.064 36.096-86.784 0-29.952-8.448-52.224-23.808-66.816-16.128-14.592-39.936-21.504-71.424-21.504-33.792 0-59.136 11.52-76.032 34.56-15.36 19.541333-24.362667 48.64-27.050667 86.058667l-0.597333 11.477333h-89.856c0.768-61.44 18.432-110.592 53.76-148.224 36.096-39.936 83.712-59.904 142.848-59.904z');
|
||||
},
|
||||
});
|
||||
|
||||
editor.addButton('aksrt_h3', {
|
||||
title: 'Heading 3',
|
||||
icon: 'c-svg heading3',
|
||||
stateSelector: 'h3',
|
||||
onclick: function () { toggleBlockFormat(editor, 'h3'); },
|
||||
onPostRender: function () {
|
||||
replaceButtonIconWithSvg(this, 'M801.024 208.896c55.296 0 100.608 13.056 134.4 39.936 33.024 26.88 49.92 63.744 49.92 111.36 0 59.904-30.72 99.84-91.392 119.808 32.256 9.984 57.6 24.576 74.496 44.544 18.432 20.736 27.648 47.616 27.648 79.872 0 50.688-17.664 92.16-52.992 124.416-36.864 33.024-85.248 49.92-145.152 49.92-56.832 0-102.912-14.592-137.472-43.776-38.4-32.256-59.904-79.872-64.512-141.312h91.392c1.536 35.328 12.288 62.976 33.792 82.176 19.2 17.664 44.544 26.88 76.032 26.88 34.56 0 62.208-9.984 82.176-29.184 17.664-17.664 26.88-39.168 26.88-65.28 0-31.488-9.984-54.528-28.416-69.12-18.432-15.36-45.312-22.272-80.64-22.272h-38.4V449.28h38.4c32.256 0 56.832-6.912 73.728-20.736 16.128-13.824 24.576-34.56 24.576-61.44 0-26.88-7.68-46.848-22.272-60.672-16.128-13.824-39.936-20.736-71.424-20.736-32.256 0-56.832 7.68-74.496 23.808-18.432 16.128-29.184 40.704-32.256 73.728h-88.32c4.608-55.296 24.576-98.304 61.44-129.024 34.56-30.72 79.104-45.312 132.864-45.312z m-657.408 10.752v228.864h278.016V219.648h89.856V768H421.632v-242.688H143.616V768H53.76V219.648h89.856z');
|
||||
},
|
||||
});
|
||||
|
||||
editor.addButton('aksrt_remove', {
|
||||
tooltip: i18n.removeBlock || '删除',
|
||||
icon: 'dashicon dashicons-no-alt',
|
||||
onclick: function () {
|
||||
removeEditorBlock(editor, editor.selection.getNode());
|
||||
},
|
||||
});
|
||||
|
||||
editor.on('wptoolbar', function (event) {
|
||||
if (event.toolbar) return;
|
||||
var probe = document.createElement('div');
|
||||
probe.innerHTML = editor.getContent();
|
||||
if (probe.querySelectorAll('p,h1,h2,h3,h4,h5,h6,ul,ol,div,blockquote,pre,img,iframe,table,hr').length <= 4) return;
|
||||
|
||||
var bars = floatToolbarButtons(editor, event);
|
||||
if (bars.length && editor.wp && typeof editor.wp._createToolbar === 'function') {
|
||||
event.toolbar = editor.wp._createToolbar(bars);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 复刻参考主题 get_all_toolbar:按光标所在块与是否选中文字给出上下文按钮。
|
||||
function floatToolbarButtons(editor, event) {
|
||||
var element = event.element;
|
||||
if (!element) return [];
|
||||
var nodeName = element.nodeName;
|
||||
var innerText = element.innerText ? String(element.innerText).trim() : '';
|
||||
var hasSelection = !!editor.selection.getContent();
|
||||
var parent = element.parentElement;
|
||||
var inRoot = !!(parent && parent.id === 'tinymce');
|
||||
var bars = [];
|
||||
|
||||
if (['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'UL', 'LI', 'STRONG', 'SPAN', 'CODE'].indexOf(nodeName) !== -1) {
|
||||
if (inRoot || (nodeName === 'SPAN' && parent && parent.parentElement && parent.parentElement.id === 'tinymce')) {
|
||||
bars.push('aksrt_h2', 'aksrt_h3');
|
||||
}
|
||||
|
||||
if (hasSelection) {
|
||||
bars = ['H1', 'H2', 'H3', 'H4', 'H5'].indexOf(nodeName) === -1 ? ['bold', 'link', 'wp_code'] : [];
|
||||
bars.push('forecolor');
|
||||
if (nodeName === 'P' && editorCfg.canHide) bars.push('aksrt_hide');
|
||||
bars.push('aksrt_remove');
|
||||
return bars;
|
||||
}
|
||||
|
||||
if (innerText) {
|
||||
bars.push('bullist', 'numlist', 'aligncenter');
|
||||
if (nodeName === 'P' && inRoot) {
|
||||
bars.push('aksrt_quote');
|
||||
if (editorCfg.canHide) bars.push('aksrt_hide');
|
||||
}
|
||||
bars.push('aksrt_remove');
|
||||
} else {
|
||||
bars.push('bullist', 'aksrt_quote');
|
||||
if (editorCfg.canHide) bars.push('aksrt_hide');
|
||||
if (editorCfg.canUploadImage) bars.push('aksrt_image');
|
||||
bars.push('aksrt_video', 'aksrt_precode');
|
||||
}
|
||||
}
|
||||
|
||||
if (!bars.length && (nodeName === 'DIV' || nodeName === 'PRE') && !(element.classList && element.classList.contains('file-download-name'))) {
|
||||
bars.push('aksrt_remove');
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
function registerEditorTools() {
|
||||
if (!window.tinymce || !window.tinymce.PluginManager || window.aksrtSubmitEditorToolsRegistered) {
|
||||
return false;
|
||||
@@ -711,6 +898,10 @@
|
||||
menu: menu,
|
||||
});
|
||||
}
|
||||
|
||||
if (editorCfg.floatToolbar) {
|
||||
registerFloatToolbar(editor);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* AKSRT Page Admin Set — 前端页面编辑弹窗提交(后台「扩展&增强 → 系统工具」)。
|
||||
*
|
||||
* 弹窗内容由 PageAdminSet::ajax_modal() 经 aksrt-modal-runtime 注入,
|
||||
* 这里只负责表单提交:序列化 → admin-ajax → 成功后刷新页面。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
(function (window, document) {
|
||||
'use strict';
|
||||
|
||||
function notify(message, type) {
|
||||
if (typeof window.notyf === 'function') {
|
||||
window.notyf(message, type || 'error', 4, 'aksrt-page-admin-set-toast');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target.closest('form.aksrt-admin-set-form');
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
|
||||
var submitButton = form.querySelector('.aksrt-admin-set-submit');
|
||||
if (submitButton && submitButton.disabled) return;
|
||||
if (submitButton) submitButton.disabled = true;
|
||||
|
||||
var action = (form.querySelector('input[name="action"]') || {}).value || '';
|
||||
var ajaxUrl = (window.aksrtData && window.aksrtData.ajaxUrl) || (window._win && window._win.ajax_url) || '';
|
||||
if (!ajaxUrl) {
|
||||
notify('请求地址缺失', 'error');
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var url = ajaxUrl + (ajaxUrl.indexOf('?') === -1 ? '?' : '&') + 'action=' + encodeURIComponent(action);
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: new FormData(form),
|
||||
})
|
||||
.then(function (response) {
|
||||
return response.json().catch(function () { return {}; }).then(function (payload) {
|
||||
if (!response.ok || !payload || !payload.success) {
|
||||
throw new Error((payload && payload.data && payload.data.msg) || '保存失败,请重试');
|
||||
}
|
||||
return payload;
|
||||
});
|
||||
})
|
||||
.then(function (payload) {
|
||||
var data = payload.data || {};
|
||||
notify(data.msg || '保存成功', 'success');
|
||||
if (data.reload !== false) {
|
||||
window.setTimeout(function () { window.location.reload(); }, 600);
|
||||
} else if (submitButton) {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
notify(error.message || '保存失败,请重试', 'error');
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
});
|
||||
});
|
||||
})(window, document);
|
||||
@@ -170,6 +170,12 @@ if (class_exists('\AKSRT\Settings\MailNotifications')) {
|
||||
if (class_exists('\AKSRT\Extensions\SecurityHardening')) {
|
||||
\AKSRT\Extensions\SecurityHardening::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Extensions\SystemTools')) {
|
||||
\AKSRT\Extensions\SystemTools::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Frontend\PageAdminSet')) {
|
||||
\AKSRT\Frontend\PageAdminSet::get_instance()->init();
|
||||
}
|
||||
|
||||
// 短代码统一注册入口(registry)
|
||||
if (class_exists('\AKSRT\Core\Shortcodes')) {
|
||||
|
||||
+2
-3
@@ -62,11 +62,10 @@ add_action('save_post', static function ($post_id, $post = null): void {
|
||||
|
||||
// ============================================================
|
||||
|
||||
// 前端隐藏 WordPress Admin Bar
|
||||
// 前端隐藏 WordPress Admin Bar:由 SystemTools 按「扩展&增强 → 系统工具」
|
||||
// 的 hide_admin_bar 设置门控(默认开启),不再无条件注入。
|
||||
// ============================================================
|
||||
|
||||
add_filter('show_admin_bar', '__return_false');
|
||||
|
||||
// ============================================================
|
||||
// SMTP 邮件配置注入
|
||||
// ============================================================
|
||||
|
||||
@@ -155,9 +155,6 @@ if (!function_exists('aksrt_float_right')) :
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_floating_buttons_settings();
|
||||
$items = is_array($settings['items'] ?? null) ? $settings['items'] : [];
|
||||
if (!$items) {
|
||||
return;
|
||||
}
|
||||
|
||||
$is_mobile = wp_is_mobile();
|
||||
$btn = '';
|
||||
@@ -224,6 +221,12 @@ if (!function_exists('aksrt_float_right')) :
|
||||
return;
|
||||
}
|
||||
|
||||
// 扩展点(对齐参考主题 zib_float_right filter):模块可注入自定义悬浮按钮。
|
||||
$btn = (string) apply_filters('aksrt_float_right_buttons', $btn, $is_mobile);
|
||||
if ($btn === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$filter = (array) ($settings['filter_blur'] ?? []);
|
||||
$class = ($settings['style'] ?? 'round') . ' position-' . ($settings['position'] ?? 'bottom');
|
||||
$class .= (($is_mobile && in_array('m_s', $filter, true)) || (!$is_mobile && in_array('pc_s', $filter, true))) ? ' filter' : '';
|
||||
|
||||
@@ -480,6 +480,14 @@ if (!function_exists('aksrt_time_ago')) :
|
||||
$ctime = (int) strtotime(current_time('mysql'));
|
||||
$t = $ctime - $time;
|
||||
|
||||
// 关闭倒计时显示时按「系统工具」自定义时间格式输出(legacy zib_get_time_ago parity)。
|
||||
$system = class_exists('\AKSRT\Settings\ExtensionRuntime')
|
||||
? \AKSRT\Settings\ExtensionRuntime::get_system_tools_settings()
|
||||
: [];
|
||||
if (empty($system['time_ago_s']) && ($system['time_format'] ?? '') !== '') {
|
||||
return date($system['time_format'], $time);
|
||||
}
|
||||
|
||||
if ($t < 0) {
|
||||
return date('Y-m-d H:i', $time);
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ class SchemaExtensions {
|
||||
'domain' => 'extensions',
|
||||
'title' => '扩展&增强',
|
||||
'sections' => [
|
||||
'ip' => SchemaExtensionsIP::build(),
|
||||
'audit' => SchemaExtensionsAudit::build(),
|
||||
'push' => SchemaExtensionsPush::build(),
|
||||
'security' => SchemaExtensionsSecurity::build(),
|
||||
'system' => SchemaExtensionsSystem::build(),
|
||||
'ip' => SchemaExtensionsIP::build(),
|
||||
'audit' => SchemaExtensionsAudit::build(),
|
||||
'push' => SchemaExtensionsPush::build(),
|
||||
'security' => SchemaExtensionsSecurity::build(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaExtensionsSystem {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'system',
|
||||
'title' => '系统工具',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'optimize', 'title' => 'WordPress 优化', 'fields' => [
|
||||
['key' => 'hide_admin_bar', 'label' => '前台关闭顶部admin_bar', 'type' => 'switch', 'default' => true, 'help' => '开启后前台不显示WordPress顶部黑条'],
|
||||
['key' => 'remove_emoji', 'label' => '删除WordPress自带Emoji表情', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'remove_open_sans', 'label' => '禁用Google字体', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'remove_more_wp_head', 'label' => '清理多余的头部Meta标签', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'remove_dashboard_widgets', 'label' => 'WordPress 后台加速', 'type' => 'switch', 'default' => true, 'help' => '移除仪表盘的站点健康和WordPress活动等不必要功能,并禁止检测浏览器和PHP版本(这两个非常耗时),优化后台加载速度'],
|
||||
['key' => 'display_wp_update', 'label' => '禁止WordPress检测更新', 'type' => 'switch', 'default' => true, 'help' => '开启后将禁用WordPress核心、插件和主题的自动更新及更新检测'],
|
||||
]],
|
||||
['key' => 'upload', 'title' => '上传优化', 'fields' => [
|
||||
['key' => 'split_upload_s', 'label' => '大文件分片上传', 'type' => 'switch', 'default' => true, 'help' => '分片上传大文件,优化大文件上传效率(支持断点续传)'],
|
||||
['key' => 'split_minimum_size', 'label' => '分片上传最小文件', 'type' => 'number', 'default' => 20, 'min' => 4, 'max' => 100, 'unit' => 'M', 'dependsOn' => ['key' => 'split_upload_s', 'value' => true], 'help' => '大于此大小的文件才会分片上传,请根据服务器性能合理设置;较大文件直接上传会比较耗时且无法突破系统上传大小限制,分片上传可以有效解决此问题;前端上传大小限制在「功能权限 → 上传权限」中设置。注意:能否顺利上传大文件还取决于服务器性能、带宽、CDN等因素,如果网站使用了OSS/COS等第三方云储存,则不建议在网站上传大文件'],
|
||||
['key' => 'newfilename', 'label' => '上传文件自动重命名', 'type' => 'switch', 'default' => false, 'help' => '上传文件时系统会查询重名文件,开启此功能后可减少重名文件,从而提高上传效率'],
|
||||
['key' => 'newfilename_type', 'label' => '重命名格式', 'type' => 'radio', 'default' => 'time', 'dependsOn' => ['key' => 'newfilename', 'value' => true], 'options' => [
|
||||
['label' => '随机字串符', 'value' => 'random'],
|
||||
['label' => '时间+原文件名', 'value' => 'time'],
|
||||
]],
|
||||
['key' => 'disabled_large_thumbnail', 'label' => '禁止WP大尺寸缩略图裁剪', 'type' => 'switch', 'default' => true, 'help' => '开启后将禁用WordPress自动裁剪medium_large、1536x1536、2048x2048的缩略图,可减少服务器硬盘压力,同时提高上传效率;如果您使用的插件必须要这些尺寸的缩略图,或使用了云储存的图片处理功能,则请关闭'],
|
||||
]],
|
||||
['key' => 'editor', 'title' => '编辑器与显示', 'fields' => [
|
||||
['key' => 'admin_user_del_fields', 'label' => '关闭后台用户编辑多余的选项', 'type' => 'switch', 'default' => true, 'help' => '开启后在后台编辑用户资料时将不显示无用的多余选项'],
|
||||
['key' => 'admin_frontend_set', 'label' => '前端页面编辑', 'type' => 'switch', 'default' => true, 'help' => '管理员可在前台快速修改页面、文章参数'],
|
||||
['key' => 'close_gutenberg', 'label' => '禁用古腾堡编辑器', 'type' => 'switch', 'default' => false, 'help' => '后台文章编辑器将退回使用经典编辑器;禁用后主题的编辑器增强功能将不能使用,请酌情禁用!'],
|
||||
['key' => 'disabled_autoembed', 'label' => '禁用Auto Embed', 'type' => 'switch', 'default' => true, 'help' => 'Auto Embed是WordPress自动检测并嵌入链接的一种功能,主要适配海外媒体网站,国内几乎用不上,建议禁用,可大幅度提高页面渲染速度'],
|
||||
['key' => 'mce_float_toolbar', 'label' => '经典编辑器增强浮动工具栏', 'type' => 'checkbox', 'default' => ['pc_s', 'm_s'], 'sanitize_type' => 'array', 'options' => [
|
||||
['label' => 'PC端开启', 'value' => 'pc_s'],
|
||||
['label' => '移动端开启', 'value' => 'm_s'],
|
||||
], 'help' => '开启后前端编辑器会显示浮动的工具栏,更加方便书写'],
|
||||
['key' => 'time_ago_s', 'label' => '倒计时显示', 'type' => 'switch', 'default' => true, 'help' => '时间格式化为:X分钟前,X小时前,X天前....'],
|
||||
['key' => 'time_format', 'label' => '自定义时间格式', 'type' => 'input', 'default' => 'n月j日 H:i', 'dependsOn' => ['key' => 'time_ago_s', 'operator' => 'falsy'], 'help' => '关闭倒计时显示后生效,时间格式接受标准PHP时间格式,请注意控制长度!'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -131,13 +131,6 @@ class SchemaPermissions {
|
||||
['key' => 'val', 'label' => '格式', 'type' => 'textarea', 'default' => '', 'rows' => 1, 'placeholder' => '如:svg=image/svg+xml,mp3=audio/mpeg'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'behavior', 'title' => '上传行为', 'fields' => [
|
||||
['key' => 'newfilename', 'label' => '上传文件自动重命名', 'type' => 'switch', 'default' => false, 'help' => '开启后上传文件按规则重命名,减少重名文件查询,提高上传效率。'],
|
||||
['key' => 'newfilename_type', 'label' => '重命名格式', 'type' => 'radio', 'default' => 'time', 'dependsOn' => ['key' => 'newfilename', 'value' => true], 'options' => [
|
||||
['label' => '时间 + 原文件名', 'value' => 'time'],
|
||||
['label' => '随机字符串', 'value' => 'random'],
|
||||
]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'moderation' => [
|
||||
|
||||
@@ -17,6 +17,7 @@ class FrontSubmit {
|
||||
use FrontSubmitPay;
|
||||
use FrontSubmitFeatured;
|
||||
use FrontSubmitMedia;
|
||||
use FrontSubmitChunk;
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
@@ -93,6 +94,8 @@ class FrontSubmit {
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->register_chunk_routes();
|
||||
}
|
||||
|
||||
public function can_upload_attachment(\WP_REST_Request $request): bool {
|
||||
@@ -128,15 +131,26 @@ class FrontSubmit {
|
||||
}
|
||||
|
||||
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;
|
||||
$kind = (string) $request->get_param('kind');
|
||||
$files = $request->get_file_params();
|
||||
$file = $files['file'] ?? null;
|
||||
|
||||
if (!is_array($file)) {
|
||||
return new \WP_Error('aksrt_upload_missing_file', __('请选择要上传的文件。', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
return $this->persist_uploaded_file($kind, $file, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并把一个上传文件落库为附件;整传与分片合并共用同一条管线。
|
||||
*
|
||||
* @param string $kind image|video|resource
|
||||
* @param array $file $_FILES 风格数组(name/tmp_name/size/error)
|
||||
* @param bool $sideload true 时文件不是 HTTP 直传(分片合并产物),用 sideload 动作保存
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function persist_uploaded_file(string $kind, array $file, bool $sideload = false) {
|
||||
$max_size = self::upload_limit_bytes($kind);
|
||||
$upload_error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE);
|
||||
if ($upload_error === UPLOAD_ERR_INI_SIZE || $upload_error === UPLOAD_ERR_FORM_SIZE) {
|
||||
@@ -156,7 +170,7 @@ class FrontSubmit {
|
||||
return new \WP_Error('aksrt_upload_failed', __('服务器无法保存上传文件。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$size = (int) ($file['size'] ?? 0);
|
||||
$size = (int) ($file['size'] ?? 0);
|
||||
|
||||
if ($size <= 0 || $size > $max_size) {
|
||||
return new \WP_Error(
|
||||
@@ -197,15 +211,18 @@ class FrontSubmit {
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
}
|
||||
|
||||
// 合并产物不是 HTTP 直传文件,走 sideload 动作(copy 而非 move_uploaded_file)。
|
||||
$overrides = $sideload ? ['test_form' => false, 'action' => 'wp_handle_sideload'] : ['test_form' => false];
|
||||
|
||||
$_FILES['aksrt_front_submit_file'] = $file;
|
||||
$attachment_id = media_handle_upload('aksrt_front_submit_file', 0, [], ['test_form' => false]);
|
||||
$attachment_id = media_handle_upload('aksrt_front_submit_file', 0, [], $overrides);
|
||||
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]);
|
||||
wp_update_post(['ID' => $attachment_id, 'post_author' => get_current_user_id()]);
|
||||
update_post_meta($attachment_id, '_aksrt_front_submit_media', $kind);
|
||||
|
||||
$attachment = $this->prepare_attachment_response($attachment_id);
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Core;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* 前台大文件分片上传(后台「扩展&增强 → 系统工具」的 split_upload_s / split_minimum_size)。
|
||||
*
|
||||
* 行为对齐参考主题 action/media.php 的 user_split_upload / user_split_upload_merge /
|
||||
* user_split_uploaded_chunk 协议:2MB 一片、断点续传(已传分片按序跳过)、合并产物
|
||||
* 经 sideload 动作重新走 persist_uploaded_file() 标准校验与落库管线。
|
||||
*/
|
||||
trait FrontSubmitChunk {
|
||||
|
||||
/** 每个分片的固定大小(参考主题 split_chunk_size = 2MB)。 */
|
||||
const CHUNK_SIZE_BYTES = 2 * 1048576;
|
||||
|
||||
public function register_chunk_routes(): void {
|
||||
$kind_arg = [
|
||||
'validate_callback' => static function ($value): bool {
|
||||
return in_array((string) $value, ['image', 'video', 'resource'], true);
|
||||
},
|
||||
];
|
||||
|
||||
register_rest_route('aksrt/v1', '/front-submit/upload-chunk/(?P<kind>image|video|resource)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_upload_chunk'],
|
||||
'permission_callback' => [$this, 'can_upload_attachment'],
|
||||
'args' => [
|
||||
'kind' => $kind_arg,
|
||||
'chunk' => ['default' => 0, 'sanitize_callback' => 'absint'],
|
||||
'chunks' => ['default' => 0, 'sanitize_callback' => 'absint'],
|
||||
'name' => ['default' => '', 'sanitize_callback' => 'sanitize_file_name'],
|
||||
'size' => ['default' => 0, 'sanitize_callback' => 'absint'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/front-submit/upload-merge/(?P<kind>image|video|resource)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_upload_chunk_merge'],
|
||||
'permission_callback' => [$this, 'can_upload_attachment'],
|
||||
'args' => [
|
||||
'kind' => $kind_arg,
|
||||
'chunks' => ['default' => 0, 'sanitize_callback' => 'absint'],
|
||||
'name' => ['default' => '', 'sanitize_callback' => 'sanitize_file_name'],
|
||||
'size' => ['default' => 0, 'sanitize_callback' => 'absint'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/front-submit/upload-status/(?P<kind>image|video|resource)', [
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'rest_upload_chunk_status'],
|
||||
'permission_callback' => [$this, 'can_upload_attachment'],
|
||||
'args' => [
|
||||
'kind' => $kind_arg,
|
||||
'chunks' => ['default' => 0, 'sanitize_callback' => 'absint'],
|
||||
'name' => ['default' => '', 'sanitize_callback' => 'sanitize_file_name'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传单个分片;index 为 0 基,同时累加校验总大小不超上限。
|
||||
*/
|
||||
public function rest_upload_chunk(\WP_REST_Request $request) {
|
||||
$file = $request->get_file_params()['file'] ?? null;
|
||||
if (!is_array($file) || empty($file['tmp_name']) || (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
return new \WP_Error('aksrt_chunk_missing_file', __('上传信息错误,请重新选择文件', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
$params = $this->chunk_params($request, true);
|
||||
if (is_wp_error($params)) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$max_size = self::upload_limit_bytes($params['kind']);
|
||||
if ($params['size'] <= 0 || $params['size'] > $max_size) {
|
||||
return new \WP_Error('aksrt_upload_too_large', sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), ['status' => 413]);
|
||||
}
|
||||
|
||||
$chunk_bytes = (int) @filesize((string) $file['tmp_name']);
|
||||
if ($chunk_bytes <= 0 || $chunk_bytes > self::CHUNK_SIZE_BYTES) {
|
||||
return new \WP_Error('aksrt_chunk_invalid_size', __('分片大小异常,请重新上传', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
$temp_dir = $this->chunk_temp_dir();
|
||||
if ($params['chunk'] === 0) {
|
||||
$this->chunk_temp_cleanup($temp_dir);
|
||||
}
|
||||
|
||||
$existing = $this->chunk_part_paths($temp_dir, $params['key']);
|
||||
$uploaded = 0;
|
||||
foreach ($existing as $path) {
|
||||
$uploaded += (int) @filesize($path);
|
||||
}
|
||||
if ($uploaded + $chunk_bytes > $max_size) {
|
||||
return new \WP_Error('aksrt_upload_too_large', sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), ['status' => 413]);
|
||||
}
|
||||
|
||||
$target = $temp_dir . '/' . $this->chunk_part_name($params['key'], $params['chunk']);
|
||||
$moved = @move_uploaded_file((string) $file['tmp_name'], $target);
|
||||
if (!$moved) {
|
||||
$moved = @copy((string) $file['tmp_name'], $target);
|
||||
@unlink((string) $file['tmp_name']);
|
||||
}
|
||||
if (!$moved) {
|
||||
return new \WP_Error('aksrt_chunk_save_failed', __('分片保存失败,请重新上传', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
'chunk' => $params['chunk'],
|
||||
'chunks' => $params['chunks'],
|
||||
'uploaded_size' => $uploaded + $chunk_bytes,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断点续传:返回已落盘的分片序号,前端按序跳过。
|
||||
*/
|
||||
public function rest_upload_chunk_status(\WP_REST_Request $request) {
|
||||
$params = $this->chunk_params($request);
|
||||
if (is_wp_error($params)) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$parts = $this->chunk_part_paths($this->chunk_temp_dir(), $params['key']);
|
||||
$uploaded = [];
|
||||
foreach ($parts as $path) {
|
||||
$uploaded[] = $this->chunk_index_from_name((string) $path);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
'uploaded' => $uploaded,
|
||||
'uploaded_count' => count($uploaded),
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并分片并按标准管线落库;成功或失败后都清理分片。
|
||||
*/
|
||||
public function rest_upload_chunk_merge(\WP_REST_Request $request) {
|
||||
$params = $this->chunk_params($request);
|
||||
if (is_wp_error($params)) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$max_size = self::upload_limit_bytes($params['kind']);
|
||||
$temp_dir = $this->chunk_temp_dir();
|
||||
$merge_path = $temp_dir . '/' . $this->chunk_part_name($params['key'], 'merge');
|
||||
|
||||
$fp = @fopen($merge_path, 'wb');
|
||||
if (!$fp) {
|
||||
return new \WP_Error('aksrt_chunk_merge_failed', __('分片合并失败,请重新上传', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$missing = 0;
|
||||
for ($i = 0; $i < $params['chunks']; $i++) {
|
||||
$part = $temp_dir . '/' . $this->chunk_part_name($params['key'], $i);
|
||||
if (!is_file($part)) {
|
||||
$missing++;
|
||||
break;
|
||||
}
|
||||
fwrite($fp, (string) file_get_contents($part));
|
||||
}
|
||||
fclose($fp);
|
||||
|
||||
if ($missing) {
|
||||
@unlink($merge_path);
|
||||
return new \WP_Error('aksrt_chunk_missing', __('文件缺少分片,正在重新上传缺失分片', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
|
||||
$merged_size = (int) @filesize($merge_path);
|
||||
if ($merged_size <= 0 || ($params['size'] > 0 && $merged_size !== $params['size'])) {
|
||||
@unlink($merge_path);
|
||||
return new \WP_Error('aksrt_chunk_size_mismatch', __('合并后的文件大小校验失败,请重新上传', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if ($merged_size > $max_size) {
|
||||
@unlink($merge_path);
|
||||
return new \WP_Error('aksrt_upload_too_large', sprintf(__('文件大小不能超过 %s。', 'aksrt'), size_format($max_size)), ['status' => 413]);
|
||||
}
|
||||
|
||||
$name = sanitize_file_name($params['name']);
|
||||
$type = (string) (wp_check_filetype_and_ext($merge_path, $name, get_allowed_mime_types())['type'] ?? '');
|
||||
|
||||
$result = $this->persist_uploaded_file($params['kind'], [
|
||||
'tmp_name' => $merge_path,
|
||||
'name' => $name,
|
||||
'size' => $merged_size,
|
||||
'type' => $type,
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
], true);
|
||||
|
||||
// 无论落库成败都清理本次分片(persist 成功时已 copy 走合并文件,失败时也要清场)。
|
||||
foreach ($this->chunk_part_paths($temp_dir, $params['key']) as $path) {
|
||||
@unlink($path);
|
||||
}
|
||||
@unlink($merge_path);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** 校验并归一分片参数(name/chunks/chunk/size/key)。 */
|
||||
private function chunk_params(\WP_REST_Request $request, bool $with_index = false) {
|
||||
$name = sanitize_file_name((string) $request->get_param('name'));
|
||||
$chunks = (int) $request->get_param('chunks');
|
||||
$chunk = (int) $request->get_param('chunk');
|
||||
|
||||
if ($name === '' || $chunks < 1 || $chunks > 20480) {
|
||||
return new \WP_Error('aksrt_chunk_invalid_params', __('分片参数异常', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if ($with_index && ($chunk < 0 || $chunk >= $chunks)) {
|
||||
return new \WP_Error('aksrt_chunk_invalid_params', __('分片参数异常', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
return [
|
||||
'kind' => (string) $request->get_param('kind'),
|
||||
'name' => $name,
|
||||
'chunks' => $chunks,
|
||||
'chunk' => $chunk,
|
||||
'size' => (int) $request->get_param('size'),
|
||||
// 分片按用户隔离,避免同名文件互相覆盖、也防止探测他人分片。
|
||||
'key' => md5(get_current_blog_id() . '|' . get_current_user_id() . '|' . $name),
|
||||
];
|
||||
}
|
||||
|
||||
/** 分片临时目录(参考主题为 wp-content/zib-temp,带 index.php 保护和 48h 清理)。 */
|
||||
private function chunk_temp_dir(): string {
|
||||
$temp_dir = (string) apply_filters('aksrt_temp_dir', WP_CONTENT_DIR . '/aksrt-temp');
|
||||
if (!@is_dir($temp_dir)) {
|
||||
wp_mkdir_p($temp_dir);
|
||||
}
|
||||
$index_file = $temp_dir . '/index.php';
|
||||
if (!file_exists($index_file)) {
|
||||
@file_put_contents($index_file, "<?php\n// Silence is golden.\n");
|
||||
}
|
||||
return $temp_dir;
|
||||
}
|
||||
|
||||
/** 清理超过 48 小时的遗留分片。 */
|
||||
private function chunk_temp_cleanup(string $temp_dir): void {
|
||||
$expire = time() - 48 * HOUR_IN_SECONDS;
|
||||
foreach ((array) glob($temp_dir . '/*.part') as $file) {
|
||||
if (@filemtime((string) $file) < $expire) {
|
||||
@unlink((string) $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 当前 key 的全部分片路径(不含 merge 产物)。 */
|
||||
private function chunk_part_paths(string $temp_dir, string $key): array {
|
||||
$paths = [];
|
||||
foreach ((array) glob($temp_dir . '/' . $this->chunk_part_name($key, '*')) as $path) {
|
||||
$path = (string) $path;
|
||||
if (substr($path, -10) === 'merge.part') {
|
||||
continue;
|
||||
}
|
||||
$paths[] = $path;
|
||||
}
|
||||
sort($paths, SORT_NATURAL);
|
||||
return $paths;
|
||||
}
|
||||
|
||||
private function chunk_part_name(string $key, $index): string {
|
||||
return sprintf('%d-%s-%s.part', get_current_blog_id(), $key, (string) $index);
|
||||
}
|
||||
|
||||
private function chunk_index_from_name(string $path): int {
|
||||
$base = basename($path, '.part');
|
||||
$dash = strrpos($base, '-');
|
||||
return $dash === false ? -1 : (int) substr($base, $dash + 1);
|
||||
}
|
||||
}
|
||||
@@ -213,8 +213,6 @@ trait ScriptsPart1c {
|
||||
'upload_ext' => implode('|', array_keys(get_allowed_mime_types())),
|
||||
'user_upload_nonce' => wp_create_nonce('user_upload'),
|
||||
'post_action_nonce' => wp_create_nonce('post_action'),
|
||||
'is_split_upload' => false,
|
||||
'split_minimum_size' => 10,
|
||||
'translate_config' => [],
|
||||
'i18n' => [
|
||||
'load_more' => __('加载更多', 'aksrt'),
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
/**
|
||||
* 系统工具:WordPress 优化、上传优化与编辑器显示开关。
|
||||
*
|
||||
* 对应后台「扩展&增强 → 系统工具」设置(ExtensionRuntime::get_system_tools_settings()),
|
||||
* 行为对齐参考主题 zib-theme.php / zib-head.php / admin-main.php / admin-set.php 的系统工具套件。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Extensions;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SystemTools {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
$settings = \AKSRT\Settings\ExtensionRuntime::get_system_tools_settings();
|
||||
|
||||
if (!empty($settings['hide_admin_bar']) || !is_super_admin()) {
|
||||
add_filter('show_admin_bar', '__return_false');
|
||||
}
|
||||
|
||||
if (!empty($settings['remove_emoji'])) {
|
||||
$this->remove_emoji();
|
||||
}
|
||||
|
||||
if (!empty($settings['remove_open_sans'])) {
|
||||
add_action('init', [$this, 'remove_open_sans']);
|
||||
}
|
||||
|
||||
if (!empty($settings['remove_more_wp_head'])) {
|
||||
add_action('after_setup_theme', [$this, 'remove_more_wp_head']);
|
||||
}
|
||||
|
||||
if (!empty($settings['remove_dashboard_widgets'])) {
|
||||
add_action('do_meta_boxes', [$this, 'remove_dashboard_widgets'], 10, 3);
|
||||
add_filter('site_status_tests', [$this, 'filter_site_status_tests']);
|
||||
add_filter('pre_http_request', [$this, 'block_site_health_requests'], 10, 3);
|
||||
}
|
||||
|
||||
if (!empty($settings['display_wp_update'])) {
|
||||
$this->disable_wp_update();
|
||||
}
|
||||
|
||||
if (!empty($settings['disabled_large_thumbnail'])) {
|
||||
add_action('after_setup_theme', [$this, 'disable_large_thumbnail']);
|
||||
}
|
||||
|
||||
if (!empty($settings['close_gutenberg'])) {
|
||||
add_filter('use_block_editor_for_post', '__return_false');
|
||||
}
|
||||
|
||||
if (!empty($settings['disabled_autoembed'])) {
|
||||
$this->disable_autoembed();
|
||||
}
|
||||
|
||||
if (!empty($settings['admin_user_del_fields'])) {
|
||||
add_action('show_user_profile', [$this, 'hide_user_profile_fields']);
|
||||
add_action('edit_user_profile', [$this, 'hide_user_profile_fields']);
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除WordPress自带Emoji表情(对齐参考主题 remove_emoji 套件)。 */
|
||||
private function remove_emoji(): void {
|
||||
remove_action('admin_print_scripts', 'print_emoji_detection_script');
|
||||
remove_action('admin_print_styles', 'print_emoji_styles');
|
||||
remove_action('wp_head', 'print_emoji_detection_script', 7);
|
||||
remove_action('wp_print_styles', 'print_emoji_styles');
|
||||
remove_filter('the_content_feed', 'wp_staticize_emoji');
|
||||
remove_filter('comment_text_rss', 'wp_staticize_emoji');
|
||||
remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
|
||||
}
|
||||
|
||||
/** 禁用Google字体(Open Sans)。 */
|
||||
public function remove_open_sans(): void {
|
||||
wp_deregister_style('open-sans');
|
||||
wp_register_style('open-sans', false);
|
||||
wp_enqueue_style('open-sans', '');
|
||||
}
|
||||
|
||||
/** 清理wp_head带入的多余Meta标签。 */
|
||||
public function remove_more_wp_head(): void {
|
||||
remove_action('wp_head', 'feed_links_extra', 3);
|
||||
remove_action('wp_head', 'feed_links', 2);
|
||||
remove_action('wp_head', 'rsd_link');
|
||||
remove_action('wp_head', 'wlwmanifest_link');
|
||||
remove_action('wp_head', 'index_rel_link');
|
||||
remove_action('wp_head', 'parent_post_rel_link', 10, 0);
|
||||
remove_action('wp_head', 'start_post_rel_link', 10, 0);
|
||||
remove_action('wp_head', 'adjacent_posts_rel_link', 10, 0);
|
||||
remove_action('wp_head', 'wp_generator');
|
||||
remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0);
|
||||
remove_action('wp_head', 'wp_shortlink_wp_head', 10, 0);
|
||||
remove_action('wp_head', 'rest_output_link_wp_head', 10, 0);
|
||||
remove_action('wp_head', 'wp_oembed_add_discovery_links', 10, 1);
|
||||
remove_action('wp_head', 'rel_canonical', 10, 0);
|
||||
}
|
||||
|
||||
/** 移除仪表盘的站点健康、PHP/浏览器版本检查小组件。 */
|
||||
public function remove_dashboard_widgets($screen_id, $context): void {
|
||||
if ('dashboard' !== $screen_id || 'normal' !== $context) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wp_meta_boxes;
|
||||
|
||||
unset($wp_meta_boxes['dashboard']['normal']['high']['dashboard_php_nag']);
|
||||
unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_site_health']);
|
||||
unset($wp_meta_boxes['dashboard']['side']['core']['dashboard_primary']);
|
||||
}
|
||||
|
||||
/** 站点健康检查中移除页面缓存与PHP版本检测(非常耗时)。 */
|
||||
public function filter_site_status_tests(array $tests): array {
|
||||
if (isset($tests['async']['page_cache'])) {
|
||||
unset($tests['async']['page_cache']);
|
||||
}
|
||||
if (isset($tests['direct']['php_version'])) {
|
||||
unset($tests['direct']['php_version']);
|
||||
}
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/** 禁止后台联网检测PHP版本、浏览器与贡献者信息,提高后台加载速度。 */
|
||||
public function block_site_health_requests($pre, $parsed_args, $url) {
|
||||
$blocked = [
|
||||
'api.wordpress.org/core/serve-happy/',
|
||||
'api.wordpress.org/core/browse-happy/',
|
||||
'api.wordpress.org/core/credits/',
|
||||
];
|
||||
|
||||
foreach ($blocked as $arg) {
|
||||
if (strpos((string) $url, $arg) !== false) {
|
||||
return [
|
||||
'headers' => [],
|
||||
'body' => '',
|
||||
'response' => [
|
||||
'code' => false,
|
||||
'message' => false,
|
||||
],
|
||||
'cookies' => [],
|
||||
'http_response' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $pre;
|
||||
}
|
||||
|
||||
/** 禁止WordPress检测核心/插件/主题更新,并屏蔽更新通知。 */
|
||||
private function disable_wp_update(): void {
|
||||
remove_action('admin_init', '_maybe_update_core');
|
||||
remove_action('admin_init', '_maybe_update_plugins');
|
||||
remove_action('admin_init', '_maybe_update_themes');
|
||||
add_action('admin_menu', [$this, 'hide_update_notices']);
|
||||
|
||||
add_filter('automatic_updater_disabled', '__return_true');
|
||||
remove_action('init', 'wp_schedule_update_checks');
|
||||
remove_action('wp_version_check', 'wp_version_check');
|
||||
remove_action('wp_update_plugins', 'wp_update_plugins');
|
||||
remove_action('wp_update_themes', 'wp_update_themes');
|
||||
add_filter('auto_update_core', '__return_false');
|
||||
add_filter('allow_major_auto_core_updates', '__return_false');
|
||||
add_filter('allow_dev_auto_core_updates', '__return_false');
|
||||
add_filter('allow_minor_auto_core_updates', '__return_false');
|
||||
}
|
||||
|
||||
/** 禁用更新通知。 */
|
||||
public function hide_update_notices(): void {
|
||||
remove_action('admin_notices', 'update_nag', 3);
|
||||
remove_action('network_admin_notices', 'update_nag', 3);
|
||||
}
|
||||
|
||||
/** 禁止WP大尺寸缩略图裁剪(medium_large、1536x1536、2048x2048)。 */
|
||||
public function disable_large_thumbnail(): void {
|
||||
remove_image_size('1536x1536');
|
||||
remove_image_size('2048x2048');
|
||||
add_filter('intermediate_image_sizes', static function ($sizes) {
|
||||
return array_merge(array_diff((array) $sizes, ['medium_large', '1536x1536', '2048x2048']));
|
||||
}, 20);
|
||||
}
|
||||
|
||||
/** 禁用Auto Embed自动嵌入功能,提高页面渲染速度。 */
|
||||
private function disable_autoembed(): void {
|
||||
remove_filter('the_content', [$GLOBALS['wp_embed'], 'autoembed'], 8);
|
||||
remove_filter('widget_text_content', [$GLOBALS['wp_embed'], 'autoembed'], 8);
|
||||
remove_filter('widget_block_content', [$GLOBALS['wp_embed'], 'autoembed'], 8);
|
||||
remove_action('edit_form_advanced', [$GLOBALS['wp_embed'], 'maybe_run_ajax_cache']);
|
||||
remove_action('edit_page_form', [$GLOBALS['wp_embed'], 'maybe_run_ajax_cache']);
|
||||
}
|
||||
|
||||
/** 关闭后台用户编辑页面中的多余选项。 */
|
||||
public function hide_user_profile_fields(): void {
|
||||
echo '<style>.user-first-name-wrap,.user-last-name-wrap,.user-admin-bar-front-wrap,.user-comment-shortcuts-wrap,.user-syntax-highlighting-wrap,.user-rich-editing-wrap,.user-profile-picture .description,.user-language-wrap{display:none}</style>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* 前端页面编辑(后台「扩展&增强 → 系统工具」的 admin_frontend_set)。
|
||||
*
|
||||
* 对齐参考主题 zib-frontend-set.php:管理员在前台通过悬浮按钮打开弹窗,
|
||||
* 快速修改当前文章/页面的标题、副标题、布局、计数、评论开关或分类术语的
|
||||
* 名称与描述;保存走白名单,行为与后台 PostMetaBox / wp_update_term 一致。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Frontend;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PageAdminSet {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_filter('aksrt_float_right_buttons', [$this, 'float_button'], 10, 2);
|
||||
add_action('wp_ajax_aksrt_frontend_set_modal', [$this, 'ajax_modal']);
|
||||
add_action('wp_ajax_aksrt_frontend_set_save', [$this, 'ajax_save']);
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_script']);
|
||||
}
|
||||
|
||||
/** 开启条件:设置开关 + 管理员 + 单页/术语页 + 有效对象(排除投稿与导航页模板)。 */
|
||||
public static function enabled(): bool {
|
||||
if (!class_exists('\AKSRT\Settings\ExtensionRuntime') || !is_user_logged_in()) {
|
||||
return false;
|
||||
}
|
||||
$settings = \AKSRT\Settings\ExtensionRuntime::get_system_tools_settings();
|
||||
if (empty($settings['admin_frontend_set']) || !current_user_can('manage_options')) {
|
||||
return false;
|
||||
}
|
||||
if (!get_queried_object_id() || (!is_single() && !is_page() && !is_category() && !is_tag() && !is_tax())) {
|
||||
return false;
|
||||
}
|
||||
if (is_page_template('templates/pages/page-submit.php') || is_page_template('templates/pages/page-posts-navs.php')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 悬浮按钮:注入右侧悬浮按钮栏(对齐参考主题 zib_admin_frontend_set_botton)。 */
|
||||
public function float_button($buttons, $is_mobile) {
|
||||
if (!self::enabled()) {
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
$url = add_query_arg(
|
||||
[
|
||||
'action' => 'aksrt_frontend_set_modal',
|
||||
'_wpnonce' => wp_create_nonce('aksrt_frontend_set'),
|
||||
],
|
||||
admin_url('admin-ajax.php')
|
||||
);
|
||||
|
||||
$button = '<a class="float-btn admin-set-btn" href="javascript:;"'
|
||||
. ' data-aksrt-modal="remote" data-aksrt-modal-cache="false" data-aksrt-modal-kind="admin-set"'
|
||||
. ' data-aksrt-modal-url="' . esc_url($url) . '"'
|
||||
. aksrt_float_btn_tooltip_attr(__('在此编辑此页面参数', 'aksrt'), (bool) $is_mobile)
|
||||
. '><i class="fa fas fa-cog"></i></a>';
|
||||
|
||||
return $button . (string) $buttons;
|
||||
}
|
||||
|
||||
/** 条件加载弹窗提交脚本。 */
|
||||
public function enqueue_script(): void {
|
||||
if (!self::enabled()) {
|
||||
return;
|
||||
}
|
||||
$src = get_template_directory() . '/assets/js/page-admin-set.js';
|
||||
wp_enqueue_script(
|
||||
'aksrt-page-admin-set',
|
||||
get_template_directory_uri() . '/assets/js/page-admin-set.js',
|
||||
[],
|
||||
file_exists($src) ? (string) filemtime($src) : null,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/** GET 弹窗内容(注入 aksrt-modal-runtime)。 */
|
||||
public function ajax_modal(): void {
|
||||
check_ajax_referer('aksrt_frontend_set', '_wpnonce');
|
||||
if (!self::enabled()) {
|
||||
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$object_id = (int) get_queried_object_id();
|
||||
if (is_category() || is_tag() || is_tax()) {
|
||||
$html = $this->term_modal_html($object_id, (string) get_queried_object()->taxonomy);
|
||||
} else {
|
||||
$html = $this->post_modal_html($object_id, get_post_type($object_id) ?: 'post');
|
||||
}
|
||||
|
||||
wp_send_json_success(['html' => $html]);
|
||||
}
|
||||
|
||||
/** POST 保存:仅接受白名单字段。 */
|
||||
public function ajax_save(): void {
|
||||
check_ajax_referer('aksrt_frontend_set', '_wpnonce');
|
||||
if (!self::enabled()) {
|
||||
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$object_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
|
||||
if (!$object_id) {
|
||||
wp_send_json_error(['msg' => __('页面数据出错', 'aksrt')]);
|
||||
}
|
||||
|
||||
$type = isset($_POST['type']) ? sanitize_key((string) $_POST['type']) : '';
|
||||
if ($type === 'tax') {
|
||||
$taxonomy = isset($_POST['subtype']) ? sanitize_key((string) $_POST['subtype']) : '';
|
||||
$this->save_term($object_id, $taxonomy);
|
||||
} else {
|
||||
$this->save_post($object_id);
|
||||
}
|
||||
|
||||
wp_send_json_success(['msg' => __('保存成功,正在刷新页面', 'aksrt'), 'reload' => true]);
|
||||
}
|
||||
|
||||
private function save_post(int $post_id): void {
|
||||
$post = get_post($post_id);
|
||||
if (!$post) {
|
||||
wp_send_json_error(['msg' => __('未找到可用数据', 'aksrt')]);
|
||||
}
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$postarr = ['ID' => $post_id];
|
||||
if (isset($_POST['post_title'])) {
|
||||
$title = sanitize_text_field(wp_unslash((string) $_POST['post_title']));
|
||||
if ($title !== '') {
|
||||
$postarr['post_title'] = $title;
|
||||
}
|
||||
}
|
||||
if (isset($_POST['comments_open'])) {
|
||||
$postarr['comment_status'] = !empty($_POST['comments_open']) ? 'open' : 'closed';
|
||||
}
|
||||
if (count($postarr) > 1) {
|
||||
$result = wp_update_post($postarr, true);
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['msg' => $result->get_error_message()]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['post_format']) && post_type_supports($post->post_type, 'post-formats')) {
|
||||
$format = sanitize_key((string) wp_unslash($_POST['post_format']));
|
||||
set_post_format($post_id, $format === 'standard' ? '' : $format);
|
||||
}
|
||||
|
||||
if ($post->post_type === 'post' && class_exists('\AKSRT\Core\PostMetaBox')) {
|
||||
$layout = isset($_POST['show_layout']) ? sanitize_key((string) wp_unslash($_POST['show_layout'])) : '';
|
||||
if ($layout !== '' && !in_array($layout, ['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'], true)) {
|
||||
$layout = 'false';
|
||||
}
|
||||
|
||||
// 保留白名单之外的既有配置键(封面、视频等),仅覆盖弹窗提交的字段。
|
||||
// 复选框未勾选时浏览器不会提交键名,必须无条件写入两个开关。
|
||||
$meta = get_post_meta($post_id, \AKSRT\Core\PostMetaBox::META_KEY, true);
|
||||
$meta = is_array($meta) ? $meta : [];
|
||||
if ($layout !== '') {
|
||||
$meta['show_layout'] = $layout;
|
||||
}
|
||||
if (isset($_POST['subtitle'])) {
|
||||
$meta['subtitle'] = sanitize_text_field(wp_unslash((string) $_POST['subtitle']));
|
||||
}
|
||||
$meta['no_article-navs'] = !empty($_POST['no_article-navs']);
|
||||
$meta['article_maxheight_xz'] = !empty($_POST['article_maxheight_xz']);
|
||||
update_post_meta($post_id, \AKSRT\Core\PostMetaBox::META_KEY, $meta);
|
||||
|
||||
if (isset($_POST['views'])) {
|
||||
update_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, max(0, (int) $_POST['views']));
|
||||
}
|
||||
if (isset($_POST['like'])) {
|
||||
update_post_meta($post_id, '_aksrt_like_count', max(0, (int) $_POST['like']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function save_term(int $term_id, string $taxonomy = ''): void {
|
||||
$term = $taxonomy !== '' ? get_term($term_id, $taxonomy) : get_term($term_id);
|
||||
if (!$term || is_wp_error($term)) {
|
||||
wp_send_json_error(['msg' => __('未找到可用数据', 'aksrt')]);
|
||||
}
|
||||
if (!current_user_can('manage_categories') && !current_user_can('manage_terms')) {
|
||||
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
if (isset($_POST['term_name'])) {
|
||||
$name = sanitize_text_field(wp_unslash((string) $_POST['term_name']));
|
||||
if ($name !== '') {
|
||||
$data['name'] = $name;
|
||||
}
|
||||
}
|
||||
if (isset($_POST['term_description'])) {
|
||||
$data['description'] = sanitize_textarea_field(wp_unslash((string) $_POST['term_description']));
|
||||
}
|
||||
if (!$data) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = wp_update_term($term_id, (string) $term->taxonomy, $data);
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['msg' => $result->get_error_message()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function post_modal_html(int $post_id, string $post_type): string {
|
||||
$meta = class_exists('\AKSRT\Core\PostMetaBox')
|
||||
? \AKSRT\Core\PostMetaBox::get_meta($post_id)
|
||||
: [];
|
||||
$views = (int) ($meta['views'] ?? 0);
|
||||
$like = (int) ($meta['like'] ?? 0);
|
||||
$is_single = is_single();
|
||||
|
||||
$html = '<div class="aksrt-admin-set">';
|
||||
$html .= '<p class="em09 muted-3-color mb10">' . esc_html(get_the_title($post_id)) . '</p>';
|
||||
|
||||
$html .= $this->text_field('post_title', __('标题', 'aksrt'), (string) get_the_title($post_id));
|
||||
|
||||
if ($is_single) {
|
||||
$html .= $this->text_field('subtitle', __('副标题', 'aksrt'), (string) ($meta['subtitle'] ?? ''));
|
||||
$layout = (string) ($meta['show_layout'] ?? 'false');
|
||||
$html .= '<div class="relative line-form mb10 modal-line-field"><select name="show_layout" class="line-form-input">'
|
||||
. '<option value="false"' . selected($layout, 'false', false) . '>' . esc_html__('跟随主题', 'aksrt') . '</option>'
|
||||
. '<option value="no_sidebar"' . selected($layout, 'no_sidebar', false) . '>' . esc_html__('无侧边栏', 'aksrt') . '</option>'
|
||||
. '<option value="sidebar_left"' . selected($layout, 'sidebar_left', false) . '>' . esc_html__('侧边栏靠左', 'aksrt') . '</option>'
|
||||
. '<option value="sidebar_right"' . selected($layout, 'sidebar_right', false) . '>' . esc_html__('侧边栏靠右', 'aksrt') . '</option>'
|
||||
. '</select><i class="line-form-line"></i></div>';
|
||||
$html .= $this->number_field('like', __('点赞数', 'aksrt'), $like);
|
||||
$html .= $this->number_field('views', __('阅读数', 'aksrt'), $views);
|
||||
$html .= $this->checkbox_field('no_article-navs', __('不显示目录树', 'aksrt'), !empty($meta['no_article-navs']));
|
||||
$html .= $this->checkbox_field('article_maxheight_xz', __('限制文章最大高度', 'aksrt'), !empty($meta['article_maxheight_xz']));
|
||||
}
|
||||
|
||||
$html .= '<div class="relative line-form mb10 modal-line-field"><select name="comments_open" class="line-form-input">'
|
||||
. '<option value="open"' . selected(comments_open($post_id), true, false) . '>' . esc_html__('开启评论', 'aksrt') . '</option>'
|
||||
. '<option value=""' . selected(comments_open($post_id), false, false) . '>' . esc_html__('关闭评论', 'aksrt') . '</option>'
|
||||
. '</select><i class="line-form-line"></i></div>';
|
||||
|
||||
$html .= $this->hidden_fields('post', $post_id, $post_type);
|
||||
$html .= '<div class="flex jsb mt10"><button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>'
|
||||
. '<button type="submit" class="but jb-blue aksrt-admin-set-submit"><i class="fa fas fa-check"></i> ' . esc_html__('确认修改', 'aksrt') . '</button></div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $this->wrap_form($html);
|
||||
}
|
||||
|
||||
private function term_modal_html(int $term_id, string $taxonomy): string {
|
||||
$term = get_term($term_id, $taxonomy);
|
||||
if (!$term || is_wp_error($term)) {
|
||||
return '<div class="aksrt-admin-set"><p class="muted-3-color">' . esc_html__('未找到可用数据', 'aksrt') . '</p></div>';
|
||||
}
|
||||
|
||||
$html = '<div class="aksrt-admin-set">';
|
||||
$html .= '<p class="em09 muted-3-color mb10">' . esc_html($term->name) . '</p>';
|
||||
$html .= $this->text_field('term_name', __('名称', 'aksrt'), (string) $term->name);
|
||||
$html .= $this->text_field('term_description', __('描述', 'aksrt'), (string) $term->description);
|
||||
$html .= $this->hidden_fields('tax', $term_id, $taxonomy);
|
||||
$html .= '<div class="flex jsb mt10"><button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>'
|
||||
. '<button type="submit" class="but jb-blue aksrt-admin-set-submit"><i class="fa fas fa-check"></i> ' . esc_html__('确认修改', 'aksrt') . '</button></div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $this->wrap_form($html);
|
||||
}
|
||||
|
||||
private function wrap_form(string $inner): string {
|
||||
return '<form class="aksrt-admin-set-form">'
|
||||
. '<input type="hidden" name="action" value="aksrt_frontend_set_save">'
|
||||
. wp_nonce_field('aksrt_frontend_set', '_wpnonce', true, false)
|
||||
. $inner
|
||||
. '</form>';
|
||||
}
|
||||
|
||||
private function hidden_fields(string $type, int $id, string $subtype): string {
|
||||
return '<input type="hidden" name="type" value="' . esc_attr($type) . '">'
|
||||
. '<input type="hidden" name="subtype" value="' . esc_attr($subtype) . '">'
|
||||
. '<input type="hidden" name="id" value="' . esc_attr((string) $id) . '">';
|
||||
}
|
||||
|
||||
private function text_field(string $name, string $label, string $value): string {
|
||||
return '<div class="relative line-form mb10 modal-line-field"><input type="text" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" class="line-form-input">'
|
||||
. '<div class="scale-placeholder">' . esc_html($label) . '</div><i class="line-form-line"></i></div>';
|
||||
}
|
||||
|
||||
private function number_field(string $name, string $label, int $value): string {
|
||||
return '<div class="relative line-form mb10 modal-line-field"><input type="number" min="0" step="1" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '" class="line-form-input">'
|
||||
. '<div class="scale-placeholder">' . esc_html($label) . '</div><i class="line-form-line"></i></div>';
|
||||
}
|
||||
|
||||
private function checkbox_field(string $name, string $label, bool $checked): string {
|
||||
return '<div class="mb10 form-switch-label"><label><input type="checkbox" name="' . esc_attr($name) . '" value="1"' . checked($checked, true, false) . '> ' . esc_html($label) . '</label></div>';
|
||||
}
|
||||
}
|
||||
@@ -189,6 +189,55 @@ class ExtensionRuntime {
|
||||
];
|
||||
}
|
||||
|
||||
/** 获取系统工具设置(WordPress 优化、上传优化、编辑器与显示)。 */
|
||||
public static function get_system_tools_settings(): array {
|
||||
$settings = Settings::get_all();
|
||||
|
||||
$switch = static function (string $key, bool $default) use ($settings): bool {
|
||||
return !isset($settings[$key]) ? $default : !empty($settings[$key]);
|
||||
};
|
||||
|
||||
$rename_type = sanitize_key((string) ($settings['newfilename_type'] ?? 'time'));
|
||||
if (!in_array($rename_type, ['random', 'time'], true)) {
|
||||
$rename_type = 'time';
|
||||
}
|
||||
|
||||
// Only fill defaults when the key was never saved — empty array means "none".
|
||||
$toolbar_scenes = ['pc_s', 'm_s'];
|
||||
if (!array_key_exists('mce_float_toolbar', $settings)) {
|
||||
$float_toolbar = $toolbar_scenes;
|
||||
} else {
|
||||
$float_toolbar = [];
|
||||
foreach ((array) ($settings['mce_float_toolbar'] ?? []) as $scene) {
|
||||
$scene = sanitize_key((string) $scene);
|
||||
if (in_array($scene, $toolbar_scenes, true) && !in_array($scene, $float_toolbar, true)) {
|
||||
$float_toolbar[] = $scene;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'hide_admin_bar' => $switch('hide_admin_bar', true),
|
||||
'remove_emoji' => $switch('remove_emoji', true),
|
||||
'remove_open_sans' => $switch('remove_open_sans', true),
|
||||
'remove_more_wp_head' => $switch('remove_more_wp_head', true),
|
||||
'remove_dashboard_widgets' => $switch('remove_dashboard_widgets', true),
|
||||
'display_wp_update' => $switch('display_wp_update', true),
|
||||
'split_upload_s' => $switch('split_upload_s', true),
|
||||
'split_minimum_size' => max(4, min(100, (int) ($settings['split_minimum_size'] ?? 20))),
|
||||
'newfilename' => $switch('newfilename', false),
|
||||
'newfilename_type' => $rename_type,
|
||||
'disabled_large_thumbnail' => $switch('disabled_large_thumbnail', true),
|
||||
'admin_user_del_fields' => $switch('admin_user_del_fields', true),
|
||||
'admin_frontend_set' => $switch('admin_frontend_set', true),
|
||||
'close_gutenberg' => $switch('close_gutenberg', false),
|
||||
'disabled_autoembed' => $switch('disabled_autoembed', true),
|
||||
'mce_float_toolbar' => $float_toolbar,
|
||||
'time_ago_s' => $switch('time_ago_s', true),
|
||||
'time_format' => (string) ($settings['time_format'] ?? 'n月j日 H:i'),
|
||||
];
|
||||
}
|
||||
|
||||
/** 获取短信接口设置。 */
|
||||
public static function get_sms_settings(): array {
|
||||
$settings = Settings::get_all();
|
||||
|
||||
@@ -80,6 +80,11 @@ if (file_exists($js)) {
|
||||
$image_upload_multiple = class_exists('\AKSRT\Attachment\UploadLimits')
|
||||
? \AKSRT\Attachment\UploadLimits::image_batch_limit()
|
||||
: 6;
|
||||
$system_tools = class_exists('\AKSRT\Settings\ExtensionRuntime')
|
||||
? \AKSRT\Settings\ExtensionRuntime::get_system_tools_settings()
|
||||
: [];
|
||||
$float_scenes = is_array($system_tools['mce_float_toolbar'] ?? null) ? $system_tools['mce_float_toolbar'] : ['pc_s', 'm_s'];
|
||||
$float_toolbar = wp_is_mobile() ? in_array('m_s', $float_scenes, true) : in_array('pc_s', $float_scenes, true);
|
||||
|
||||
wp_localize_script('aksrt-front-submit-editor', 'aksrtSubmitData', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
@@ -95,6 +100,10 @@ if (file_exists($js)) {
|
||||
'file' => ['url' => esc_url_raw(rest_url('aksrt/v1/front-submit/media/file'))],
|
||||
],
|
||||
|
||||
'splitUpload' => [
|
||||
'enabled' => !empty($system_tools['split_upload_s']),
|
||||
'minSize' => (int) ($system_tools['split_minimum_size'] ?? 20),
|
||||
],
|
||||
|
||||
'imageUploadMultiple' => $image_upload_multiple,
|
||||
'ai' => [
|
||||
@@ -109,6 +118,7 @@ if (file_exists($js)) {
|
||||
'canUploadFile' => !empty($ctx['can_upload_file']),
|
||||
'canHide' => !empty($ctx['can_hide']),
|
||||
'canPay' => !empty($ctx['can_pay']),
|
||||
'floatToolbar' => $float_toolbar,
|
||||
],
|
||||
'uploads' => [
|
||||
'image' => !empty($ctx['can_upload_img']) ? [
|
||||
|
||||
@@ -58,6 +58,7 @@ namespace {
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsAudit.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsPush.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsSecurity.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsSystem.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensions.php';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/** Split upload (分片上传) runtime contract harness: chunk params, part naming, upload/status/merge protocol. */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
define('HOUR_IN_SECONDS', 3600);
|
||||
define('WP_CONTENT_DIR', sys_get_temp_dir() . '/aksrt-split-test-' . getmypid());
|
||||
|
||||
function sanitize_file_name(string $name): string { return preg_replace('/[^A-Za-z0-9._\-]/', '', $name) ?? ''; }
|
||||
function __(string $text, string $domain = ''): string { return $text; }
|
||||
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
|
||||
function get_current_blog_id(): int { return 1; }
|
||||
function get_current_user_id(): int { return 7; }
|
||||
function apply_filters(string $tag, $value) { return $value; }
|
||||
function wp_mkdir_p(string $dir): bool { return is_dir($dir) || mkdir($dir, 0777, true); }
|
||||
function is_wp_error($thing): bool { return $thing instanceof \WP_Error; }
|
||||
function size_format(int $bytes): string { return $bytes . 'B'; }
|
||||
function get_allowed_mime_types(): array { return ['mp4' => 'video/mp4']; }
|
||||
function wp_check_filetype_and_ext(string $file, string $name, array $mimes = []): array {
|
||||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||||
return ['ext' => $ext, 'type' => $mimes[$ext] ?? ''];
|
||||
}
|
||||
|
||||
class WP_Error {
|
||||
private array $errors;
|
||||
public function __construct($code = '', $message = '') { $this->errors = [$code => $message]; }
|
||||
public function get_error_message(): string { return (string) reset($this->errors); }
|
||||
public function get_error_code() { return (string) array_key_first($this->errors); }
|
||||
}
|
||||
|
||||
class WP_REST_Server {
|
||||
const READABLE = 'GET';
|
||||
}
|
||||
|
||||
class WP_REST_Response {
|
||||
private array $data;
|
||||
private int $status;
|
||||
public function __construct($data = [], int $status = 200) { $this->data = $data; $this->status = $status; }
|
||||
public function get_data(): array { return $this->data; }
|
||||
public function get_status(): int { return $this->status; }
|
||||
}
|
||||
|
||||
class WP_REST_Request {
|
||||
}
|
||||
|
||||
class FakeRestRequest extends \WP_REST_Request {
|
||||
private array $params;
|
||||
private array $files;
|
||||
public function __construct(array $params = [], array $files = []) {
|
||||
$this->params = $params;
|
||||
$this->files = $files;
|
||||
}
|
||||
public function get_param(string $key) { return $this->params[$key] ?? null; }
|
||||
public function get_file_params(): array { return $this->files; }
|
||||
}
|
||||
|
||||
function split_upload_check(bool $condition, string $message): void {
|
||||
if (!$condition) throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
require dirname(__DIR__, 2) . '/src/Core/FrontSubmitChunk.php';
|
||||
}
|
||||
|
||||
namespace AKSRT\Core {
|
||||
final class FakeFrontSubmit {
|
||||
use \AKSRT\Core\FrontSubmitChunk;
|
||||
|
||||
public static int $limit_bytes = 10 * 1048576;
|
||||
public static array $persisted = [];
|
||||
|
||||
public static function upload_limit_bytes(string $kind): int {
|
||||
return self::$limit_bytes;
|
||||
}
|
||||
|
||||
/** Stand-in for FrontSubmit::persist_uploaded_file(): records and echoes merged content. */
|
||||
public function persist_uploaded_file(string $kind, array $file, bool $sideload = false) {
|
||||
self::$persisted[] = ['kind' => $kind, 'file' => $file, 'sideload' => $sideload];
|
||||
return 'persisted:' . (string) file_get_contents((string) $file['tmp_name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
use AKSRT\Core\FakeFrontSubmit;
|
||||
|
||||
$submit = new FakeFrontSubmit();
|
||||
$temp_dir = WP_CONTENT_DIR . '/aksrt-temp';
|
||||
|
||||
// 1) Chunk parameter validation.
|
||||
$err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => '', 'chunks' => 3, 'chunk' => 0, 'size' => 10]));
|
||||
split_upload_check($err instanceof \WP_Error, 'empty name should be rejected');
|
||||
$err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => 'a.mp4', 'chunks' => 0, 'chunk' => 0, 'size' => 10]));
|
||||
split_upload_check($err instanceof \WP_Error, 'zero chunk count should be rejected');
|
||||
$err = $submit->rest_upload_chunk(new FakeRestRequest(['name' => 'a.mp4', 'chunks' => 3, 'chunk' => 5, 'size' => 10]));
|
||||
split_upload_check($err instanceof \WP_Error, 'out-of-range chunk index should be rejected');
|
||||
|
||||
// 2) Chunk upload lands per-index .part files, oversized total is refused.
|
||||
$make_file = static function (string $content): string {
|
||||
$path = tempnam(sys_get_temp_dir(), 'chunk');
|
||||
file_put_contents($path, $content);
|
||||
return $path;
|
||||
};
|
||||
$params = static fn (int $chunk, int $chunks, int $size): array => [
|
||||
'kind' => 'video', 'chunk' => $chunk, 'chunks' => $chunks, 'name' => 'movie.mp4', 'size' => $size,
|
||||
];
|
||||
|
||||
$response = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 3, 6), ['file' => ['tmp_name' => $make_file('AAA'), 'error' => 0]]));
|
||||
split_upload_check($response instanceof \WP_REST_Response, 'first chunk should upload');
|
||||
split_upload_check($response->get_data()['uploaded_size'] === 3, 'uploaded_size should count the stored chunk');
|
||||
$submit->rest_upload_chunk(new FakeRestRequest($params(2, 3, 6), ['file' => ['tmp_name' => $make_file('CCC'), 'error' => 0]]));
|
||||
|
||||
$parts = glob($temp_dir . '/*.part');
|
||||
split_upload_check(count($parts) === 2, 'two part files should exist');
|
||||
foreach ($parts as $part) {
|
||||
split_upload_check(strpos(basename($part), '1-') === 0, 'part name should be blog-id prefixed');
|
||||
}
|
||||
|
||||
// 3) Status reports the uploaded indexes (resume contract).
|
||||
$response = $submit->rest_upload_chunk_status(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3]));
|
||||
split_upload_check($response instanceof \WP_REST_Response, 'status should respond');
|
||||
split_upload_check($response->get_data()['uploaded'] === [0, 2], 'status should list uploaded chunk indexes');
|
||||
|
||||
// 4) Merge refuses when a chunk is missing.
|
||||
$err = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 9]));
|
||||
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_missing', 'merge should refuse with missing chunks');
|
||||
|
||||
// 5) Merge with a size mismatch is refused.
|
||||
$submit->rest_upload_chunk(new FakeRestRequest($params(1, 3, 6), ['file' => ['tmp_name' => $make_file('BBB'), 'error' => 0]]));
|
||||
$err = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 999]));
|
||||
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_size_mismatch', 'size mismatch should refuse the merge');
|
||||
|
||||
// 6) Happy-path merge concatenates in order, flags sideload, and cleans up.
|
||||
$response = $submit->rest_upload_chunk_merge(new FakeRestRequest(['kind' => 'video', 'name' => 'movie.mp4', 'chunks' => 3, 'size' => 9]));
|
||||
split_upload_check(is_string($response) && $response === 'persisted:AAABBBCCC', 'merge should concatenate chunks in order');
|
||||
split_upload_check(count(FakeFrontSubmit::$persisted) === 1, 'merge should persist exactly once');
|
||||
$call = FakeFrontSubmit::$persisted[0];
|
||||
split_upload_check($call['kind'] === 'video' && $call['sideload'] === true, 'merge should persist as a sideloaded video');
|
||||
split_upload_check($call['file']['name'] === 'movie.mp4' && $call['file']['size'] === 9, 'merge should rebuild the $_FILES-style payload');
|
||||
split_upload_check(glob($temp_dir . '/*.part') === [], 'merge should clean up all parts');
|
||||
|
||||
// 7) Per-user keying: a different file name never sees another name's parts.
|
||||
$response = $submit->rest_upload_chunk_status(new FakeRestRequest(['kind' => 'video', 'name' => 'other.mp4', 'chunks' => 3]));
|
||||
split_upload_check($response instanceof \WP_REST_Response && $response->get_data()['uploaded'] === [], 'a different name should start with no uploaded chunks');
|
||||
|
||||
// 8) Chunk size cap refuses oversized chunk blobs.
|
||||
$big = $make_file(str_repeat('x', 3 * 1048576));
|
||||
$err = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 1, 3 * 1048576), ['file' => ['tmp_name' => $big, 'error' => 0]]));
|
||||
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_chunk_invalid_size', 'oversized chunk blobs should be refused');
|
||||
|
||||
// 9) Kind size limit caps cumulative uploads.
|
||||
FakeFrontSubmit::$limit_bytes = 4;
|
||||
$err = $submit->rest_upload_chunk(new FakeRestRequest($params(0, 2, 9), ['file' => ['tmp_name' => $make_file('AAAAA'), 'error' => 0]]));
|
||||
split_upload_check($err instanceof \WP_Error && $err->get_error_code() === 'aksrt_upload_too_large', 'cumulative size over the kind limit should be refused');
|
||||
|
||||
// Cleanup.
|
||||
FakeFrontSubmit::$persisted = [];
|
||||
foreach ((array) glob(WP_CONTENT_DIR . '/*.part') as $leftover) {
|
||||
@unlink((string) $leftover);
|
||||
}
|
||||
@unlink(WP_CONTENT_DIR . '/aksrt-temp/index.php');
|
||||
@rmdir(WP_CONTENT_DIR . '/aksrt-temp');
|
||||
@rmdir(WP_CONTENT_DIR);
|
||||
|
||||
echo "Split upload harness passed\n";
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
|
||||
const SYSTEM_FIELDS = [
|
||||
'hide_admin_bar', 'remove_emoji', 'remove_open_sans', 'remove_more_wp_head',
|
||||
'remove_dashboard_widgets', 'display_wp_update',
|
||||
'split_upload_s', 'split_minimum_size', 'newfilename', 'newfilename_type',
|
||||
'disabled_large_thumbnail',
|
||||
'admin_user_del_fields', 'admin_frontend_set', 'close_gutenberg', 'disabled_autoembed',
|
||||
'mce_float_toolbar', 'time_ago_s', 'time_format',
|
||||
];
|
||||
|
||||
test('system tools schema registers the 系统工具 page under 扩展&增强', () => {
|
||||
const schema = read('src/Admin/Schema/SchemaExtensionsSystem.php');
|
||||
const registry = read('src/Admin/Schema/SchemaExtensions.php');
|
||||
const clientMirror = read('assets/admin/src/schema/sections/extensions/system.js');
|
||||
const clientIndex = read('assets/admin/src/schema/sections/extensions/index.js');
|
||||
const nav = read('assets/admin/src/constants/sections/extensions.js');
|
||||
const sectionDefaults = read('assets/admin/src/constants/sections/index.js');
|
||||
|
||||
assert.match(registry, /'system'\s+=> SchemaExtensionsSystem::build\(\)/);
|
||||
assert.match(schema, /'key'\s+=> 'system'/);
|
||||
assert.match(schema, /'title'\s+=> '系统工具'/);
|
||||
for (const key of SYSTEM_FIELDS) {
|
||||
assert.match(schema, new RegExp(`'${key}'`), `missing schema field ${key}`);
|
||||
assert.match(clientMirror, new RegExp(`${key}`), `missing client mirror field ${key}`);
|
||||
}
|
||||
// Conditional fields mirror the reference theme's dependency rules.
|
||||
assert.match(schema, /'newfilename_type'[^[]*'dependsOn'\s+=> \['key' => 'newfilename', 'value' => true\]/s);
|
||||
assert.match(schema, /'split_minimum_size'[^[]*'dependsOn'\s+=> \['key' => 'split_upload_s', 'value' => true\]/s);
|
||||
assert.match(schema, /'time_format'[^[]*'dependsOn'\s+=> \['key' => 'time_ago_s', 'operator' => 'falsy'\]/s);
|
||||
assert.match(clientMirror, /split_minimum_size[\s\S]*dependsOn:\s*\{\s*key:\s*'split_upload_s',\s*value:\s*true\s*\}/);
|
||||
assert.match(clientMirror, /time_format[\s\S]*dependsOn:\s*\{\s*key:\s*'time_ago_s',\s*operator:\s*'falsy'\s*\}/);
|
||||
assert.match(clientIndex, /import \{ systemSchema \} from '\.\/system\.js'/);
|
||||
assert.match(clientIndex, /system: systemSchema/);
|
||||
assert.match(nav, /key: 'extensions-system'/);
|
||||
assert.match(nav, /route: '\/extensions\/system'/);
|
||||
assert.match(nav, /section: 'system'/);
|
||||
// 系统工具 is the first child of 扩展&增强 (reference theme order) and the default landing page.
|
||||
assert.ok(nav.indexOf('extensions-system') < nav.indexOf('extensions-ip'), '系统工具 should precede IP归属地');
|
||||
assert.match(sectionDefaults, /extensions:\s*'extensions-system'/);
|
||||
});
|
||||
|
||||
test('system tools settings are consumed at runtime and initialized on boot', () => {
|
||||
const runtime = read('src/Settings/ExtensionRuntime.php');
|
||||
const bootstrap = read('functions.php');
|
||||
|
||||
assert.match(runtime, /public static function get_system_tools_settings\(\): array/);
|
||||
assert.match(runtime, /'newfilename_type'\s+=> \$rename_type/);
|
||||
assert.match(runtime, /'split_minimum_size'\s+=> max\(4, min\(100, \(int\) \(\$settings\['split_minimum_size'\] \?\? 20\)\)\)/);
|
||||
assert.match(bootstrap, /\\AKSRT\\Extensions\\SystemTools::get_instance\(\)->init\(\)/);
|
||||
assert.match(bootstrap, /\\AKSRT\\Frontend\\PageAdminSet::get_instance\(\)->init\(\)/);
|
||||
});
|
||||
|
||||
test('system tools switches wire the reference theme hook suite', () => {
|
||||
const tools = read('src/Extensions/SystemTools.php');
|
||||
|
||||
assert.match(tools, /!empty\(\$settings\['hide_admin_bar'\]\) \|\| !is_super_admin\(\)/);
|
||||
assert.match(tools, /remove_action\('wp_head', 'print_emoji_detection_script', 7\)/);
|
||||
assert.match(tools, /remove_filter\('wp_mail', 'wp_staticize_emoji_for_email'\)/);
|
||||
assert.match(tools, /add_action\('init', \[\$this, 'remove_open_sans'\]\)/);
|
||||
assert.match(tools, /wp_deregister_style\('open-sans'\)/);
|
||||
assert.match(tools, /add_action\('after_setup_theme', \[\$this, 'remove_more_wp_head'\]\)/);
|
||||
assert.match(tools, /remove_action\('wp_head', 'wp_generator'\)/);
|
||||
assert.match(tools, /add_action\('do_meta_boxes', \[\$this, 'remove_dashboard_widgets'\], 10, 3\)/);
|
||||
assert.match(tools, /unset\(\$wp_meta_boxes\['dashboard'\]\['normal'\]\['high'\]\['dashboard_php_nag'\]\)/);
|
||||
assert.match(tools, /add_filter\('site_status_tests', \[\$this, 'filter_site_status_tests'\]\)/);
|
||||
assert.match(tools, /add_filter\('pre_http_request', \[\$this, 'block_site_health_requests'\], 10, 3\)/);
|
||||
assert.match(tools, /'api\.wordpress\.org\/core\/serve-happy\/'/);
|
||||
assert.match(tools, /remove_action\('admin_init', '_maybe_update_core'\)/);
|
||||
assert.match(tools, /add_filter\('automatic_updater_disabled', '__return_true'\)/);
|
||||
assert.match(tools, /remove_action\('wp_version_check', 'wp_version_check'\)/);
|
||||
assert.match(tools, /add_action\('admin_menu', \[\$this, 'hide_update_notices'\]\)/);
|
||||
assert.match(tools, /remove_action\('admin_notices', 'update_nag', 3\)/);
|
||||
assert.match(tools, /remove_image_size\('1536x1536'\)/);
|
||||
assert.match(tools, /add_filter\('intermediate_image_sizes', static function \(\$sizes\)/);
|
||||
assert.match(tools, /add_filter\('use_block_editor_for_post', '__return_false'\)/);
|
||||
assert.match(tools, /remove_filter\('the_content', \[\$GLOBALS\['wp_embed'\], 'autoembed'\], 8\)/);
|
||||
assert.match(tools, /add_action\('show_user_profile', \[\$this, 'hide_user_profile_fields'\]\)/);
|
||||
assert.match(tools, /add_action\('edit_user_profile', \[\$this, 'hide_user_profile_fields'\]\)/);
|
||||
});
|
||||
|
||||
test('upload rename settings are canonical to 系统工具 and consumed by the upload pipeline', () => {
|
||||
const permissionsSchema = read('src/Admin/Schema/SchemaPermissions.php');
|
||||
const permissionsMirror = read('assets/admin/src/schema/sections/permissions/index.js');
|
||||
const systemSchema = read('src/Admin/Schema/SchemaExtensionsSystem.php');
|
||||
const uploadLimits = read('src/Attachment/UploadLimits.php');
|
||||
|
||||
assert.doesNotMatch(permissionsSchema, /newfilename/);
|
||||
assert.doesNotMatch(permissionsMirror, /newfilename/);
|
||||
assert.match(systemSchema, /'newfilename'/);
|
||||
assert.match(uploadLimits, /add_filter\('wp_handle_upload_prefilter', \[\$this, 'rename_upload_file'\]/);
|
||||
assert.match(uploadLimits, /\$settings\['newfilename_type'\] \?\? 'time'\) === 'random'/);
|
||||
});
|
||||
|
||||
test('split upload ships a full chunk protocol: routes, merge pipeline, and client uploader', () => {
|
||||
const chunk = read('src/Core/FrontSubmitChunk.php');
|
||||
const frontSubmit = read('src/Core/FrontSubmit.php');
|
||||
const editorJs = read('assets/js/front-submit-editor.js');
|
||||
const submitPage = read('templates/pages/page-submit.php');
|
||||
const schema = read('src/Admin/Schema/SchemaExtensionsSystem.php');
|
||||
|
||||
// The submit class consumes the chunk trait and registers its routes.
|
||||
assert.match(frontSubmit, /use FrontSubmitChunk;/);
|
||||
assert.match(frontSubmit, /\$this->register_chunk_routes\(\)/);
|
||||
assert.match(frontSubmit, /public function persist_uploaded_file\(string \$kind, array \$file, bool \$sideload = false\)/);
|
||||
|
||||
// Backend mirrors the reference theme's user_split_upload protocol.
|
||||
assert.match(chunk, /\/front-submit\/upload-chunk\/\(\?P<kind>image\|video\|resource\)/);
|
||||
assert.match(chunk, /\/front-submit\/upload-merge\(\?P<kind>image\|video\|resource\)|\/front-submit\/upload-merge\/\(\?P<kind>image\|video\|resource\)/);
|
||||
assert.match(chunk, /\/front-submit\/upload-status\/\(\?P<kind>image\|video\|resource\)/);
|
||||
assert.match(chunk, /permission_callback' => \[\$this, 'can_upload_attachment'\]/);
|
||||
assert.match(chunk, /const CHUNK_SIZE_BYTES = 2 \* 1048576;/);
|
||||
assert.match(chunk, /md5\(get_current_blog_id\(\) \. '\|' \. get_current_user_id\(\) \. '\|' \. \$name\)/);
|
||||
assert.match(chunk, /\$this->persist_uploaded_file\(\$params\['kind'\], \[/);
|
||||
assert.match(chunk, /apply_filters\('aksrt_temp_dir', WP_CONTENT_DIR \. '\/aksrt-temp'\)/);
|
||||
assert.match(chunk, /\$this->chunk_temp_cleanup\(\$temp_dir\)/);
|
||||
|
||||
// Whole-file upload and chunk merge share one persistence pipeline; merged
|
||||
// files are not HTTP uploads, so the pipeline must use the sideload action.
|
||||
assert.match(frontSubmit, /return \$this->persist_uploaded_file\(\$kind, \$file, false\);/);
|
||||
assert.match(frontSubmit, /'action' => 'wp_handle_sideload'/);
|
||||
assert.match(frontSubmit, /media_handle_upload\('aksrt_front_submit_file', 0, \[\], \$overrides\)/);
|
||||
|
||||
// Client uploader picks the chunked path past the threshold and resumes from status.
|
||||
assert.match(editorJs, /function uploadInChunks\(file, type\)/);
|
||||
assert.match(editorJs, /splitUploadConfig\(\)/);
|
||||
assert.match(editorJs, /file\.size > split\.minSize \* 1048576/);
|
||||
assert.match(editorJs, /chunkUrl\('upload-status'\)/);
|
||||
assert.match(editorJs, /chunkUrl\('upload-chunk'\)/);
|
||||
assert.match(editorJs, /chunkUrl\('upload-merge'\)/);
|
||||
assert.match(editorJs, /'\/front-submit\/' \+ action \+ '\/' \+ uploadKey/);
|
||||
|
||||
// Config flows from the settings page into aksrtSubmitData.
|
||||
assert.match(submitPage, /'splitUpload' => \[/);
|
||||
assert.match(submitPage, /'enabled' => !empty\(\$system_tools\['split_upload_s'\]\)/);
|
||||
assert.match(submitPage, /'minSize' => \(int\) \(\$system_tools\['split_minimum_size'\] \?\? 20\)/);
|
||||
assert.match(schema, /'split_upload_s'/);
|
||||
assert.match(schema, /'split_minimum_size'/);
|
||||
|
||||
// The legacy dead config keys stay out of the shared script payload.
|
||||
assert.doesNotMatch(read('src/Core/ScriptsPart1c.php'), /is_split_upload|split_minimum_size/);
|
||||
});
|
||||
|
||||
test('frontend page edit ships gate, float button, modal, and whitelisted save', () => {
|
||||
const pageAdminSet = read('src/Frontend/PageAdminSet.php');
|
||||
const pageAdminJs = read('assets/js/page-admin-set.js');
|
||||
const floatButtons = read('inc/helpers/aksrt-float-buttons.php');
|
||||
const bootstrap = read('functions.php');
|
||||
|
||||
assert.match(bootstrap, /\\AKSRT\\Frontend\\PageAdminSet::get_instance\(\)->init\(\)/);
|
||||
assert.match(pageAdminSet, /public static function enabled\(\): bool/);
|
||||
assert.match(pageAdminSet, /empty\(\$settings\['admin_frontend_set'\]/);
|
||||
assert.match(pageAdminSet, /current_user_can\('manage_options'\)/);
|
||||
assert.match(pageAdminSet, /is_page_template\('templates\/pages\/page-submit\.php'\)/);
|
||||
assert.match(pageAdminSet, /is_page_template\('templates\/pages\/page-posts-navs\.php'\)/);
|
||||
|
||||
// Float bar gains the same extension point the reference theme hooks into.
|
||||
assert.match(floatButtons, /apply_filters\('aksrt_float_right_buttons', \$btn, \$is_mobile\)/);
|
||||
assert.match(pageAdminSet, /add_filter\('aksrt_float_right_buttons', \[\$this, 'float_button'\], 10, 2\)/);
|
||||
assert.match(pageAdminSet, /data-aksrt-modal="remote"/);
|
||||
assert.match(pageAdminSet, /check_ajax_referer\('aksrt_frontend_set', '_wpnonce'\)/);
|
||||
|
||||
// Modal + save endpoints; save is whitelisted and preserves other meta keys.
|
||||
assert.match(pageAdminSet, /add_action\('wp_ajax_aksrt_frontend_set_modal', \[\$this, 'ajax_modal'\]\)/);
|
||||
assert.match(pageAdminSet, /add_action\('wp_ajax_aksrt_frontend_set_save', \[\$this, 'ajax_save'\]\)/);
|
||||
assert.match(pageAdminSet, /\\AKSRT\\Core\\PostMetaBox::META_KEY/);
|
||||
assert.match(pageAdminSet, /\\AKSRT\\Core\\DomainModel::POST_VIEW_COUNT_KEY/);
|
||||
assert.match(pageAdminSet, /update_post_meta\(\$post_id, '_aksrt_like_count', max\(0, \(int\) \$_POST\['like'\]\)\)/);
|
||||
assert.match(pageAdminSet, /wp_update_term\(\$term_id, \(string\) \$term->taxonomy, \$data\)/);
|
||||
assert.match(pageAdminSet, /set_post_format\(\$post_id, \$format === 'standard' \? '' : \$format\)/);
|
||||
|
||||
// Client submits through the shared modal runtime contract; the ajax action
|
||||
// comes from the form's hidden field that the modal endpoint embeds.
|
||||
assert.match(pageAdminJs, /form\.aksrt-admin-set-form/);
|
||||
assert.match(pageAdminJs, /input\[name="action"\]/);
|
||||
assert.match(pageAdminJs, /window\.location\.reload/);
|
||||
assert.match(pageAdminSet, /'html' => \$html/);
|
||||
});
|
||||
|
||||
test('float toolbar config reaches the tinymce plugin with reference-context buttons', () => {
|
||||
const editorJs = read('assets/js/front-submit-editor.js');
|
||||
const submitPage = read('templates/pages/page-submit.php');
|
||||
const editorTemplate = read('templates/parts/submit/editor-main.php');
|
||||
const schema = read('src/Admin/Schema/SchemaExtensionsSystem.php');
|
||||
|
||||
assert.match(submitPage, /\$float_toolbar = wp_is_mobile\(\) \? in_array\('m_s', \$float_scenes, true\) : in_array\('pc_s', \$float_scenes, true\)/);
|
||||
assert.match(submitPage, /'floatToolbar'\s+=> \$float_toolbar/);
|
||||
assert.match(editorJs, /if \(editorCfg\.floatToolbar\) \{\s*\n\s*registerFloatToolbar\(editor\);/);
|
||||
assert.match(editorJs, /editor\.on\('wptoolbar', function \(event\)/);
|
||||
assert.match(editorJs, /editor\.wp\._createToolbar\(bars\)/);
|
||||
assert.match(editorJs, /editor\.addButton\('aksrt_h2'/);
|
||||
assert.match(editorJs, /editor\.addButton\('aksrt_h3'/);
|
||||
assert.match(editorJs, /editor\.addButton\('aksrt_remove'/);
|
||||
assert.match(editorJs, /function floatToolbarButtons\(editor, event\)/);
|
||||
assert.match(schema, /'mce_float_toolbar', 'label' => '经典编辑器增强浮动工具栏', 'type' => 'checkbox'/);
|
||||
// The editor template still wires the plugin into wp_editor.
|
||||
assert.match(editorTemplate, /aksrt_editor_tools/);
|
||||
});
|
||||
|
||||
test('countdown display switch gates aksrt_time_ago with the custom format fallback', () => {
|
||||
const helper = read('inc/helpers/template-tags/search-meta.php');
|
||||
const coreHooks = read('inc/core-hooks.php');
|
||||
|
||||
assert.match(helper, /empty\(\$system\['time_ago_s'\]\) && \(\$system\['time_format'\] \?\? ''\) !== ''/);
|
||||
assert.match(helper, /return date\(\$system\['time_format'\], \$time\)/);
|
||||
// Admin bar gating moved out of the unconditional core hook into SystemTools.
|
||||
assert.doesNotMatch(coreHooks, /add_filter\('show_admin_bar', '__return_false'\)/);
|
||||
});
|
||||
|
||||
test('system tools runtime contract harness passes', () => {
|
||||
const harness = path.join(root, 'tests', 'widget-system', 'system-tools-harness.php');
|
||||
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||
assert.match(output, /System tools harness passed/);
|
||||
});
|
||||
|
||||
test('split upload runtime contract harness passes', () => {
|
||||
const harness = path.join(root, 'tests', 'widget-system', 'split-upload-harness.php');
|
||||
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||
assert.match(output, /Split upload harness passed/);
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
/** System tools (系统工具) runtime contract harness: settings defaults, hook wiring, schema, time-format gate. */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
|
||||
$GLOBALS['aksrt_system_settings'] = [];
|
||||
$GLOBALS['aksrt_system_added'] = [];
|
||||
$GLOBALS['aksrt_system_removed'] = [];
|
||||
|
||||
function __(string $text, string $domain = ''): string { return $text; }
|
||||
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
|
||||
function is_super_admin(): bool { return (bool) $GLOBALS['aksrt_system_is_super_admin']; }
|
||||
function current_time(string $type): string { return date('Y-m-d H:i:s'); }
|
||||
function add_filter(string $hook, $callback, int $priority = 10, int $args = 1): void {
|
||||
$GLOBALS['aksrt_system_added'][] = ['filter', $hook, $callback, $priority];
|
||||
}
|
||||
function add_action(string $hook, $callback, int $priority = 10, int $args = 1): void {
|
||||
$GLOBALS['aksrt_system_added'][] = ['action', $hook, $callback, $priority];
|
||||
}
|
||||
function remove_filter(string $hook, $callback, int $priority = 10): void {
|
||||
$GLOBALS['aksrt_system_removed'][] = ['filter', $hook, $callback, $priority];
|
||||
}
|
||||
function remove_action(string $hook, $callback, int $priority = 10): void {
|
||||
$GLOBALS['aksrt_system_removed'][] = ['action', $hook, $callback, $priority];
|
||||
}
|
||||
function wp_deregister_style(string $handle): void {}
|
||||
function wp_register_style(string $handle, $src): void {}
|
||||
function wp_enqueue_style(string $handle, $src = ''): void {}
|
||||
function remove_image_size(string $name): void {
|
||||
$GLOBALS['aksrt_system_added'][] = ['remove_image_size', $name, null, 10];
|
||||
}
|
||||
|
||||
function system_tools_check(bool $condition, string $message): void {
|
||||
if (!$condition) throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
function system_tools_added(string $type, string $hook): bool {
|
||||
foreach ($GLOBALS['aksrt_system_added'] as $row) {
|
||||
if ($row[0] === $type && $row[1] === $hook) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function system_tools_removed(string $type, string $hook, string $needle = ''): bool {
|
||||
foreach ($GLOBALS['aksrt_system_removed'] as $row) {
|
||||
if ($row[0] !== $type || $row[1] !== $hook) continue;
|
||||
if ($needle === '') return true;
|
||||
if (is_array($row[2])) {
|
||||
$parts = [];
|
||||
foreach ($row[2] as $part) {
|
||||
$parts[] = is_object($part) ? get_class($part) : (string) $part;
|
||||
}
|
||||
$callback = implode('|', $parts);
|
||||
} elseif (is_object($row[2])) {
|
||||
$callback = get_class($row[2]);
|
||||
} else {
|
||||
$callback = (string) $row[2];
|
||||
}
|
||||
if (strpos($callback, $needle) !== false) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function system_tools_reset(): void {
|
||||
$GLOBALS['aksrt_system_added'] = [];
|
||||
$GLOBALS['aksrt_system_removed'] = [];
|
||||
}
|
||||
|
||||
require dirname(__DIR__, 2) . '/src/Settings/ExtensionRuntime.php';
|
||||
require dirname(__DIR__, 2) . '/src/Extensions/SystemTools.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsIP.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsAudit.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsPush.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsSecurity.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensionsSystem.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaExtensions.php';
|
||||
require dirname(__DIR__, 2) . '/inc/helpers/template-tags/search-meta.php';
|
||||
}
|
||||
|
||||
namespace AKSRT\Settings {
|
||||
final class Settings {
|
||||
public static function get_all(): array { return $GLOBALS['aksrt_system_settings']; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\SystemToolsHarness {
|
||||
final class WpEmbedStub {
|
||||
public function autoembed($content) { return $content; }
|
||||
public function maybe_run_ajax_cache(): void {}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
use AKSRT\Admin\Schema\SchemaExtensions;
|
||||
use AKSRT\Admin\Schema\SchemaExtensionsSystem;
|
||||
use AKSRT\Extensions\SystemTools;
|
||||
use AKSRT\Settings\ExtensionRuntime;
|
||||
|
||||
$GLOBALS['aksrt_system_is_super_admin'] = false;
|
||||
$GLOBALS['wp_embed'] = new \AKSRT\SystemToolsHarness\WpEmbedStub();
|
||||
|
||||
// 1) Defaults match the reference theme's 系统工具 defaults.
|
||||
$defaults = ExtensionRuntime::get_system_tools_settings();
|
||||
$expected = [
|
||||
'hide_admin_bar' => true,
|
||||
'remove_emoji' => true,
|
||||
'remove_open_sans' => true,
|
||||
'remove_more_wp_head' => true,
|
||||
'remove_dashboard_widgets' => true,
|
||||
'display_wp_update' => true,
|
||||
'split_upload_s' => true,
|
||||
'split_minimum_size' => 20,
|
||||
'newfilename' => false,
|
||||
'newfilename_type' => 'time',
|
||||
'disabled_large_thumbnail' => true,
|
||||
'admin_user_del_fields' => true,
|
||||
'admin_frontend_set' => true,
|
||||
'close_gutenberg' => false,
|
||||
'disabled_autoembed' => true,
|
||||
'mce_float_toolbar' => ['pc_s', 'm_s'],
|
||||
'time_ago_s' => true,
|
||||
'time_format' => 'n月j日 H:i',
|
||||
];
|
||||
foreach ($expected as $key => $value) {
|
||||
system_tools_check(array_key_exists($key, $defaults), "system tools setting missing: {$key}");
|
||||
system_tools_check($defaults[$key] === $value, "system tools default wrong for {$key}");
|
||||
}
|
||||
|
||||
// 2) Stored values sanitize: rename type whitelist, switch coercion, passthrough format.
|
||||
$GLOBALS['aksrt_system_settings'] = [
|
||||
'newfilename_type' => 'bogus',
|
||||
'newfilename' => 1,
|
||||
'time_ago_s' => 0,
|
||||
'time_format' => 'Y/m/d H:i',
|
||||
'close_gutenberg' => 'yes',
|
||||
'split_minimum_size' => 500,
|
||||
'mce_float_toolbar' => ['m_s', 'bogus', 'm_s'],
|
||||
];
|
||||
$clean = ExtensionRuntime::get_system_tools_settings();
|
||||
system_tools_check($clean['newfilename_type'] === 'time', 'bogus rename type should fall back to time');
|
||||
system_tools_check($clean['newfilename'] === true, 'truthy rename switch should coerce to true');
|
||||
system_tools_check($clean['time_ago_s'] === false, 'falsy time_ago_s should coerce to false');
|
||||
system_tools_check($clean['time_format'] === 'Y/m/d H:i', 'time format should pass through');
|
||||
system_tools_check($clean['close_gutenberg'] === true, 'truthy gutenberg switch should coerce to true');
|
||||
system_tools_check($clean['split_minimum_size'] === 100, 'split minimum size should clamp to the 4..100 range');
|
||||
system_tools_check($clean['mce_float_toolbar'] === ['m_s'], 'float toolbar scenes should whitelist and dedupe');
|
||||
|
||||
// 2b) Unsaved float toolbar key keeps the both-scenes default; explicit empty stays empty.
|
||||
$GLOBALS['aksrt_system_settings'] = ['hide_admin_bar' => 0];
|
||||
$clean = ExtensionRuntime::get_system_tools_settings();
|
||||
system_tools_check($clean['mce_float_toolbar'] === ['pc_s', 'm_s'], 'float toolbar should default to pc+mobile');
|
||||
system_tools_check($clean['hide_admin_bar'] === false, 'stored falsy value should not fall back to default');
|
||||
$GLOBALS['aksrt_system_settings'] = ['mce_float_toolbar' => []];
|
||||
$clean = ExtensionRuntime::get_system_tools_settings();
|
||||
system_tools_check($clean['mce_float_toolbar'] === [], 'explicitly empty float toolbar should stay empty');
|
||||
|
||||
// 3) Hook wiring with every switch enabled.
|
||||
$GLOBALS['aksrt_system_settings'] = array_fill_keys(array_keys($expected), true);
|
||||
$GLOBALS['aksrt_system_settings']['time_format'] = 'n月j日 H:i';
|
||||
$GLOBALS['aksrt_system_is_super_admin'] = false;
|
||||
system_tools_reset();
|
||||
SystemTools::get_instance()->init();
|
||||
|
||||
system_tools_check(system_tools_added('filter', 'show_admin_bar'), 'hide_admin_bar should gate show_admin_bar');
|
||||
system_tools_check(system_tools_added('action', 'init'), 'remove_open_sans should hook init');
|
||||
system_tools_check(system_tools_added('action', 'after_setup_theme'), 'head cleanup and thumbnail pruning should hook after_setup_theme');
|
||||
system_tools_check(system_tools_added('action', 'do_meta_boxes'), 'dashboard widget removal should hook do_meta_boxes');
|
||||
system_tools_check(system_tools_added('filter', 'site_status_tests'), 'site status tests should be filtered');
|
||||
system_tools_check(system_tools_added('filter', 'pre_http_request'), 'site health http requests should be blocked');
|
||||
system_tools_check(system_tools_added('action', 'admin_menu'), 'update notices should hook admin_menu');
|
||||
system_tools_check(system_tools_added('filter', 'automatic_updater_disabled'), 'automatic updates should be disabled');
|
||||
system_tools_check(system_tools_added('filter', 'use_block_editor_for_post'), 'close_gutenberg should gate the block editor');
|
||||
system_tools_check(system_tools_added('action', 'show_user_profile'), 'user profile cleanup should hook show_user_profile');
|
||||
system_tools_check(system_tools_added('action', 'edit_user_profile'), 'user profile cleanup should hook edit_user_profile');
|
||||
system_tools_check(system_tools_removed('action', 'admin_print_scripts', 'print_emoji_detection_script'), 'emoji scripts should be removed');
|
||||
system_tools_check(system_tools_removed('filter', 'the_content', 'autoembed'), 'auto embed should be removed from the_content');
|
||||
system_tools_check(system_tools_removed('action', 'admin_init', '_maybe_update_core'), 'core update checks should be removed');
|
||||
system_tools_check(system_tools_removed('action', 'init', 'wp_schedule_update_checks'), 'update cron schedule should be removed');
|
||||
|
||||
// 4) hide_admin_bar off keeps the admin bar for super admins.
|
||||
$GLOBALS['aksrt_system_settings']['hide_admin_bar'] = false;
|
||||
$GLOBALS['aksrt_system_is_super_admin'] = true;
|
||||
system_tools_reset();
|
||||
SystemTools::get_instance()->init();
|
||||
system_tools_check(!system_tools_added('filter', 'show_admin_bar'), 'super admin should keep the admin bar when hide_admin_bar is off');
|
||||
|
||||
// 5) Dashboard widget removal edits the dashboard meta boxes only in the normal context.
|
||||
$tools = SystemTools::get_instance();
|
||||
$GLOBALS['wp_meta_boxes'] = [
|
||||
'dashboard' => [
|
||||
'normal' => [
|
||||
'high' => ['dashboard_php_nag' => ['id' => 'dashboard_php_nag']],
|
||||
'core' => ['dashboard_site_health' => ['id' => 'dashboard_site_health']],
|
||||
],
|
||||
'side' => [
|
||||
'core' => ['dashboard_primary' => ['id' => 'dashboard_primary']],
|
||||
],
|
||||
],
|
||||
];
|
||||
$tools->remove_dashboard_widgets('dashboard', 'normal');
|
||||
system_tools_check(!isset($GLOBALS['wp_meta_boxes']['dashboard']['normal']['high']['dashboard_php_nag']), 'php nag widget should be removed');
|
||||
system_tools_check(!isset($GLOBALS['wp_meta_boxes']['dashboard']['normal']['core']['dashboard_site_health']), 'site health widget should be removed');
|
||||
system_tools_check(!isset($GLOBALS['wp_meta_boxes']['dashboard']['side']['core']['dashboard_primary']), 'primary widget should be removed');
|
||||
$GLOBALS['wp_meta_boxes']['dashboard']['side']['core']['dashboard_primary'] = ['id' => 'dashboard_primary'];
|
||||
$tools->remove_dashboard_widgets('dashboard', 'side');
|
||||
system_tools_check(isset($GLOBALS['wp_meta_boxes']['dashboard']['side']['core']['dashboard_primary']), 'non-normal context should be ignored');
|
||||
|
||||
// 6) Site status tests drop page cache and php version probes.
|
||||
$tests = $tools->filter_site_status_tests([
|
||||
'async' => ['page_cache' => [], 'dilation' => []],
|
||||
'direct' => ['php_version' => [], 'updates' => []],
|
||||
]);
|
||||
system_tools_check(!isset($tests['async']['page_cache']) && isset($tests['async']['dilation']), 'page_cache test should be dropped');
|
||||
system_tools_check(!isset($tests['direct']['php_version']) && isset($tests['direct']['updates']), 'php_version test should be dropped');
|
||||
|
||||
// 7) Health endpoint blocking is scoped to version/browser/credits checks.
|
||||
$blocked = $tools->block_site_health_requests(null, [], 'https://api.wordpress.org/core/serve-happy/1.0/');
|
||||
system_tools_check(is_array($blocked) && $blocked['body'] === '', 'serve-happy requests should be blocked');
|
||||
$blocked = $tools->block_site_health_requests(null, [], 'https://api.wordpress.org/core/browse-happy/1.0/');
|
||||
system_tools_check(is_array($blocked) && $blocked['body'] === '', 'browse-happy requests should be blocked');
|
||||
$passed = $tools->block_site_health_requests('passthrough', [], 'https://api.wordpress.org/plugins/update-check/1.0/');
|
||||
system_tools_check($passed === 'passthrough', 'update checks should pass through the blocker');
|
||||
|
||||
// 8) Large thumbnail pruning removes the reference theme's size list.
|
||||
$GLOBALS['aksrt_system_added'] = [];
|
||||
$tools->disable_large_thumbnail();
|
||||
$sizes_filter = null;
|
||||
foreach ($GLOBALS['aksrt_system_added'] as $row) {
|
||||
if ($row[0] === 'filter' && $row[1] === 'intermediate_image_sizes') {
|
||||
$sizes_filter = $row[2];
|
||||
}
|
||||
}
|
||||
system_tools_check(is_callable($sizes_filter), 'intermediate_image_sizes filter should be registered');
|
||||
system_tools_check($sizes_filter(['medium_large', '1536x1536', '2048x2048', 'thumbnail']) === ['thumbnail'], 'medium_large/1536x1536/2048x2048 should be pruned');
|
||||
|
||||
// 9) Schema registry exposes the 系统工具 page with all fields.
|
||||
$sections = SchemaExtensions::build()['sections'];
|
||||
system_tools_check(isset($sections['system']) && $sections['system']['title'] === '系统工具', 'system section should register under extensions');
|
||||
$keys = [];
|
||||
foreach ($sections['system']['groups'] as $group) {
|
||||
foreach ($group['fields'] as $field) {
|
||||
$keys[] = $field['key'];
|
||||
}
|
||||
}
|
||||
system_tools_check(count($keys) === count(array_unique($keys)), 'system schema field keys must be unique');
|
||||
foreach (array_keys($expected) as $key) {
|
||||
system_tools_check(in_array($key, $keys, true), "system schema missing field {$key}");
|
||||
}
|
||||
$client_defaults = array_keys($expected);
|
||||
system_tools_check(count(SchemaExtensionsSystem::build()['groups']) === 3, 'system schema should keep three groups');
|
||||
|
||||
// 10) aksrt_time_ago honors the 倒计时显示 switch and custom format.
|
||||
$past = date('Y-m-d H:i:s', strtotime('-3 days'));
|
||||
$GLOBALS['aksrt_system_settings'] = [];
|
||||
system_tools_check(strpos(aksrt_time_ago($past), '天前') !== false, 'relative time should be used by default');
|
||||
$GLOBALS['aksrt_system_settings'] = ['time_ago_s' => 0, 'time_format' => 'Y年n月j日'];
|
||||
$stamp = strtotime('-3 days');
|
||||
system_tools_check(aksrt_time_ago(date('Y-m-d H:i:s', $stamp)) === date('Y年n月j日', $stamp), 'custom time format should apply when countdown display is off');
|
||||
$GLOBALS['aksrt_system_settings'] = ['time_ago_s' => 0, 'time_format' => ''];
|
||||
system_tools_check(strpos(aksrt_time_ago($past), '天前') !== false, 'empty custom format should fall back to relative time');
|
||||
$GLOBALS['aksrt_system_settings'] = ['time_ago_s' => 1];
|
||||
system_tools_check(strpos(aksrt_time_ago($past), '天前') !== false, 'countdown display should stay relative when enabled');
|
||||
|
||||
echo "System tools harness passed\n";
|
||||
}
|
||||
Reference in New Issue
Block a user