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>
610 lines
29 KiB
PHP
610 lines
29 KiB
PHP
<?php
|
|
/**
|
|
* WordPress-free behavior harness for front-submit pending-upload ownership.
|
|
* Exercises the production FrontSubmitSave, FrontSubmitMedia, FrontSubmit,
|
|
* and PendingUploads implementations against a small in-memory WP surface.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace {
|
|
if (!defined('ABSPATH')) define('ABSPATH', __DIR__ . '/');
|
|
if (!defined('DAY_IN_SECONDS')) define('DAY_IN_SECONDS', 86400);
|
|
if (!defined('HOUR_IN_SECONDS')) define('HOUR_IN_SECONDS', 3600);
|
|
|
|
class WP_Post {
|
|
public int $ID = 0;
|
|
public string $post_type = 'post';
|
|
public int $post_author = 0;
|
|
public string $post_status = 'draft';
|
|
public int $post_parent = 0;
|
|
public string $post_content = '';
|
|
public string $post_title = '';
|
|
public string $post_mime_type = '';
|
|
|
|
public function __construct(array $data = []) {
|
|
foreach ($data as $key => $value) {
|
|
if (property_exists($this, (string) $key)) {
|
|
$this->{$key} = match ($key) {
|
|
'ID', 'post_author', 'post_parent' => (int) $value,
|
|
default => (string) $value,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class WP_Error {
|
|
private string $code;
|
|
private string $message;
|
|
|
|
public function __construct($code = '', $message = '', $data = null) {
|
|
$this->code = (string) $code;
|
|
$this->message = (string) $message;
|
|
}
|
|
|
|
public function get_error_code(): string { return $this->code; }
|
|
public function get_error_message(): string { return $this->message; }
|
|
}
|
|
|
|
class 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; }
|
|
}
|
|
|
|
class WP_REST_Response {
|
|
private $data;
|
|
private int $status;
|
|
|
|
public function __construct($data = [], int $status = 200) {
|
|
$this->data = $data;
|
|
$this->status = $status;
|
|
}
|
|
|
|
public function get_data() { return $this->data; }
|
|
public function get_status(): int { return $this->status; }
|
|
}
|
|
|
|
class WP_REST_Server { public const READABLE = 'GET'; }
|
|
|
|
final class FrontSubmitPendingWpdb {
|
|
public string $posts = 'wp_posts';
|
|
public string $postmeta = 'wp_postmeta';
|
|
|
|
public function update($table, array $data, array $where, array $format = [], array $where_format = []) {
|
|
$state = &$GLOBALS['front_pending_state'];
|
|
if (!empty($state['fail_relation_updates'])) return false;
|
|
$id = (int) ($where['ID'] ?? 0);
|
|
$post = $state['posts'][$id] ?? null;
|
|
if (!$post instanceof WP_Post) return 0;
|
|
foreach ($data as $key => $value) {
|
|
if ($key === 'post_parent') $post->post_parent = (int) $value;
|
|
}
|
|
$state['relation_updates'][] = ['id' => $id, 'data' => $data];
|
|
return 1;
|
|
}
|
|
|
|
public function prepare(string $query, ...$args): string { return $query; }
|
|
public function get_col(string $query): array { return []; }
|
|
}
|
|
|
|
function fp_assert(bool $condition, string $message): void {
|
|
if (!$condition) throw new \RuntimeException($message);
|
|
}
|
|
|
|
function fp_reset(): void {
|
|
$GLOBALS['front_pending_state'] = [
|
|
'user_id' => 7,
|
|
'posts' => [],
|
|
'post_meta' => [],
|
|
'post_thumbnails' => [],
|
|
'pay_meta' => [],
|
|
'url_map' => [],
|
|
'deleted' => [],
|
|
'relation_updates' => [],
|
|
'meta_writes' => [],
|
|
'upload_calls' => [],
|
|
'featured_save_calls' => 0,
|
|
'fail_relation_updates' => false,
|
|
'fail_insert' => false,
|
|
'fail_update' => false,
|
|
'insert_zero' => false,
|
|
'update_zero' => false,
|
|
'next_post_id' => 1000,
|
|
'next_attachment_id' => 2000,
|
|
'failed_js_ids' => [],
|
|
];
|
|
$GLOBALS['wpdb'] = new FrontSubmitPendingWpdb();
|
|
$_FILES = [];
|
|
}
|
|
|
|
function fp_post(int $id, string $type = 'post', int $author = 7, string $status = 'draft', string $content = '', int $parent = 0, string $mime = ''): WP_Post {
|
|
$post = new WP_Post([
|
|
'ID' => $id,
|
|
'post_type' => $type,
|
|
'post_author' => $author,
|
|
'post_status' => $status,
|
|
'post_content' => $content,
|
|
'post_parent' => $parent,
|
|
'post_mime_type' => $mime,
|
|
'post_title' => 'Harness post ' . $id,
|
|
]);
|
|
$GLOBALS['front_pending_state']['posts'][$id] = $post;
|
|
return $post;
|
|
}
|
|
|
|
function fp_attachment(int $id, int $author = 7, string $mime = 'image/jpeg', int $parent = 0, string $status = 'inherit', bool $pending = true, ?string $kind = 'image'): WP_Post {
|
|
$post = fp_post($id, 'attachment', $author, $status, '', $parent, $mime);
|
|
if ($kind !== null) update_post_meta($id, '_aksrt_front_submit_media', $kind);
|
|
$GLOBALS['front_pending_state']['url_map']['https://example.test/uploads/media-' . $id . '.' . fp_ext($mime)] = $id;
|
|
if ($pending) \AKSRT\Attachment\PendingUploads::mark($id);
|
|
return $post;
|
|
}
|
|
|
|
function fp_ext(string $mime): string {
|
|
return match ($mime) {
|
|
'image/jpeg' => 'jpg', 'image/png' => 'png', 'video/mp4' => 'mp4', 'application/pdf' => 'pdf', default => 'bin',
|
|
};
|
|
}
|
|
|
|
function fp_pending(int $id): bool {
|
|
return get_post_meta($id, \AKSRT\Attachment\PendingUploads::META, true) !== '';
|
|
}
|
|
|
|
function fp_article_data(string $content, array $extra = []): array {
|
|
return array_merge([
|
|
'post_title' => 'Pending lifecycle behavior test',
|
|
'post_content' => $content,
|
|
'category' => [1],
|
|
], $extra);
|
|
}
|
|
|
|
function __($text, $domain = ''): string { return (string) $text; }
|
|
function absint($value): int { return abs((int) $value); }
|
|
function sanitize_key($value): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $value)) ?? ''; }
|
|
function sanitize_text_field($value): string { return trim(strip_tags((string) $value)); }
|
|
function sanitize_textarea_field($value): string { return trim(strip_tags((string) $value)); }
|
|
function sanitize_file_name($value): string { return preg_replace('/[^A-Za-z0-9._\-]/', '', (string) $value) ?? ''; }
|
|
function wp_unslash($value) { return $value; }
|
|
function wp_strip_all_tags($value): string { return trim(strip_tags((string) $value)); }
|
|
function wp_kses_post($value): string { return (string) $value; }
|
|
function is_wp_error($value): bool { return $value instanceof WP_Error; }
|
|
function get_current_user_id(): int { return (int) $GLOBALS['front_pending_state']['user_id']; }
|
|
function current_user_can($capability, ...$args): bool { return false; }
|
|
function user_can($user_id, $capability, ...$args): bool { return false; }
|
|
function add_filter($tag, $callback, $priority = 10, $accepted_args = 1): bool { return true; }
|
|
function remove_filter($tag, $callback, $priority = 10): bool { return true; }
|
|
function do_action($tag, ...$args): void { $GLOBALS['front_pending_state']['actions'][] = $tag; }
|
|
function current_time($type): string { return '2026-10-30 12:00:00'; }
|
|
function wp_set_post_categories($post_id, $categories): void {}
|
|
function wp_set_post_tags($post_id, $tags): void {}
|
|
function get_permalink($post_id): string { return 'https://example.test/?p=' . (int) $post_id; }
|
|
function clean_post_cache($post_id): void {}
|
|
function wp_max_upload_size(): int { return 50 * 1024 * 1024; }
|
|
function size_format($bytes): string { return (string) $bytes . 'B'; }
|
|
function get_allowed_mime_types(): array { return ['pdf' => 'application/pdf']; }
|
|
function wp_check_filetype_and_ext($file, $filename, $mimes = []): array {
|
|
$ext = strtolower(pathinfo((string) $filename, PATHINFO_EXTENSION));
|
|
return ['ext' => $ext, 'type' => $mimes[$ext] ?? ''];
|
|
}
|
|
function get_post($post_id) {
|
|
if ($post_id instanceof WP_Post) return $post_id;
|
|
return $GLOBALS['front_pending_state']['posts'][(int) $post_id] ?? null;
|
|
}
|
|
function get_post_meta($post_id, $key = '', $single = false) {
|
|
$value = $GLOBALS['front_pending_state']['post_meta'][(int) $post_id][(string) $key] ?? null;
|
|
if ($single) return $value ?? '';
|
|
return $value === null ? [] : [$value];
|
|
}
|
|
function update_post_meta($post_id, $key, $value): bool {
|
|
$state = &$GLOBALS['front_pending_state'];
|
|
$id = (int) $post_id;
|
|
$meta_key = (string) $key;
|
|
$state['post_meta'][$id][$meta_key] = $value;
|
|
$state['meta_writes'][] = ['id' => $id, 'key' => $meta_key, 'value' => $value];
|
|
return true;
|
|
}
|
|
function delete_post_meta($post_id, $key): bool {
|
|
unset($GLOBALS['front_pending_state']['post_meta'][(int) $post_id][(string) $key]);
|
|
return true;
|
|
}
|
|
function get_post_thumbnail_id($post_id): int { return (int) ($GLOBALS['front_pending_state']['post_thumbnails'][(int) $post_id] ?? 0); }
|
|
function set_post_thumbnail($post_id, $thumbnail_id): bool {
|
|
$GLOBALS['front_pending_state']['post_thumbnails'][(int) $post_id] = (int) $thumbnail_id;
|
|
return true;
|
|
}
|
|
function delete_post_thumbnail($post_id): bool {
|
|
unset($GLOBALS['front_pending_state']['post_thumbnails'][(int) $post_id]);
|
|
return true;
|
|
}
|
|
function wp_attachment_is_image($attachment_id): bool {
|
|
$post = get_post((int) $attachment_id);
|
|
return $post instanceof WP_Post && strpos($post->post_mime_type, 'image/') === 0;
|
|
}
|
|
function get_posts(array $args = []): array {
|
|
$results = [];
|
|
foreach ($GLOBALS['front_pending_state']['posts'] as $id => $post) {
|
|
if (!$post instanceof WP_Post) continue;
|
|
if (($args['post_type'] ?? '') !== $post->post_type) continue;
|
|
if (isset($args['post_parent']) && $post->post_parent !== (int) $args['post_parent']) continue;
|
|
if (isset($args['author']) && $post->post_author !== (int) $args['author']) continue;
|
|
if (isset($args['post_status']) && !in_array($post->post_status, (array) $args['post_status'], true)) continue;
|
|
if (!empty($args['meta_key']) && get_post_meta((int) $id, (string) $args['meta_key'], true) === '') continue;
|
|
$results[] = !empty($args['fields']) && $args['fields'] === 'ids' ? (int) $id : $post;
|
|
}
|
|
return $results;
|
|
}
|
|
function wp_insert_post(array $postarr, $wp_error = false) {
|
|
$state = &$GLOBALS['front_pending_state'];
|
|
if ($state['fail_insert']) return new WP_Error('insert_failed', 'insert failed');
|
|
if ($state['insert_zero']) return 0;
|
|
$id = (int) $state['next_post_id']++;
|
|
$postarr['ID'] = $id;
|
|
$state['posts'][$id] = new WP_Post($postarr + ['post_type' => 'post', 'post_status' => 'draft']);
|
|
return $id;
|
|
}
|
|
function wp_update_post(array $postarr, $wp_error = false) {
|
|
$state = &$GLOBALS['front_pending_state'];
|
|
$id = (int) ($postarr['ID'] ?? 0);
|
|
if ($id <= 0 || !isset($state['posts'][$id])) return new WP_Error('update_missing', 'post missing');
|
|
if ($state['fail_update']) return new WP_Error('update_failed', 'update failed');
|
|
if ($state['update_zero']) return 0;
|
|
$post = $state['posts'][$id];
|
|
foreach ($postarr as $key => $value) {
|
|
if (property_exists($post, (string) $key)) {
|
|
$post->{$key} = in_array($key, ['ID', 'post_author', 'post_parent'], true) ? (int) $value : (string) $value;
|
|
}
|
|
}
|
|
return $id;
|
|
}
|
|
function aksrt_get_pay_meta($post_id): array {
|
|
return $GLOBALS['front_pending_state']['pay_meta'][(int) $post_id] ?? [];
|
|
}
|
|
function attachment_url_to_postid($url): int {
|
|
return (int) ($GLOBALS['front_pending_state']['url_map'][(string) $url] ?? 0);
|
|
}
|
|
function wp_get_upload_dir(): array { return ['baseurl' => 'https://example.test/uploads']; }
|
|
function set_url_scheme($url, $scheme = null): string {
|
|
$url = (string) $url;
|
|
return preg_match('#^https?://#i', $url) ? $url : 'https://' . ltrim($url, '/');
|
|
}
|
|
function wp_parse_url($url, $component = -1) { return parse_url((string) $url, $component); }
|
|
function wp_get_attachment_url($attachment_id): string {
|
|
foreach ($GLOBALS['front_pending_state']['url_map'] as $url => $id) {
|
|
if ((int) $id === (int) $attachment_id) return (string) $url;
|
|
}
|
|
return 'https://example.test/uploads/media-' . (int) $attachment_id . '.bin';
|
|
}
|
|
function wp_get_attachment_image_url($attachment_id, $size = 'thumbnail') { return wp_get_attachment_url($attachment_id); }
|
|
function wp_prepare_attachment_for_js($attachment_id) {
|
|
if (in_array((int) $attachment_id, $GLOBALS['front_pending_state']['failed_js_ids'], true)) return false;
|
|
$post = get_post((int) $attachment_id);
|
|
if (!$post instanceof WP_Post) return false;
|
|
$mime = $post->post_mime_type;
|
|
[$type, $subtype] = array_pad(explode('/', $mime, 2), 2, '');
|
|
$url = wp_get_attachment_url((int) $attachment_id);
|
|
return [
|
|
'id' => (int) $attachment_id,
|
|
'title' => $post->post_title,
|
|
'name' => basename($url),
|
|
'filename' => basename($url),
|
|
'url' => $url,
|
|
'type' => $type,
|
|
'subtype' => $subtype,
|
|
'mime' => $mime,
|
|
'sizes' => [],
|
|
'filesizeInBytes' => 1,
|
|
];
|
|
}
|
|
function get_the_title($post_id): string { return (string) (get_post((int) $post_id)->post_title ?? ''); }
|
|
function get_attached_file($attachment_id): string { return sys_get_temp_dir() . '/front-submit-' . (int) $attachment_id; }
|
|
function wp_basename($path): string { return basename((string) $path); }
|
|
function media_handle_upload($file_id, $post_id, $post_data = [], $overrides = []) {
|
|
$state = &$GLOBALS['front_pending_state'];
|
|
$id = (int) $state['next_attachment_id']++;
|
|
$file = $_FILES[$file_id] ?? [];
|
|
$state['posts'][$id] = new WP_Post([
|
|
'ID' => $id,
|
|
'post_type' => 'attachment',
|
|
'post_author' => 0,
|
|
'post_status' => 'inherit',
|
|
'post_parent' => 0,
|
|
'post_title' => (string) ($file['name'] ?? 'upload'),
|
|
'post_mime_type' => 'application/pdf',
|
|
]);
|
|
$url = 'https://example.test/uploads/upload-' . $id . '.pdf';
|
|
$state['url_map'][$url] = $id;
|
|
$state['upload_calls'][] = ['id' => $id, 'overrides' => $overrides];
|
|
return $id;
|
|
}
|
|
function wp_delete_attachment($attachment_id, $force_delete = false) {
|
|
$state = &$GLOBALS['front_pending_state'];
|
|
$id = (int) $attachment_id;
|
|
$state['deleted'][] = $id;
|
|
unset($state['posts'][$id], $state['post_meta'][$id]);
|
|
return (object) ['ID' => $id];
|
|
}
|
|
|
|
fp_reset();
|
|
require_once dirname(__DIR__, 2) . '/src/Attachment/PendingUploads.php';
|
|
require_once dirname(__DIR__, 2) . '/src/Core/FrontSubmitSave.php';
|
|
require_once dirname(__DIR__, 2) . '/src/Core/FrontSubmitPay.php';
|
|
require_once dirname(__DIR__, 2) . '/src/Core/FrontSubmitFeatured.php';
|
|
require_once dirname(__DIR__, 2) . '/src/Core/FrontSubmitMedia.php';
|
|
require_once dirname(__DIR__, 2) . '/src/Core/FrontSubmitChunk.php';
|
|
require_once dirname(__DIR__, 2) . '/src/Core/FrontSubmit.php';
|
|
}
|
|
|
|
namespace AKSRT\Core {
|
|
final class DomainModel {
|
|
public const PAY_META_KEY = '_aksrt_pay';
|
|
public const POST_META_KEY = 'aksrt_post_meta';
|
|
}
|
|
|
|
final class FrontSubmitPendingProbe {
|
|
use FrontSubmitSave;
|
|
use FrontSubmitMedia;
|
|
|
|
public function settings(): array {
|
|
return [
|
|
'enabled' => true,
|
|
'default_user_id' => 1,
|
|
'title_min' => 3,
|
|
'title_max' => 100,
|
|
'allowed_cat_ids' => [],
|
|
'allow_tags' => false,
|
|
'custom_filter_s' => false,
|
|
];
|
|
}
|
|
|
|
public function can_submit(?int $user_id = null): bool { return true; }
|
|
|
|
public function can_edit_post(int $post_id, ?int $user_id = null): bool {
|
|
$post = \get_post($post_id);
|
|
return $post instanceof \WP_Post && (int) $post->post_author === (int) ($user_id ?? \get_current_user_id());
|
|
}
|
|
|
|
public function user_can_cap(string $cap, int $user_id = 0, ?int $context = null): bool {
|
|
return $cap !== 'new_post_audit_no';
|
|
}
|
|
|
|
private function save_pay_meta(int $post_id, array $data, int $user_id, array $settings): array {
|
|
if (isset($data['fake_saved_pay_meta']) && is_array($data['fake_saved_pay_meta'])) {
|
|
$GLOBALS['front_pending_state']['pay_meta'][$post_id] = $data['fake_saved_pay_meta'];
|
|
\update_post_meta($post_id, DomainModel::PAY_META_KEY, $data['fake_saved_pay_meta']);
|
|
}
|
|
if (!empty($data['fake_pay_failure'])) {
|
|
return ['success' => false, 'message' => 'pay validation failed'];
|
|
}
|
|
return ['success' => true];
|
|
}
|
|
|
|
private function save_featured_meta(int $post_id, array $data, int $user_id): void {
|
|
$GLOBALS['front_pending_state']['featured_save_calls']++;
|
|
$meta = $data['fake_featured_meta'] ?? null;
|
|
if (is_array($meta)) \update_post_meta($post_id, DomainModel::POST_META_KEY, $meta);
|
|
$thumbnail = (int) ($data['fake_thumbnail_id'] ?? 0);
|
|
if ($thumbnail > 0) \set_post_thumbnail($post_id, $thumbnail);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace AKSRT\User {
|
|
final class Restriction {
|
|
private static ?self $instance = null;
|
|
public static function get_instance(): self { return self::$instance ??= new self(); }
|
|
public function is_suspended(int $user_id): bool { return false; }
|
|
public function get_active_suspend(int $user_id): array { return []; }
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
use AKSRT\Attachment\PendingUploads;
|
|
use AKSRT\Core\FrontSubmit;
|
|
use AKSRT\Core\FrontSubmitPendingProbe;
|
|
use AKSRT\Core\DomainModel;
|
|
|
|
$submit = new FrontSubmitPendingProbe();
|
|
|
|
// Successful single-file and sideload persistence both pass through the real
|
|
// FrontSubmit normalizer and mark the resulting attachment as pending.
|
|
fp_reset();
|
|
$tmp = tempnam(sys_get_temp_dir(), 'front-submit-pending-');
|
|
file_put_contents($tmp, 'pdf test');
|
|
$real_submit = new FrontSubmit();
|
|
foreach ([false, true] as $sideload) {
|
|
$response = $real_submit->persist_uploaded_file('resource', [
|
|
'name' => 'document.pdf', 'tmp_name' => $tmp, 'size' => filesize($tmp), 'error' => UPLOAD_ERR_OK,
|
|
], $sideload);
|
|
fp_assert($response instanceof \WP_REST_Response && $response->get_status() === 201, 'real upload persistence should return a created response');
|
|
$id = (int) ($response->get_data()['attachment_id'] ?? 0);
|
|
fp_assert($id > 0 && fp_pending($id), 'each normalized upload must be marked pending');
|
|
fp_assert(($GLOBALS['front_pending_state']['post_meta'][$id]['_aksrt_front_submit_media'] ?? '') === 'resource', 'upload should retain the normalized media type');
|
|
}
|
|
fp_assert(count($GLOBALS['front_pending_state']['upload_calls']) === 2, 'single and sideload calls should each persist once');
|
|
fp_assert(empty($GLOBALS['front_pending_state']['upload_calls'][0]['overrides']['action']), 'ordinary upload should use the upload action');
|
|
fp_assert(($GLOBALS['front_pending_state']['upload_calls'][1]['overrides']['action'] ?? '') === 'wp_handle_sideload', 'chunk merge sideload should use the sideload action');
|
|
$GLOBALS['front_pending_state']['failed_js_ids'][] = 2002;
|
|
$un_normalized = $real_submit->persist_uploaded_file('resource', [
|
|
'name' => 'unreadable.pdf', 'tmp_name' => $tmp, 'size' => filesize($tmp), 'error' => UPLOAD_ERR_OK,
|
|
]);
|
|
fp_assert(is_wp_error($un_normalized), 'upload normalization failure should return an error');
|
|
$premature_marks = array_filter($GLOBALS['front_pending_state']['meta_writes'], static fn (array $row): bool => $row['id'] === 2002 && $row['key'] === PendingUploads::META);
|
|
fp_assert($premature_marks === [], 'upload must not be marked pending before response normalization succeeds');
|
|
@unlink($tmp);
|
|
|
|
// Draft save covers all persisted reference sources, including more than the
|
|
// default twenty URLs in both saved pay metadata and post_content.
|
|
fp_reset();
|
|
$id_refs = [100, 101, 102, 103, 104, 105, 106, 107, 108];
|
|
foreach ($id_refs as $id) {
|
|
$mime = in_array($id, [101, 104], true) ? 'video/mp4' : ($id === 102 ? 'application/pdf' : 'image/jpeg');
|
|
fp_attachment($id, 7, $mime);
|
|
}
|
|
$pay_url_ids = [];
|
|
$pay_downloads = [
|
|
['attachment_id' => 107, 'url' => ''],
|
|
['attachment_id' => 108, 'url' => ''],
|
|
];
|
|
for ($i = 0; $i < 23; $i++) {
|
|
$id = 200 + $i;
|
|
$url = 'https://example.test/uploads/pay-url-' . $i . '.pdf';
|
|
fp_attachment($id, 7, 'application/pdf', 0, 'inherit', true, null);
|
|
$GLOBALS['front_pending_state']['url_map'][$url] = $id;
|
|
$pay_downloads[] = ['attachment_id' => 0, 'url' => $url];
|
|
$pay_url_ids[] = $id;
|
|
}
|
|
$body_url_ids = [];
|
|
$body_urls = [];
|
|
for ($i = 0; $i < 23; $i++) {
|
|
$id = 300 + $i;
|
|
$url = 'https://example.test/uploads/body-url-' . $i . '.jpg';
|
|
fp_attachment($id, 7, 'image/jpeg', 0, 'inherit', true, null);
|
|
$GLOBALS['front_pending_state']['url_map'][$url] = $id;
|
|
$body_urls[] = $url;
|
|
$body_url_ids[] = $id;
|
|
}
|
|
$unreferenced = 399;
|
|
fp_attachment($unreferenced, 7, 'image/jpeg');
|
|
$content = 'Draft body with enough text for a save. '
|
|
. '<img data-edit-file-id="100" src="/image.jpg">'
|
|
. '<video data-edit-file-id="101" data-video-url="https://video.example.test/movie.mp4"></video>'
|
|
. '<a data-aksrt-file-id="102" href="/resource.pdf">resource</a>'
|
|
. implode(' ', $body_urls);
|
|
$result = $submit->persist(fp_article_data($content, [
|
|
'fake_saved_pay_meta' => ['downloads' => $pay_downloads],
|
|
'fake_featured_meta' => [
|
|
'featured_video_attachment_id' => 104,
|
|
'featured_slide' => '105,106',
|
|
],
|
|
'fake_thumbnail_id' => 103,
|
|
]), 'posts_draft');
|
|
fp_assert(!empty($result['success']), 'draft with persisted references should save');
|
|
$draft_id = (int) ($result['post_id'] ?? 0);
|
|
fp_assert(($GLOBALS['front_pending_state']['posts'][$draft_id]->post_status ?? '') === 'draft', 'draft status should be preserved');
|
|
$claimed_ids = array_merge($id_refs, $pay_url_ids, $body_url_ids);
|
|
foreach ($claimed_ids as $id) fp_assert(!fp_pending($id), 'saved draft reference should be claimed: ' . $id);
|
|
foreach ([100, 101, 102, 103, 104, 105, 106, 107, 108] as $id) {
|
|
fp_assert(($GLOBALS['front_pending_state']['posts'][$id]->post_parent ?? -1) === $draft_id, 'ID-backed reference should bind to the saved draft: ' . $id);
|
|
}
|
|
fp_assert(fp_pending($unreferenced), 'newly uploaded but unreferenced media should remain pending');
|
|
|
|
// Pending submissions also synchronize after a successful post write.
|
|
fp_reset();
|
|
fp_attachment(410, 7, 'image/jpeg');
|
|
$pending = $submit->persist(fp_article_data('<p>Pending body has sufficient words.</p><img data-edit-file-id="410" src="/p.jpg">'));
|
|
fp_assert(!empty($pending['success']), 'pending post should save');
|
|
$pending_id = (int) $pending['post_id'];
|
|
fp_assert(($GLOBALS['front_pending_state']['posts'][$pending_id]->post_status ?? '') === 'pending', 'normal submit should retain pending review status');
|
|
fp_assert(!fp_pending(410), 'pending-review post body image should be claimed');
|
|
|
|
// Edit path reuses the production persist and synchronization implementation.
|
|
fp_reset();
|
|
$edit_post = fp_post(420, 'post', 7, 'publish', 'Old content');
|
|
fp_attachment(421, 7, 'video/mp4');
|
|
$edited = $submit->persist(fp_article_data('<p>Edited body has enough content.</p><video data-video-url="https://video.example.test/edit.mp4" data-edit-file-id="421"></video>', ['posts_id' => 420]));
|
|
fp_assert(!empty($edited['success']) && (int) $edited['post_id'] === 420, 'owned post should edit successfully');
|
|
fp_assert($edit_post->post_content !== 'Old content', 'edit should write the new body before synchronization');
|
|
fp_assert(!fp_pending(421) && $GLOBALS['front_pending_state']['posts'][421]->post_parent === 420, 'edit body media should be claimed and attached');
|
|
|
|
// Existing media is reused without creating a new pending marker.
|
|
fp_reset();
|
|
$reuse_post = fp_post(430, 'post', 7, 'draft', '');
|
|
fp_attachment(431, 7, 'image/jpeg', 430, 'inherit', false, 'image');
|
|
$reuse_writes_before = count(array_filter($GLOBALS['front_pending_state']['meta_writes'], static fn (array $row): bool => $row['id'] === 431 && $row['key'] === PendingUploads::META));
|
|
$reuse = $submit->persist(fp_article_data('<p>Reused attachment body text.</p><img data-edit-file-id="431" src="/reuse.jpg">', ['posts_id' => 430]), 'posts_draft');
|
|
fp_assert(!empty($reuse['success']), 'existing media reuse should save');
|
|
$reuse_writes_after = count(array_filter($GLOBALS['front_pending_state']['meta_writes'], static fn (array $row): bool => $row['id'] === 431 && $row['key'] === PendingUploads::META));
|
|
fp_assert($reuse_writes_before === $reuse_writes_after && !fp_pending(431), 'reused media must not be marked pending again');
|
|
fp_assert($GLOBALS['front_pending_state']['posts'][431]->post_parent === 430, 'reused media should remain attached to its post');
|
|
|
|
// Removing an edit reference unbinds only; it neither deletes nor re-pends.
|
|
fp_reset();
|
|
fp_post(440, 'post', 7, 'draft', '');
|
|
fp_attachment(441, 7, 'image/jpeg', 440, 'inherit', false, 'image');
|
|
$removed = $submit->persist(fp_article_data('<p>Reference removed on edit.</p>', ['posts_id' => 440]), 'posts_draft');
|
|
fp_assert(!empty($removed['success']), 'edit removing a media reference should save');
|
|
fp_assert(isset($GLOBALS['front_pending_state']['posts'][441]), 'unreferenced attached media must not be deleted');
|
|
fp_assert($GLOBALS['front_pending_state']['posts'][441]->post_parent === 0, 'removed media should be detached from the edited post');
|
|
fp_assert(get_post_meta(441, '_aksrt_front_submit_media', true) === '', 'removed media should lose only its submit relation marker');
|
|
fp_assert(!fp_pending(441) && $GLOBALS['front_pending_state']['deleted'] === [], 'removed media must not be deleted or marked pending again');
|
|
|
|
// A payment validation error happens after the body write. The saved body is
|
|
// protected, but the request's featured values were never persisted and must
|
|
// therefore not be considered references.
|
|
fp_reset();
|
|
fp_attachment(450, 7, 'image/jpeg');
|
|
fp_attachment(451, 7, 'image/jpeg');
|
|
$pay_error = $submit->persist(fp_article_data('<p>Body remains persisted after pay error.</p><img data-edit-file-id="450" src="/saved.jpg">', [
|
|
'fake_pay_failure' => true,
|
|
'fake_featured_meta' => ['featured_video_attachment_id' => 0, 'featured_slide' => '451'],
|
|
'fake_thumbnail_id' => 451,
|
|
]));
|
|
fp_assert(empty($pay_error['success']) && (int) ($pay_error['post_id'] ?? 0) > 0, 'pay validation should return its error after the post was written');
|
|
fp_assert(!fp_pending(450), 'persisted body reference should be claimed even when payment validation fails');
|
|
fp_assert(fp_pending(451), 'unsaved featured media from failed request must not be claimed');
|
|
fp_assert($GLOBALS['front_pending_state']['featured_save_calls'] === 0, 'featured metadata must not be saved after payment failure');
|
|
|
|
// Failed insert/update writes cannot claim anything because no saved post is
|
|
// synchronized. Test both the create and edit branches.
|
|
fp_reset();
|
|
fp_attachment(460, 7, 'image/jpeg');
|
|
$GLOBALS['front_pending_state']['fail_insert'] = true;
|
|
$insert_error = $submit->persist(fp_article_data('<p>Insert error body is never saved.</p><img data-edit-file-id="460">'));
|
|
fp_assert(empty($insert_error['success']) && fp_pending(460), 'insert error must leave referenced media pending');
|
|
|
|
fp_reset();
|
|
fp_post(470, 'post', 7, 'draft', 'Existing post');
|
|
fp_attachment(471, 7, 'image/jpeg');
|
|
$GLOBALS['front_pending_state']['fail_update'] = true;
|
|
$update_error = $submit->persist(fp_article_data('<p>Update error body is never saved.</p><img data-edit-file-id="471">', ['posts_id' => 470]), 'posts_draft');
|
|
fp_assert(empty($update_error['success']) && fp_pending(471), 'update error must leave referenced media pending');
|
|
|
|
// Invalid references (foreign owner, invalid MIME/status/type, conflicting
|
|
// parent, missing ID) are rejected by the real media attachment checks.
|
|
fp_reset();
|
|
fp_attachment(480, 8, 'image/jpeg');
|
|
fp_attachment(481, 7, 'application/pdf');
|
|
fp_attachment(482, 7, 'image/jpeg', 0, 'trash');
|
|
fp_attachment(483, 7, 'image/jpeg', 999, 'inherit');
|
|
fp_attachment(484, 7, 'video/mp4', 0, 'inherit', true, null);
|
|
fp_post(485, 'post', 7, 'inherit', '', 0, 'image/jpeg');
|
|
PendingUploads::mark(485);
|
|
$invalid_content = '<p>Invalid media references must not be claimed.</p>'
|
|
. '<img data-edit-file-id="480"><img data-edit-file-id="481">'
|
|
. '<img data-edit-file-id="482"><img data-edit-file-id="483">'
|
|
. '<img data-edit-file-id="484"><img data-edit-file-id="485"><img data-edit-file-id="99999">';
|
|
$invalid = $submit->persist(fp_article_data($invalid_content));
|
|
fp_assert(!empty($invalid['success']), 'post containing invalid media markers should still save');
|
|
foreach ([480, 481, 482, 483, 484, 485] as $id) fp_assert(fp_pending($id), 'invalid attachment must remain pending: ' . $id);
|
|
fp_assert($GLOBALS['front_pending_state']['posts'][483]->post_parent === 999, 'attachment already parented elsewhere must not be reassigned');
|
|
|
|
// A relation update failure cannot expose already-saved references to TTL
|
|
// cleanup: claiming still runs after association attempts.
|
|
fp_reset();
|
|
fp_attachment(490, 7, 'image/jpeg');
|
|
$GLOBALS['front_pending_state']['fail_relation_updates'] = true;
|
|
$association_error = $submit->persist(fp_article_data('<p>Saved content survives relation failure.</p><img data-edit-file-id="490">'));
|
|
fp_assert(!empty($association_error['success']), 'database relation failure should not change the saved post result');
|
|
fp_assert(!fp_pending(490), 'saved body attachment must be claimed even when parent association fails');
|
|
fp_assert($GLOBALS['front_pending_state']['posts'][490]->post_parent === 0, 'injected relation failure should leave parent unchanged');
|
|
|
|
// The existing owner and post-type guards remain in force for direct syncs.
|
|
fp_reset();
|
|
fp_attachment(500, 7, 'image/jpeg');
|
|
$foreign_post = new \WP_Post(['ID' => 501, 'post_type' => 'post', 'post_author' => 8, 'post_status' => 'draft', 'post_content' => '<img data-edit-file-id="500">']);
|
|
$submit->sync_post_attachments($foreign_post);
|
|
$submit->sync_post_attachments(new \WP_Post(['ID' => 502, 'post_type' => 'page', 'post_author' => 7, 'post_status' => 'draft', 'post_content' => '<img data-edit-file-id="500">']));
|
|
fp_assert(fp_pending(500) && $GLOBALS['front_pending_state']['posts'][500]->post_parent === 0, 'foreign-author and non-post sync calls must not claim or attach media');
|
|
|
|
echo "Front-submit pending behavior harness passed\n";
|
|
}
|