$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, '_jio_front_submit_media', $kind); $GLOBALS['front_pending_state']['url_map']['https://example.test/uploads/media-' . $id . '.' . fp_ext($mime)] = $id; if ($pending) \Jio\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, \Jio\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 jio_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 Jio\Core { final class DomainModel { public const PAY_META_KEY = '_jio_pay'; public const POST_META_KEY = 'jio_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 Jio\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 Jio\Attachment\PendingUploads; use Jio\Core\FrontSubmit; use Jio\Core\FrontSubmitPendingProbe; use Jio\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]['_jio_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. ' . '' . '' . 'resource' . 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('

Pending body has sufficient words.

')); 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('

Edited body has enough content.

', ['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('

Reused attachment body text.

', ['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('

Reference removed on edit.

', ['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, '_jio_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('

Body remains persisted after pay error.

', [ '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('

Insert error body is never saved.

')); 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('

Update error body is never saved.

', ['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 = '

Invalid media references must not be claimed.

' . '' . '' . ''; $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('

Saved content survives relation failure.

')); 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' => '']); $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' => ''])); 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"; }