params[$key] ?? null; } } final class WP_REST_Response { public function __construct(public array $data, public int $status = 200) {} } function absint($value): int { return abs((int) $value); } function __($text, $domain = '') { return $text; } function get_current_user_id(): int { return $GLOBALS['current_user']; } function is_user_logged_in(): bool { return get_current_user_id() > 0; } function get_post(int $id) { return $GLOBALS['posts'][$id] ?? null; } function get_post_meta(int $id, string $key, bool $single = false) { return $GLOBALS['meta'][$id][$key] ?? ''; } function update_post_meta(int $id, string $key, $value): void { $GLOBALS['meta'][$id][$key] = $value; } function delete_post_meta(int $id, string $key): void { unset($GLOBALS['meta'][$id][$key]); } function wp_delete_attachment(int $id, bool $force) { $GLOBALS['deletions'][] = [$id, $force]; if (!empty($GLOBALS['delete_failures'][$id])) return false; $post = get_post($id); unset($GLOBALS['posts'][$id], $GLOBALS['meta'][$id]); return $post; } function wp_get_upload_dir(): array { return ['baseurl' => 'https://example.test/wp-content/uploads']; } function wp_parse_url(string $url, int $component = -1) { return parse_url($url, $component); } function set_url_scheme(string $url): string { return preg_replace('#^https?:#', 'https:', $url); } function attachment_url_to_postid(string $url): int { return $GLOBALS['attachment_urls'][$url] ?? 0; } function add_action(string $hook, callable $callback, int $priority = 10, int $accepted = 1): void { $GLOBALS['hooks'][$hook] = [$callback, $accepted]; } function register_rest_route(string $namespace, string $route, array $args): void { $GLOBALS['routes'][] = [$namespace, $route, $args]; } function wp_next_scheduled(string $hook) { return $GLOBALS['scheduled'][$hook] ?? false; } function wp_schedule_event(int $at, string $recurrence, string $hook): void { $GLOBALS['schedule_calls'][] = [$at, $recurrence, $hook]; $GLOBALS['scheduled'][$hook] = $at; } final class PendingWpdb { public string $posts = 'wp_posts'; public string $postmeta = 'wp_postmeta'; public array $queries = []; public ?array $forced_ids = null; public $after_select = null; private array $params = []; public function prepare(string $sql, ...$params): string { $this->params = $params; $this->queries[] = ['sql' => $sql, 'params' => $params]; return $sql; } public function get_col(string $sql): array { if ($this->forced_ids !== null) { $ids = $this->forced_ids; } else { [$key, $cutoff, $limit] = $this->params; $posts = $GLOBALS['posts']; ksort($posts); $ids = []; foreach ($posts as $id => $post) { $pending = get_post_meta($id, $key, true); if ($post->post_type !== 'attachment' || $pending === '' || (int) $pending >= $cutoff) continue; if (str_contains($sql, 'p.post_parent = 0') && $post->post_parent !== 0) continue; $ids[] = $id; if (count($ids) >= $limit) break; } } if (is_callable($this->after_select)) ($this->after_select)(); return $ids; } } require dirname(__DIR__, 2) . '/src/Attachment/PendingUploads.php'; use Jio\Attachment\PendingUploads; function reset_fixture(): void { $GLOBALS['posts'] = []; $GLOBALS['meta'] = []; $GLOBALS['attachment_urls'] = []; $GLOBALS['deletions'] = []; $GLOBALS['delete_failures'] = []; $GLOBALS['current_user'] = 7; $GLOBALS['wpdb'] = new PendingWpdb(); } function attachment(int $id, int $owner = 7, int $parent = 0, ?int $pending = null): string { $GLOBALS['posts'][$id] = (object) ['ID' => $id, 'post_type' => 'attachment', 'post_author' => $owner, 'post_parent' => $parent]; $url = 'https://example.test/wp-content/uploads/file-' . $id . '.jpg'; $GLOBALS['attachment_urls'][$url] = $id; if ($pending !== null) update_post_meta($id, PendingUploads::META, $pending); return $url; } function pending_ids(): array { return array_values(array_filter(array_keys($GLOBALS['posts']), static fn(int $id): bool => get_post_meta($id, PendingUploads::META, true) !== '')); } $service = PendingUploads::get_instance(); $results = []; reset_fixture(); $GLOBALS['hooks'] = []; $GLOBALS['routes'] = []; $GLOBALS['scheduled'] = []; $GLOBALS['schedule_calls'] = []; $service->init(); $service->maybe_schedule(); $service->maybe_schedule(); $service->register_routes(); $route = $GLOBALS['routes'][0]; $permission = $route[2]['permission_callback']; $member_allowed = $permission(); $GLOBALS['current_user'] = 0; $results['registration'] = [ 'hooks' => array_keys($GLOBALS['hooks']), 'schedule_count' => count($GLOBALS['schedule_calls']), 'recurrence' => $GLOBALS['schedule_calls'][0][1], 'namespace' => $route[0], 'route' => $route[1], 'method' => $route[2]['methods'], 'member_allowed' => $member_allowed, 'guest_allowed' => $permission(), ]; reset_fixture(); for ($id = 1; $id <= 25; $id++) attachment($id, 7, 0, time() - 2 * DAY_IN_SECONDS); attachment(26, 8, 0, time() - 2 * DAY_IN_SECONDS); attachment(27); PendingUploads::mark(27); $marked_at = get_post_meta(27, PendingUploads::META, true); PendingUploads::mark(0); PendingUploads::claim_ids(array_merge(range(1, 27), [1, 0]), 7); $results['claim_ids'] = ['remaining' => pending_ids(), 'marked_recently' => abs(time() - $marked_at) <= 2, 'invalid_marked' => isset($GLOBALS['meta'][0])]; reset_fixture(); $urls = []; for ($id = 1; $id <= 25; $id++) $urls[] = attachment($id, 7, 0, time() - 2 * DAY_IN_SECONDS); PendingUploads::claim_urls($urls, 7); $default_remaining = pending_ids(); PendingUploads::claim_urls($urls, 7, null); $results['claim_urls'] = ['default_remaining' => $default_remaining, 'unlimited_remaining' => pending_ids()]; reset_fixture(); $urls = []; for ($id = 1; $id <= 25; $id++) $urls[] = attachment($id, 7, 0, time() - 2 * DAY_IN_SECONDS); $other = attachment(26, 8, 0, time() - 2 * DAY_IN_SECONDS); $outside = 'https://cdn.other.test/file.jpg'; attachment(27, 7, 0, time() - 2 * DAY_IN_SECONDS); $GLOBALS['attachment_urls'][$outside] = 27; $text = implode(' ', array_map(static fn(string $url): string => '', array_merge($urls, [$other, $outside]))); PendingUploads::claim_text($text, 7); $text_default = pending_ids(); PendingUploads::claim_text($text, 7, null); $results['claim_text'] = ['default_remaining' => $text_default, 'unlimited_remaining' => pending_ids()]; reset_fixture(); attachment(1, 7, 0, time()); attachment(2, 8, 0, time()); attachment(3); attachment(4, 7, 99, time()); $denied = [PendingUploads::discard(2, 7), PendingUploads::discard(3, 7), PendingUploads::discard(4, 7), PendingUploads::discard(1, 0)]; $allowed = $service->rest_discard(new WP_REST_Request(['id' => 1])); $repeat = $service->rest_discard(new WP_REST_Request(['id' => 1])); $results['discard'] = ['denied' => $denied, 'success' => $allowed->data['success'], 'repeat_status' => $repeat->status, 'deleted' => $GLOBALS['deletions']]; reset_fixture(); $expired = time() - 2 * DAY_IN_SECONDS; attachment(1, 7, 0, $expired); attachment(2, 7, 0, time() - HOUR_IN_SECONDS); attachment(3); attachment(4, 7, 99, $expired); $service->cleanup(); $results['cleanup'] = [ 'deleted' => $GLOBALS['deletions'], 'remaining' => array_keys($GLOBALS['posts']), 'query' => $GLOBALS['wpdb']->queries[0], ]; reset_fixture(); for ($id = 1; $id <= 6; $id++) attachment($id, 7, 0, $expired); $GLOBALS['posts'][6]->post_type = 'post'; $GLOBALS['wpdb']->forced_ids = range(1, 6); $GLOBALS['wpdb']->after_select = static function (): void { PendingUploads::claim_ids([1], 7); $GLOBALS['posts'][2]->post_parent = 99; PendingUploads::mark(3); unset($GLOBALS['posts'][4]); }; $service->cleanup(); $results['stale_selection'] = ['deleted' => $GLOBALS['deletions']]; reset_fixture(); for ($id = 1; $id <= 105; $id++) attachment($id, 7, 0, $expired); $service->cleanup(); $first_batch = count($GLOBALS['deletions']); $service->cleanup(); $results['batch'] = ['first' => $first_batch, 'total' => count($GLOBALS['deletions']), 'remaining' => count($GLOBALS['posts'])]; reset_fixture(); attachment(1, 7, 0, $expired); $GLOBALS['delete_failures'][1] = true; $service->cleanup(); $retained_after_failure = isset($GLOBALS['posts'][1]) && get_post_meta(1, PendingUploads::META, true) !== ''; $GLOBALS['delete_failures'][1] = false; $service->cleanup(); $results['retry'] = ['retained' => $retained_after_failure, 'deleted_after_retry' => !isset($GLOBALS['posts'][1])]; echo json_encode($results, JSON_THROW_ON_ERROR);