*/ private function get_all(): array { $raw = get_option( self::OPTION, array() ); return is_array( $raw ) ? $raw : array(); } /** * Persist the registry array to the database. * * @param array $entries Full registry array to save. */ private function save( array $entries ): void { update_option( self::OPTION, $entries, false ); } // ── Writes ──────────────────────────────────────────────────────────── /** * Store a per-post scan entry keyed by scan_id. * * @param string $scan_id Scan ID returned by the WSC API. * @param int $post_id WordPress post ID. * @param string $url Permalink of the post. */ public function add_per_post( string $scan_id, int $post_id, string $url ): void { $entries = $this->get_all(); $entries[ $scan_id ] = array( 'post_id' => $post_id, 'url' => $url, 'created_at' => time(), ); $this->save( $entries ); } /** * Remove a per-post entry after webhook delivery. * * @param string $scan_id Scan ID to remove. */ public function remove( string $scan_id ): void { $entries = $this->get_all(); unset( $entries[ $scan_id ] ); $this->save( $entries ); } /** * Delete the entire per-post registry (uninstall / full reset). */ public function clear_all(): void { delete_option( self::OPTION ); } // ── Reads ───────────────────────────────────────────────────────────── /** * Check whether a scan_id is currently valid — site-level or per-post. * * @param string $scan_id Incoming scan ID from the webhook payload. * @return bool True if known and not expired. */ public function is_valid( string $scan_id ): bool { return $this->get_type( $scan_id ) !== null; } /** * Classify a scan_id as 'site', 'per_post', or null (unknown/expired). * * Site-level: compares against the existing cmplz_wsc_scan_id string option. * Per-post: looks up the per-post registry within TTL. * * @param string $scan_id Incoming scan ID. * @return string|null 'site' | 'per_post' | null */ public function get_type( string $scan_id ): ?string { $site_id = get_option( 'cmplz_wsc_scan_id', '' ); if ( is_string( $site_id ) && '' !== $site_id && $site_id === $scan_id ) { return 'site'; } $entries = $this->get_all(); if ( isset( $entries[ $scan_id ]['post_id'] ) ) { $age = time() - ( $entries[ $scan_id ]['created_at'] ?? 0 ); if ( $age < self::SCAN_TTL ) { return 'per_post'; } } return null; } /** * Return a per-post entry if it exists and is within TTL, otherwise null. * * @param string $scan_id Per-post scan ID. * @return array{post_id: int, url: string, created_at: int}|null */ public function get_per_post_entry( string $scan_id ): ?array { $entries = $this->get_all(); $entry = $entries[ $scan_id ] ?? null; if ( ! $entry || ! isset( $entry['post_id'] ) ) { return null; } if ( ( time() - ( $entry['created_at'] ?? 0 ) ) >= self::SCAN_TTL ) { return null; } return $entry; } /** * Return all non-expired per-post entries. * * @return array */ public function get_all_per_post(): array { $entries = $this->get_all(); $cutoff = time() - self::SCAN_TTL; $result = array(); foreach ( $entries as $scan_id => $entry ) { if ( isset( $entry['post_id'] ) && ( $entry['created_at'] ?? 0 ) > $cutoff ) { $result[ $scan_id ] = $entry; } } return $result; } /** * Check whether a post already has a non-expired per-post scan in flight. * * @param int $post_id WordPress post ID. * @return bool True if an active entry exists for this post. */ public function has_active_per_post_for_post_id( int $post_id ): bool { $entries = $this->get_all(); $cutoff = time() - self::SCAN_TTL; foreach ( $entries as $entry ) { if ( isset( $entry['post_id'] ) && (int) $entry['post_id'] === $post_id && ( $entry['created_at'] ?? 0 ) > $cutoff ) { return true; } } return false; } } }