feat(submit): add user media library

This commit is contained in:
2026-09-04 22:53:33 +08:00
parent 44e165d34e
commit f3fcfe45da
23 changed files with 1321 additions and 945 deletions
@@ -0,0 +1,158 @@
'use strict';
const assert = require('node:assert/strict');
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 frontSubmit = read('src/Core/FrontSubmit.php');
const mediaTrait = read('src/Core/FrontSubmitMedia.php');
const page = read('templates/pages/page-submit.php');
const editor = read('assets/js/front-submit-editor.js');
const submit = read('assets/js/front-submit.js');
const payDownload = read('assets/js/front-submit-pay-download.js');
const domainModel = read('src/Core/DomainModel.php');
const featured = read('src/Core/FrontSubmitFeatured.php');
const frontSubmitPay = read('src/Core/FrontSubmitPay.php');
const runtimeFiles = [
'assets/js/runtime/main.js',
'assets/js/runtime/main.min.js',
];
test('front submit owns the typed media REST routes and media trait wiring', () => {
assert.match(frontSubmit, /use FrontSubmitMedia\s*;/);
assert.match(frontSubmit, /add_action\('rest_api_init', \[\$this, 'register_rest_routes'\]\)/);
assert.match(frontSubmit, /add_action\('aksrt_new_add_posts', \[\$this, 'sync_post_attachments'\]\)/);
assert.match(frontSubmit, /add_action\('aksrt_new_edit_posts', \[\$this, 'sync_post_attachments'\]\)/);
assert.match(frontSubmit, /register_rest_route\('aksrt\/v1', '\/front-submit\/media\/\(\?P<type>image\|video\|file\)'/);
assert.match(frontSubmit, /'methods'\s*=>\s*\\WP_REST_Server::READABLE/);
assert.match(frontSubmit, /'callback'\s*=>\s*\[\$this, 'rest_list_attachments'\]/);
assert.match(frontSubmit, /'post_id'\s*=>\s*\[/);
assert.match(frontSubmit, /'permission_callback'\s*=>\s*\[\$this, 'can_list_attachments'\]/);
assert.match(frontSubmit, /register_rest_route\('aksrt\/v1', '\/front-submit\/upload\/\(\?P<kind>image\|video\|resource\)'/);
assert.match(frontSubmit, /'methods'\s*=>\s*'POST'/);
assert.match(frontSubmit, /'callback'\s*=>\s*\[\$this, 'rest_upload_attachment'\]/);
assert.match(frontSubmit, /'permission_callback'\s*=>\s*\[\$this, 'can_upload_attachment'\]/);
});
test('front submit media listing freezes ownership, visibility, ordering, paging, search, and MIME semantics', () => {
assert.match(mediaTrait, /\$user_id\s*=\s*get_current_user_id\(\)/);
assert.match(mediaTrait, /'post_type'\s*=>\s*'attachment'/);
assert.match(mediaTrait, /'post_status'\s*=>\s*\['inherit', 'private'\]/);
assert.match(mediaTrait, /'author'\s*=>\s*\$user_id/);
assert.match(mediaTrait, /'posts_per_page'\s*=>\s*48/);
assert.match(mediaTrait, /'paged'\s*=>\s*\$page/);
assert.match(mediaTrait, /'orderby'\s*=>\s*'ID'/);
assert.match(mediaTrait, /'order'\s*=>\s*'DESC'/);
assert.match(mediaTrait, /\$request->get_param\('search'\)/);
assert.match(mediaTrait, /\$request->get_param\('exclude'\)/);
assert.match(mediaTrait, /\$args\['post__not_in'\]\s*=\s*\$exclude/);
assert.match(mediaTrait, /\$args\['s'\]\s*=\s*\$search/);
assert.match(mediaTrait, /add_filter\('wp_allow_query_attachment_by_filename', '__return_true'\)/);
assert.match(mediaTrait, /remove_filter\('wp_allow_query_attachment_by_filename', '__return_true'\)/);
assert.match(mediaTrait, /\$args\['post_mime_type'\]\s*=\s*'image'/);
assert.match(mediaTrait, /\$args\['post_mime_type'\]\s*=\s*'video'/);
assert.doesNotMatch(mediaTrait, /\$args\['post_mime_type'\]\s*=\s*'file'/);
assert.match(mediaTrait, /return \$mime !== '';/);
assert.match(mediaTrait, /!in_array\(\$type, \['image', 'video', 'file'\], true\)/);
assert.match(frontSubmit, /\['image', 'video', 'file'\]/);
});
test('front submit media responses and uploads use the attachment contract', () => {
for (const field of ['attachment_id', 'id', 'url', 'filename', 'type', 'subtype', 'thumbnail_url', 'filesizeInBytes']) {
assert.match(mediaTrait, new RegExp(`['"]${field}['"]`), `missing normalized field ${field}`);
}
assert.match(mediaTrait, /wp_prepare_attachment_for_js/);
assert.match(mediaTrait, /'attachment_id'\s*=>\s*\(int\) \$attachment->ID/);
assert.match(mediaTrait, /'id'\s*=>\s*\(int\) \$attachment->ID/);
assert.match(mediaTrait, /'thumbnail_url'\s*=>/);
assert.match(mediaTrait, /'filesizeInBytes'\s*=>/);
assert.match(frontSubmit, /\$files\['file'\]/);
assert.match(frontSubmit, /media_handle_upload\('aksrt_front_submit_file'/);
assert.match(frontSubmit, /update_post_meta\(\$attachment_id, '_aksrt_front_submit_media', \$kind\)/);
assert.match(frontSubmit, /\$attachment\['success'\]\s*=\s*true/);
assert.match(editor, /body\.append\('file', file\)/);
assert.match(editor, /method:\s*'POST'/);
assert.match(editor, /payload\.success/);
});
test('saved submission media sync enforces ownership and cleans stale parent/meta relations', () => {
assert.match(mediaTrait, /if \(\$user_id <= 0 \|\| \(int\) \$post->post_author !== \$user_id\)/);
assert.match(mediaTrait, /'post_parent'\s*=>\s*0/);
assert.match(mediaTrait, /\(int\) \$attachment->post_author !== \$user_id/);
assert.match(mediaTrait, /\['inherit', 'private'\]/);
assert.match(mediaTrait, /\(int\) \$attachment->post_parent !== 0/);
assert.match(mediaTrait, /'meta_key'\s*=>\s*'_aksrt_front_submit_media'/);
assert.match(mediaTrait, /delete_post_meta\(\$attachment_id, '_aksrt_front_submit_media'\)/);
assert.match(mediaTrait, /update_post_meta\(\$attachment_id, '_aksrt_front_submit_media', \$kind\)/);
assert.match(mediaTrait, /featured_video_attachment_id/);
assert.match(mediaTrait, /data-aksrt-file-id/);
assert.match(mediaTrait, /\['post_parent'\s*=>\s*\$post_id\]/);
});
test('submit page configures media URLs, REST nonce, and the three media copy groups without mini-upload', () => {
assert.match(page, /'restNonce'\s*=>\s*wp_create_nonce\('wp_rest'\)/);
assert.match(page, /'mediaUrl'\s*=>\s*esc_url_raw\(rest_url\('aksrt\/v1\/front-submit\/media'\)\)/);
assert.match(page, /'image'\s*=>\s*\['url'\s*=>\s*esc_url_raw\(rest_url\('aksrt\/v1\/front-submit\/media\/image'\)\)\]/);
assert.match(page, /'video'\s*=>\s*\['url'\s*=>\s*esc_url_raw\(rest_url\('aksrt\/v1\/front-submit\/media\/video'\)\)\]/);
assert.match(page, /'file'\s*=>\s*\['url'\s*=>\s*esc_url_raw\(rest_url\('aksrt\/v1\/front-submit\/media\/file'\)\)\]/);
for (const copy of ['mineImage', 'mineVideo', 'mineFile', 'uploadImage', 'uploadVideo', 'uploadFile']) {
assert.match(page, new RegExp(`['"]${copy}['"]`), `missing copy ${copy}`);
}
assert.doesNotMatch(page, /aksrt-mini-upload/);
assert.equal(fs.existsSync(path.join(root, 'assets/js/mini-upload.js')), false, 'mini-upload.js must stay removed');
});
test('shared submit media picker exposes mine search paging selection and nonce-backed upload', () => {
assert.match(editor, /window\.AksrtSubmitMedia\s*=\s*\{\s*open:/);
assert.match(editor, /data-editor-media-tab="mine"/);
assert.match(editor, /data-editor-media-search/);
assert.match(editor, /state\.search\.length >= 2/);
assert.match(editor, /state\.search\.length === 1/);
assert.match(editor, /state\.searchTimer = window\.setTimeout/);
assert.match(editor, /state\.searchTimer[\s\S]*?600\)/);
assert.match(editor, /params\.set\('paged', String\(page\)\)/);
assert.match(editor, /data-editor-media-more/);
assert.match(editor, /params\.set\('exclude', state\.exclude\.join\(','\)\)/);
assert.match(editor, /multiple\s*=\s*options\.multiple/);
assert.match(editor, /if \(!multiple\) maxItems = 1/);
assert.match(editor, /state\.selected\.length >= maxItems/);
assert.match(editor, /headers:\s*\{\s*'X-WP-Nonce': cfg\.restNonce \|\| ''\s*\}/);
assert.match(editor, /data-aksrt-file-id/);
assert.doesNotMatch(editor, /data-download-file/);
assert.match(submit, /data-aksrt-submit-ready/);
});
test('paid downloads and featured covers use the shared picker and attachment IDs', () => {
assert.match(payDownload, /window\.AksrtSubmitMedia\.open\(/);
assert.match(payDownload, /type:\s*'file'/);
assert.match(payDownload, /item\.attachment_id \|\| item\.id/);
assert.match(payDownload, /data-pay-resource-field="attachment_id"/);
assert.match(domainModel, /'attachment_id'\s*=>\s*is_scalar\(\$row\['attachment_id'\]/);
assert.match(domainModel, /max\(0, \(int\) \$row\['attachment_id'\]\)/);
assert.match(submit, /openMediaPicker:\s*function \(options\)/);
assert.match(submit, /window\.AksrtSubmitMedia\.open\(options\)/);
assert.match(submit, /type:\s*'image'/);
assert.match(submit, /type:\s*'video'/);
assert.match(submit, /item\.attachment_id \|\| item\.id/);
assert.match(domainModel, /'featured_video_attachment_id'\s*=>\s*0/);
assert.match(payDownload, /attachment_id/);
assert.match(frontSubmitPay, /\$meta\['downloads'\]\s*=\s*\$downloads/);
assert.match(featured, /featured_video_attachment_id/);
});
test('runtime bundles contain no legacy download-file action handler', () => {
for (const file of runtimeFiles) {
const source = read(file);
assert.doesNotMatch(source, /action=download_file/);
assert.doesNotMatch(source, /download-nonce/);
}
});
@@ -47,7 +47,7 @@ test('front submit uses the registered local jQuery runtime', () => {
assert.doesNotMatch(source, /jquery-migrate/);
const page = read('templates/pages/page-submit.php');
assert.match(page, /\['loader_js', 'aksrt-front-submit-pay-download'\]/);
assert.match(page, /\['loader_js', 'aksrt-front-submit-editor', 'aksrt-front-submit-pay-download'\]/);
});
test('legacy runtime modules use jQuery explicitly without a dollar alias', () => {
@@ -87,7 +87,7 @@ test('front submit paid resources use the reference modal editor and compact lis
assert.match(pay, /class="hidden-input"/);
assert.doesNotMatch(pay, /data-pay-download-upload|data-pay-download-tpl/);
['url', 'extract_code', 'copy_key', 'copy_val', 'label', 'class'].forEach((field) => {
['url', 'attachment_id', 'extract_code', 'copy_key', 'copy_val', 'label', 'class'].forEach((field) => {
assert.match(downloads, new RegExp(`['"]${field}['"]`));
assert.match(model, new RegExp(`['"]${field}['"]`));
});
@@ -17,6 +17,8 @@ test('legacy theme action endpoint stays removed from runtime bundles', () => {
assert.doesNotMatch(source, /_win\.bd\.on\(['"]click['"],\s*['"]\[data-action\]['"]/, file)
assert.doesNotMatch(source, /\baction_ajax\s*\(/, file)
assert.doesNotMatch(source, /\/action\/action\.php/, file)
assert.doesNotMatch(source, /action=download_file/, `${file} must not restore the legacy download-file handler`)
assert.doesNotMatch(source, /download-nonce/, `${file} must not restore the legacy download nonce attribute`)
}
})