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>
303 lines
10 KiB
PHP
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 !== '';
|
|
}
|
|
}
|