Mark new regular and chunked uploads as pending and claim persisted article references, including partial saves and large reference sets. Protect associated media from cleanup and add upload lifecycle regression coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
303 lines
10 KiB
PHP
303 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Core;
|
|
|
|
use AKSRT\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('aksrt_get_pay_meta')
|
|
? aksrt_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' => '_aksrt_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, '_aksrt_front_submit_media');
|
|
}
|
|
}
|
|
foreach ($desired as $attachment_id => $kind) {
|
|
$attachment_id = (int) $attachment_id;
|
|
update_post_meta($attachment_id, '_aksrt_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-aksrt-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 !== '';
|
|
}
|
|
}
|