Files
wordpress-theme-aksrt/src/Core/FrontSubmitMedia.php
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

303 lines
10 KiB
PHP

<?php
/**
* @package Jio
*/
namespace Jio\Core;
use Jio\Attachment\PendingUploads;
defined('ABSPATH') || exit;
/**
* Front-submit media library, attachment normalization, and post relations.
*/
trait FrontSubmitMedia {
/**
* Check whether the current user may browse their front-submit media.
*/
public function can_list_attachments(\WP_REST_Request $request): bool {
$user_id = get_current_user_id();
return $user_id > 0 && $this->can_use_media($request, $user_id);
}
/**
* Check whether a user may use the submit media routes for this post.
*/
private function can_use_media(\WP_REST_Request $request, int $user_id): bool {
$post_id = absint($request->get_param('post_id'));
if ($post_id > 0) {
return $this->can_edit_post($post_id, $user_id);
}
return $this->can_submit($user_id);
}
/**
* Sanitize a REST attachment exclusion list.
*
* @param mixed $value Comma-separated or array attachment IDs.
* @return int[]
*/
public function sanitize_attachment_ids($value): array {
if (!is_array($value)) {
$value = preg_split('/[,\s]+/', (string) $value) ?: [];
}
return array_values(array_unique(array_filter(array_map(static function ($id): int {
return is_scalar($id) ? absint($id) : 0;
}, $value))));
}
/**
* Return the current user's front-submit attachments.
*/
public function rest_list_attachments(\WP_REST_Request $request): \WP_REST_Response {
$user_id = get_current_user_id();
$type = sanitize_key((string) $request->get_param('type'));
$page = max(1, absint($request->get_param('paged')));
$search = sanitize_text_field((string) $request->get_param('search'));
$exclude = $this->sanitize_attachment_ids($request->get_param('exclude'));
if ($user_id <= 0 || !in_array($type, ['image', 'video', 'file'], true)) {
return new \WP_REST_Response([
'items' => [],
'total' => 0,
'total_pages' => 0,
'page' => $page,
'per_page' => 48,
], 200);
}
$args = [
'post_type' => 'attachment',
'post_status' => ['inherit', 'private'],
'author' => $user_id,
'posts_per_page' => 48,
'paged' => $page,
'orderby' => 'ID',
'order' => 'DESC',
'no_found_rows' => false,
];
if ($type === 'image') {
$args['post_mime_type'] = 'image';
} elseif ($type === 'video') {
$args['post_mime_type'] = 'video';
}
if ($exclude !== []) {
$args['post__not_in'] = $exclude;
}
if ($search !== '') {
$args['s'] = $search;
add_filter('wp_allow_query_attachment_by_filename', '__return_true');
}
$query = new \WP_Query($args);
if ($search !== '') {
remove_filter('wp_allow_query_attachment_by_filename', '__return_true');
}
$items = [];
foreach ($query->posts as $attachment) {
$prepared = $this->prepare_attachment_response($attachment);
if ($prepared !== null) {
$items[] = $prepared;
}
}
$total = (int) $query->found_posts;
return new \WP_REST_Response([
'items' => $items,
'total' => $total,
'total_pages' => $total > 0 ? (int) ceil($total / 48) : 0,
'page' => $page,
'per_page' => 48,
], 200);
}
/**
* Normalize an attachment with WordPress's media response model.
*
* Only fields needed by the front-submit picker are returned. In
* particular, admin links, icons, nonces, and author data are omitted.
*
* @param int|\WP_Post $attachment Attachment object or ID.
* @return array<string, mixed>|null
*/
private function prepare_attachment_response($attachment): ?array {
$attachment = $attachment instanceof \WP_Post ? $attachment : get_post(absint($attachment));
if (!$attachment || $attachment->post_type !== 'attachment') {
return null;
}
if (!function_exists('wp_prepare_attachment_for_js')) {
require_once ABSPATH . 'wp-admin/includes/media.php';
}
$data = wp_prepare_attachment_for_js($attachment->ID);
if (!is_array($data)) {
return null;
}
$sizes = is_array($data['sizes'] ?? null) ? $data['sizes'] : [];
$size_url = static function (array $sizes, string $size, string $fallback): string {
return !empty($sizes[$size]['url']) ? (string) $sizes[$size]['url'] : $fallback;
};
$url = (string) ($data['url'] ?? wp_get_attachment_url($attachment->ID) ?? '');
$filename = (string) ($data['filename'] ?? wp_basename((string) get_attached_file($attachment->ID)));
$mime = (string) ($data['mime'] ?? get_post_mime_type($attachment->ID));
return [
'attachment_id' => (int) $attachment->ID,
'id' => (int) $attachment->ID,
'title' => (string) ($data['title'] ?? get_the_title($attachment->ID)),
'name' => (string) ($data['name'] ?? $filename),
'filename' => $filename,
'type' => (string) ($data['type'] ?? (strpos($mime, '/') !== false ? strtok($mime, '/') : '')),
'subtype' => (string) ($data['subtype'] ?? (strpos($mime, '/') !== false ? substr($mime, strpos($mime, '/') + 1) : '')),
'url' => $url,
'large_url' => $size_url($sizes, 'large', $url),
'medium_url' => $size_url($sizes, 'medium', $url),
'thumbnail_url' => $size_url($sizes, 'thumbnail', $url),
'filesizeInBytes' => (int) ($data['filesizeInBytes'] ?? (@filesize((string) get_attached_file($attachment->ID)) ?: 0)),
'mime' => $mime,
];
}
/**
* Attach IDs referenced by a saved submission to that post.
*
* @param mixed $post Saved post passed by the existing submission hooks.
*/
public function sync_post_attachments($post): void {
if (!$post instanceof \WP_Post || $post->post_type !== 'post') {
return;
}
$user_id = get_current_user_id();
$post_id = (int) $post->ID;
if ($user_id <= 0 || (int) $post->post_author !== $user_id) {
return;
}
$image_ids = $this->extract_attachment_ids((string) $post->post_content, 'image');
$video_ids = $this->extract_attachment_ids((string) $post->post_content, 'video');
$file_ids = $this->extract_attachment_ids((string) $post->post_content, 'file');
$desired = [];
foreach ($image_ids as $id) {
if ($this->attachment_can_attach($id, $user_id, $post_id, 'image')) {
$desired[$id] = 'image';
}
}
foreach ($video_ids as $id) {
if ($this->attachment_can_attach($id, $user_id, $post_id, 'video')) {
$desired[$id] = 'video';
}
}
foreach ($file_ids as $id) {
if ($this->attachment_can_attach($id, $user_id, $post_id, 'file')) {
$desired[$id] = 'file';
}
}
$pay_meta = function_exists('jio_get_pay_meta')
? jio_get_pay_meta($post_id)
: get_post_meta($post_id, DomainModel::PAY_META_KEY, true);
$pay_meta = is_array($pay_meta) ? $pay_meta : [];
$download_urls = [];
foreach (is_array($pay_meta['downloads'] ?? null) ? $pay_meta['downloads'] : [] as $download) {
$url = (string) ($download['url'] ?? '');
if ($url !== '') $download_urls[] = $url;
$id = absint($download['attachment_id'] ?? 0);
if ($id > 0 && $this->attachment_can_attach($id, $user_id, $post_id, 'file')) {
$desired[$id] = 'file';
}
}
$post_meta = get_post_meta($post_id, DomainModel::POST_META_KEY, true);
$post_meta = is_array($post_meta) ? $post_meta : [];
$featured_ids = [];
$video_id = absint($post_meta['featured_video_attachment_id'] ?? 0);
if ($video_id > 0 && $this->attachment_can_attach($video_id, $user_id, $post_id, 'video')) {
$desired[$video_id] = 'video';
}
$thumb_id = get_post_thumbnail_id($post_id);
if ($thumb_id > 0) {
$featured_ids[] = $thumb_id;
}
foreach (preg_split('/[\\s,]+/', (string) ($post_meta['featured_slide'] ?? '')) ?: [] as $token) {
$id = absint($token);
if ($id > 0) $featured_ids[] = $id;
}
foreach ($featured_ids as $id) {
if ($this->attachment_can_attach($id, $user_id, $post_id, 'image')) {
$desired[$id] = 'image';
}
}
global $wpdb;
$existing = get_posts([
'post_type' => 'attachment',
'post_status' => ['inherit', 'private'],
'post_parent' => $post_id,
'author' => $user_id,
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'meta_key' => '_jio_front_submit_media',
]);
foreach (array_map('absint', $existing) as $attachment_id) {
if (!isset($desired[$attachment_id])) {
$wpdb->update($wpdb->posts, ['post_parent' => 0], ['ID' => $attachment_id], ['%d'], ['%d']);
clean_post_cache($attachment_id);
delete_post_meta($attachment_id, '_jio_front_submit_media');
}
}
foreach ($desired as $attachment_id => $kind) {
$attachment_id = (int) $attachment_id;
update_post_meta($attachment_id, '_jio_front_submit_media', $kind);
$wpdb->update($wpdb->posts, ['post_parent' => $post_id], ['ID' => $attachment_id], ['%d'], ['%d']);
clean_post_cache($attachment_id);
}
PendingUploads::claim_ids(array_keys($desired), $user_id);
PendingUploads::claim_urls($download_urls, $user_id, null);
PendingUploads::claim_text((string) $post->post_content, $user_id, null);
}
/**
* Extract IDs from image/video edit markers or attachment relation markers.
*
* @return int[]
*/
private function extract_attachment_ids(string $content, string $type): array {
$ids = [];
if ($type === 'file') {
preg_match_all('/data-jio-file-id\s*=\s*["\']([^"\']+)["\']/i', $content, $matches);
} elseif ($type === 'image') {
preg_match_all('/<img\b[^>]*data-edit-file-id\s*=\s*["\']([^"\']+)["\'][^>]*>/i', $content, $matches);
} else {
preg_match_all('/<(?:div|video)\b[^>]*data-edit-file-id\s*=\s*["\']([^"\']+)["\'][^>]*data-video-url[^>]*>/i', $content, $matches);
if (empty($matches[1])) {
preg_match_all('/<(?:div|video)\b[^>]*data-video-url[^>]*data-edit-file-id\s*=\s*["\']([^"\']+)["\'][^>]*>/i', $content, $matches);
}
}
return array_values(array_unique(array_filter(array_map('absint', $matches[1] ?? []))));
}
private function attachment_can_attach(int $attachment_id, int $user_id, int $post_id, string $type): bool {
$attachment = get_post($attachment_id);
if (!$attachment || $attachment->post_type !== 'attachment' || (int) $attachment->post_author !== $user_id) {
return false;
}
if (!in_array($attachment->post_status, ['inherit', 'private'], true)) {
return false;
}
if ((int) $attachment->post_parent !== 0 && (int) $attachment->post_parent !== $post_id) {
return false;
}
$mime = (string) $attachment->post_mime_type;
if ($type === 'image') {
return wp_attachment_is_image($attachment_id);
}
if ($type === 'video') {
return strpos($mime, 'video/') === 0;
}
return $mime !== '';
}
}