$token, 'owner' => sanitize_key($owner), 'expires' => time() + $ttl, ]; if (!add_option(self::OPTION, $value, '', false)) { $current = get_option(self::OPTION, []); if (!is_array($current) || (int) ($current['expires'] ?? 0) >= time()) { return false; } delete_option(self::OPTION); /** @phpstan-ignore-next-line A concurrent request may acquire the option after deletion. */ if (!add_option(self::OPTION, $value, '', false)) { return false; } } $this->token = $token; return true; } public function release(): void { if ($this->token === null) { return; } $current = get_option(self::OPTION, []); if (is_array($current) && hash_equals($this->token, (string) ($current['token'] ?? ''))) { delete_option(self::OPTION); } $this->token = null; } public static function releaseJob(int $jobId): void { $current = get_option(self::OPTION, []); $owner = is_array($current) ? (string) ($current['owner'] ?? '') : ''; if (!in_array($owner, ['publication-' . $jobId, 'media-' . $jobId], true)) { return; } // Compare the complete serialized value so a replacement lock acquired // after this read cannot be deleted by the cancellation request. global $wpdb; $deleted = $wpdb->query($wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name=%s AND option_value=%s", self::OPTION, maybe_serialize($current) )); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ($deleted) { wp_cache_delete(self::OPTION, 'options'); } } public static function diagnostics(): array { $current = get_option(self::OPTION, []); if (!is_array($current) || (int) ($current['expires'] ?? 0) < time()) { return ['locked' => false]; } return [ 'locked' => true, 'owner' => (string) ($current['owner'] ?? 'unknown'), 'expires_at' => gmdate('c', (int) $current['expires']), ]; } }