get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name=%s", $name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- raw current option value for reviewed-action compare and readback; the options cache must be bypassed return null === $raw ? $default : maybe_unserialize( $raw ); } /** * Forget the cached values of Image Manager's options (and the options' shared caches) and the cached sources. */ public static function flush(): void { global $wpdb; $names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'afsd_im_' ) . '%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- enumerates this plugin's own option names by prefix so their cache entries can be flushed foreach ( $names as $name ) { wp_cache_delete( $name, 'options' ); } wp_cache_delete( 'alloptions', 'options' ); wp_cache_delete( 'notoptions', 'options' ); Sources::forget_cache(); } /** * The name of this site's Image Manager lock. * * @return string im_owner_ and a hash of the table prefix and the WordPress folder. */ public static function lock_name(): string { global $wpdb; return 'im_owner_' . substr( hash( 'sha256', $wpdb->prefix . '|' . ABSPATH ), 0, 48 ); } /** * Whether the database can hold the lock and the change transactions: MySQL or MariaDB (not SQLite), with every table * involved on InnoDB. * * @return bool True when it can. */ public static function supported(): bool { global $wpdb; if ( ! empty( $wpdb->is_sqlite ) || ! method_exists( $wpdb, 'db_version' ) ) { return false; } $tables = array( $wpdb->options, $wpdb->posts, $wpdb->postmeta, $wpdb->terms, $wpdb->term_taxonomy, $wpdb->term_relationships ); foreach ( array( 'sources', 'pages', 'owners', 'locks', 'urls', 'batch' ) as $name ) { $tables[] = Repository::table( $name ); } $rows = $wpdb->get_results( $wpdb->prepare( 'SHOW TABLE STATUS WHERE Name IN (' . implode( ',', array_fill( 0, count( $tables ), '%s' ) ) . ')', ...$tables ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SHOW TABLE STATUS engine probe; no WordPress API exposes it if ( ! is_array( $rows ) || count( $rows ) !== count( $tables ) ) { return false; } foreach ( $rows as $row ) { if ( 'innodb' !== strtolower( $row['Engine'] ?? '' ) ) { return false; } }return true; } /** * Reentrant, connection-owned MySQL mutex. All native Image Manager writers use this same owner. * $wait (seconds, at most 15) is only for rare human saves and lifecycle actions that should finish after a * short background batch instead of being lost; page loads and runners never wait. * * @param int $wait How long to wait for the lock, in seconds (0 to 15). * @return bool True when the lock is held; false on a database that cannot hold it. * @throws \RuntimeException With worker_busy when another writer holds it, or reviewed_database_unavailable during an * approved change on such a database. * @throws \Throwable What the check of a running approved change threw, passed on after the lock is released. */ public static function enter( int $wait = 0 ): bool { global $wpdb; if ( self::$depth ) { self::guard(); ++self::$depth; return true; } if ( ! self::supported() ) { if ( null !== self::$execution ) { throw new \RuntimeException( 'reviewed_database_unavailable' ); } return false; // Preserve the native service's existing lock behavior on unsupported adapter hosts. } $locked = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s,%d)', self::lock_name(), max( 0, min( 15, $wait ) ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- MySQL named owner lock (GET_LOCK); no WordPress API exists and it must never be cached if ( '1' !== (string) $locked ) { throw new \RuntimeException( 'worker_busy' ); } self::$depth = 1; try { self::guard(); } catch ( \Throwable $e ) { self::leave( true ); throw $e; } return true; } /** * True when another Image Manager writer currently owns the work (owner lock or a worker lease). * * @param \Throwable $e The error. * @return bool True when it is. */ public static function busy( \Throwable $e ): bool { return $e instanceof \RuntimeException && 'worker_busy' === $e->getMessage(); } /** * Page-load, scheduled-runner and update-check housekeeping. If another writer owns the lock, this request * skips the optional step instead of failing the whole request; the owner (or a later request) reconciles it. * Mutual exclusion is unchanged: nothing here runs without the lock. Inside an approved execution the * callback runs unchanged so a busy result still fails that execution. * * @param callable $callback The step. * @return bool True when it ran; false when Image Manager was busy. * @throws \RuntimeException Any failure other than a busy lock, passed on. */ public static function housekeeping( callable $callback ): bool { if ( null !== self::$execution ) { $callback(); return true; } try { $callback(); return true; } catch ( \RuntimeException $e ) { if ( ! self::busy( $e ) ) { throw $e; } return false; } } /** * Release the lock once its outermost holder leaves. * * @param bool $locked Whether the matching enter() took it. */ public static function leave( bool $locked ): void { if ( ! $locked || ! self::$depth ) { return; } if ( --self::$depth ) { return; } global $wpdb; $wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', self::lock_name() ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- MySQL named owner lock release (RELEASE_LOCK); no WordPress API exists } /** * During an approved change, check that its authorization still holds. * * @throws \RuntimeException With reviewed_execution_changed when it does not. */ public static function guard(): void { if ( null === self::$execution ) { return; } $ok = call_user_func( self::$execution['context']['guard'] ); if ( true !== $ok ) { throw new \RuntimeException( 'reviewed_execution_changed' ); } } /** 1.9.0: whether this request already holds Image Manager's owner lock (Image Manager itself is writing). */ public static function holding(): bool { return self::$depth > 0; } /** * The marker of the approved change running now. * * @return string The marker, or ''. */ public static function execution_marker(): string { return self::$execution['context']['marker'] ?? ''; } /** * Only the currently claimed callback may account for its own intermediate state. * * @param array $prepared The prepared change. * @param array $principal Who runs it. * @return bool True when it is the change running now and this connection holds the lock. */ public static function owns_execution( array $prepared, array $principal ): bool { global $wpdb; return self::$depth > 0 && null !== self::$execution && self::$execution['principal'] === $principal && hash_equals( self::hash( self::$execution['prepared'] ), self::hash( $prepared ) ) && (string) $wpdb->get_var( $wpdb->prepare( 'SELECT IS_USED_LOCK(%s)=CONNECTION_ID()', self::lock_name() ) ) === '1'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- MySQL named owner lock ownership check (IS_USED_LOCK); must be live } /** * The saved record of an approved change. * * @param string $marker Its marker (afsd-connector: or afsd-im-native: and a hash). * @return array|null The record, or null. */ public static function record( string $marker ): ?array { if ( ! preg_match( '/^(?:afsd-connector|afsd-im-native):[a-f0-9]{64}$/D', $marker ) ) { return null; } $value = self::raw( self::PREFIX . hash( 'sha256', $marker ) ); return is_array( $value ) ? $value : null; } /** * Whether a record belongs to this person, site, marker and prepared change. * * @param array $record The record. * @param array $prepared The prepared change. * @param array $principal Who runs it. * @param string $marker The marker. * @return bool True when it does. */ public static function bound( array $record, array $prepared, array $principal, string $marker ): bool { return ( $record['actor'] ?? null ) === $principal['user_id'] && ( $record['site'] ?? null ) === $principal['site'] && ( $record['marker'] ?? null ) === $marker && ( $record['proposal'] ?? null ) === self::hash( $prepared ); } /** * Whether the change a record belongs to has finished: its record is complete, or its Image Manager proposal or AI * project is applied. * * @param array $record The record. * @return bool True when it has. */ private static function completed( array $record ): bool { if ( 'complete' === ( $record['status'] ?? '' ) ) { return true; } if ( str_starts_with( (string) ( $record['project_id'] ?? '' ), 'proposal_' ) ) { return 'applied' === self::project_state( $record['project_id'] ); } if ( ! class_exists( '\AFSD_Connector_Store' ) ) { return false; } $project = \AFSD_Connector_Store::get( $record['project_id'] ); return is_array( $project ) && $project['actor'] === $record['actor'] && ( $project['body']['site'] ?? '' ) === $record['site'] && ( $project['body']['marker'] ?? '' ) === $record['marker'] && ( $project['body']['state'] ?? '' ) === 'applied' && ( $project['body']['receipt']['operation_id'] ?? '' ) === $record['project_id'] && self::hash( array( 'snapshot' => $project['body']['snapshot'], 'proposed' => $project['body']['proposed'], ) ) === $record['proposal']; } /** * The state of what ran an operation: an AffStackDev AI project, or an Image Manager proposal (1.9.0). * * @param string $project_id project_ or proposal_. * @return string Its state (applied, unknown, ...), or '' when it is not found. */ public static function project_state( string $project_id ): string { if ( str_starts_with( $project_id, 'proposal_' ) ) { $p = AI_Proposals::get( substr( $project_id, 9 ) ); return $p ? (string) $p['status'] : ''; } if ( ! class_exists( '\AFSD_Connector_Store' ) ) { return ''; } $project = \AFSD_Connector_Store::get( $project_id ); return is_array( $project ) ? (string) ( $project['body']['state'] ?? '' ) : ''; } /** * Release the targets that an uncertain change of this person still reserves, when the project or proposal that ran it * is in the unknown state, and record which change released them. * * @param string $marker The uncertain change's marker. * @param string $new_marker The marker of the change that releases it. * @throws \RuntimeException With reviewed_intent_not_uncertain or reviewed_intent_still_active. */ public static function release_intent( string $marker, string $new_marker ): void { self::guard(); $record = self::record( $marker ); if ( ! $record || $record['actor'] !== get_current_user_id() || $record['site'] !== Reviewed_Inputs::site() || ! in_array( $record['status'], array( 'executing', 'unknown', 'rejected' ), true ) ) { throw new \RuntimeException( 'reviewed_intent_not_uncertain' ); } if ( 'unknown' !== self::project_state( $record['project_id'] ) ) { throw new \RuntimeException( 'reviewed_intent_still_active' ); } foreach ( $record['fences'] ?? array() as $key ) { $name = self::PREFIX . 'fence_' . hash( 'sha256', $key ); $fence = self::raw( $name ); if ( ( $fence['marker'] ?? '' ) === $marker ) { self::guard(); delete_option( $name ); } } $record['released_by'] = $new_marker; $record['released_at'] = time(); self::save( $record ); } /** * Save a change's record and check that it reads back the same. * * @param array $record The record. * @return bool True when it does. */ private static function save( array $record ): bool { $name = self::PREFIX . hash( 'sha256', $record['marker'] ); update_option( $name, $record, false ); wp_cache_delete( $name, 'options' ); return self::raw( $name ) === $record; } /** * In the change's transaction, lock the rows it touches: its attachments with their meta and terms, every term for a * folder, collection or no-use change, and Image Manager's options. * * @param array $prepared The prepared change. * @throws \RuntimeException With reviewed_owner_lock_failed when the database refuses. */ private static function lock_targets( array $prepared ): void { global $wpdb; // The advisory mutex covers all Image Manager services. Row locks also fence ordinary WP attachment editors. $ids = array_values( array_unique( array_map( 'intval', $prepared['snapshot']['state']['targets']['attachments'] ?? array() ) ) ); if ( $ids ) { // Row locks inside the change's transaction; caching a lock read would defeat it. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- see the note above. $wpdb->get_col( $wpdb->prepare( 'SELECT ID FROM %i WHERE ID IN (' . implode( ',', array_fill( 0, count( $ids ), '%d' ) ) . ') ORDER BY ID FOR UPDATE', array_merge( array( $wpdb->posts ), $ids ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- see the note above. $wpdb->get_col( $wpdb->prepare( 'SELECT meta_id FROM %i WHERE post_id IN (' . implode( ',', array_fill( 0, count( $ids ), '%d' ) ) . ') ORDER BY post_id,meta_id FOR UPDATE', array_merge( array( $wpdb->postmeta ), $ids ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- see the note above. $wpdb->get_col( $wpdb->prepare( 'SELECT object_id FROM %i WHERE object_id IN (' . implode( ',', array_fill( 0, count( $ids ), '%d' ) ) . ') ORDER BY object_id,term_taxonomy_id FOR UPDATE', array_merge( array( $wpdb->term_relationships ), $ids ) ) ); foreach ( $ids as $id ) { clean_post_cache( $id ); wp_cache_delete( $id, 'post_meta' ); } } $op = $prepared['snapshot']['state']['operation']; if ( preg_match( '/^(folder|collection)_/', $op ) || 'unused_collect' === $op ) { // Includes the empty gap: native term creation/deletion cannot race the reviewed uniqueness read. $wpdb->get_col( "SELECT term_id FROM {$wpdb->terms} ORDER BY term_id FOR UPDATE" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- row locks (FOR UPDATE) inside the reviewed owner transaction; must not be cached $wpdb->get_col( "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} ORDER BY term_taxonomy_id FOR UPDATE" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- row locks (FOR UPDATE) inside the reviewed owner transaction; must not be cached } $wpdb->get_col( $wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name LIKE %s ORDER BY option_name FOR UPDATE", $wpdb->esc_like( 'afsd_im_' ) . '%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- row locks (FOR UPDATE) inside the reviewed owner transaction; must not be cached if ( $wpdb->last_error ) { throw new \RuntimeException( 'reviewed_owner_lock_failed' ); } } /** * Run an approved change exactly once: take the lock, check its authorization and that nothing changed since it was * prepared, save its claim and its target reservations before anything changes, run it in a transaction, save what it * did, then read the result back and compare it with the expected result. A change already claimed is reconciled * instead, never run again; an uncertain end keeps its reservations. * * @param array $prepared The prepared change. * @param array $context The execution's context: its marker, project ID and authorization check. * @param array $principal Who runs it (user and site). * @return array|\WP_Error The result read back, with how it was checked; or an error. * @throws \RuntimeException Only inside this function, where it is caught; callers get no exception. */ public static function execute( array $prepared, array $context, array $principal ) { if ( ! is_callable( $context['guard'] ?? null ) || ! preg_match( '/^(?:afsd-connector|afsd-im-native):[a-f0-9]{64}$/D', (string) ( $context['marker'] ?? '' ) ) || ! preg_match( '/^(?:project|proposal)_[a-f0-9-]{36}$/D', (string) ( $context['project_id'] ?? '' ) ) ) { return self::error( 'context', 'Use the exact approved executing AffStackDev AI project.' ); } $permission = Reviewed_Actions::permission( $principal ); if ( is_wp_error( $permission ) ) { return $permission; } if ( ! self::supported() ) { return self::error( 'database', 'Reviewed Image Manager actions require MySQL/MariaDB and InnoDB owner tables.' ); } $locked = false; $transaction = false; $record = null; $effect_started = false; global $wpdb; try { $locked = self::enter(); self::flush(); $record = self::record( $context['marker'] ); if ( $record ) { return self::bound( $record, $prepared, $principal, $context['marker'] ) && $record['project_id'] === $context['project_id'] ? self::reconcile( $prepared, $context['marker'], $principal ) : self::error( 'identity', 'This approved marker belongs to a different exact project.' ); } $ok = call_user_func( $context['guard'] ); if ( true !== $ok ) { return is_wp_error( $ok ) ? $ok : self::error( 'guard', 'Execution authorization changed.' ); } $ok = Reviewed_Actions::assert_current( $prepared, $principal ); if ( is_wp_error( $ok ) ) { return $ok; } $fences = Reviewed_Owner::fences( $prepared['snapshot']['state'] ); foreach ( $fences as $key ) { $name = self::PREFIX . 'fence_' . hash( 'sha256', $key ); $old = self::raw( $name ); if ( is_array( $old ) && $old['marker'] !== $context['marker'] ) { $prior = self::record( $old['marker'] ); $recovery = in_array( $prepared['snapshot']['state']['operation'], array_merge( array( 'intent_release', 'replace_recover', 'replace_undo', 'workflow_cancel', 'backup_cancel', 'remote_cancel', 'edit_discard', 'pdf_discard', 'replace_discard' ), Reviewed_Owner::extension_recovery() ), true ); if ( ! $prior || ( ! self::completed( $prior ) && 'rejected' !== ( $prior['status'] ?? '' ) && empty( $prior['released_by'] ) && ! $recovery ) ) { return self::error( 'reserved', 'An earlier uncertain operation still reserves this target. Inspect and reconcile it before another write, or prepare the explicit native recovery operation.' ); } } } $record = array( 'schema' => 1, 'marker' => $context['marker'], 'project_id' => $context['project_id'], 'actor' => $principal['user_id'], 'site' => $principal['site'], 'proposal' => self::hash( $prepared ), 'operation' => $prepared['snapshot']['state']['operation'], 'status' => 'executing', 'started' => time(), 'result' => array(), 'evidence' => array(), ); $record['fences'] = $fences; // Commit identity BEFORE any file, mail, HTTP, job or business mutation. A timeout never erases this claim. if ( ! add_option( self::PREFIX . hash( 'sha256', $context['marker'] ), $record, '', false ) ) { return self::error( 'claimed', 'This marker already started; reconcile it without replaying.' ); } foreach ( $fences as $key ) { $name = self::PREFIX . 'fence_' . hash( 'sha256', $key ); update_option( $name, array( 'marker' => $context['marker'], 'project_id' => $context['project_id'], ), false ); if ( self::raw( $name )['marker'] !== $context['marker'] ) { throw new \RuntimeException( 'reviewed_fence_unconfirmed' ); } } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- transaction control for the reviewed owner transaction; no WordPress API exists if ( false === $wpdb->query( 'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ' ) || false === $wpdb->query( 'START TRANSACTION' ) ) { return self::error( 'database', 'The owner transaction could not start; the durable marker remains reserved.' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- transaction control for the reviewed owner transaction; no WordPress API exists $transaction = true; self::lock_targets( $prepared ); self::flush(); $ok = Reviewed_Actions::assert_current( $prepared, $principal ); if ( is_wp_error( $ok ) ) { throw new \RuntimeException( 'reviewed_state_changed' ); } self::$execution = array( 'prepared' => $prepared, 'context' => $context, 'principal' => $principal, ); self::guard(); $effect_started = true; $evidence = Reviewed_Owner::apply( $prepared, $context ); if ( is_wp_error( $evidence ) ) { $record['failure_code'] = $evidence->get_error_code(); throw new \RuntimeException( 'reviewed_owner_rejected' ); } $record['evidence'] = $evidence; $record['owner_returned'] = true; self::flush(); if ( ! self::save( $record ) ) { throw new \RuntimeException( 'reviewed_evidence_unconfirmed' ); } $observed = Reviewed_Owner::observe( $prepared, $evidence ); if ( ! Reviewed_Actions::matches( $prepared['proposed']['expected_after'], $observed ) ) { throw new \RuntimeException( 'reviewed_readback_mismatch' ); } $record['result'] = $observed; $record['status'] = 'complete'; $record['finished'] = time(); self::guard(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- transaction control for the reviewed owner transaction; no WordPress API exists if ( ! self::save( $record ) || false === $wpdb->query( 'COMMIT' ) ) { throw new \RuntimeException( 'reviewed_receipt_unconfirmed' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- transaction control for the reviewed owner transaction; no WordPress API exists $transaction = false; return array( 'state' => $observed, 'verification' => 'The native Image Manager owner completed this exact approved operation and its saved result was read back. Queued work is identified separately from completed processing.', 'editor_url' => Admin::url(), ); } catch ( \Throwable $error ) { if ( $record ) { $record['status'] = $effect_started ? 'unknown' : 'rejected'; $record['failure_code'] = $record['failure_code'] ?? sanitize_key( $error->getMessage() ); // Retain the owner's actual partial state: files/external effects cannot be rolled back honestly. self::save( $record ); } if ( $transaction ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- transaction control for the reviewed owner transaction; no WordPress API exists $wpdb->query( 'COMMIT' ); $transaction = false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- transaction control for the reviewed owner transaction; no WordPress API exists return self::error( 'uncertain', 'The owner result is incomplete or uncertain. Inspect and reconcile this same project; its write will not be repeated.' ); } finally { if ( $transaction ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- transaction control for the reviewed owner transaction; no WordPress API exists $wpdb->query( 'COMMIT' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- transaction control for the reviewed owner transaction; no WordPress API exists self::$execution = null; self::flush(); self::leave( $locked ); } } /** * Report the outcome of an approved change from its saved record, without running anything: its result when it * completed as expected, or the state read back from its saved evidence when that matches; otherwise an error. * * @param array $prepared The prepared change. * @param string $marker Its marker. * @param array $principal Who runs it. * @return array|\WP_Error The result, with how it was checked; or an error. */ public static function reconcile( array $prepared, string $marker, array $principal ) { $ok = Reviewed_Actions::permission( $principal ); if ( is_wp_error( $ok ) ) { return $ok; } $record = self::record( $marker ); if ( ! $record || ! self::bound( $record, $prepared, $principal, $marker ) ) { return self::error( 'identity', 'No exact owner execution evidence exists for this actor, site and proposal.' ); } if ( 'complete' === $record['status'] && Reviewed_Actions::matches( $prepared['proposed']['expected_after'], $record['result'] ) ) { return array( 'state' => $record['result'], 'verification' => 'The durable owner receipt confirms the exact saved result observed at completion. This is historical evidence; subsequent native edits do not rewrite it. No mutation was repeated.', 'editor_url' => Admin::url(), ); } if ( true === ( $record['owner_returned'] ?? false ) ) { try { $observed = Reviewed_Owner::observe( $prepared, $record['evidence'] ); if ( Reviewed_Actions::matches( $prepared['proposed']['expected_after'], $observed ) ) { return array( 'state' => $observed, 'verification' => 'The exact owner marker and recorded native evidence match the authoritative saved result. Reconciliation only read that evidence and current owner state.', 'editor_url' => Admin::url(), ); } } catch ( \Throwable $ignored ) { // Evidence that cannot be read back is reported as uncertain below. } } // Partial/unknown records are observations only. Never call apply(), retry(), work() or a network writer here. return self::error( 'uncertain', 'This marker has no completed authoritative owner receipt. Inspect native media, job and provider state before a newly reviewed recovery; reconciliation changed nothing.' ); } /** * Internal guard shims preserve each native API call and its arguments; they are never action inputs. * * @param mixed ...$args update_option()'s arguments. * @return mixed What update_option() returns. */ public static function update_option( ...$args ) { self::guard(); return \update_option( ...$args ); } /** * Call add_option() after checking a running approved change. * * @param mixed ...$args add_option()'s arguments. * @return mixed What add_option() returns. */ public static function add_option( ...$args ) { self::guard(); return \add_option( ...$args ); } /** * Call delete_option() after checking a running approved change. * * @param mixed ...$args delete_option()'s arguments. * @return mixed What delete_option() returns. */ public static function delete_option( ...$args ) { self::guard(); return \delete_option( ...$args ); } /** * Call update_post_meta() after checking a running approved change. * * @param mixed ...$args update_post_meta()'s arguments. * @return mixed What update_post_meta() returns. */ public static function update_post_meta( ...$args ) { self::guard(); return \update_post_meta( ...$args ); } /** * Call add_post_meta() after checking a running approved change. * * @param mixed ...$args add_post_meta()'s arguments. * @return mixed What add_post_meta() returns. */ public static function add_post_meta( ...$args ) { self::guard(); return \add_post_meta( ...$args ); } /** * Call delete_post_meta() after checking a running approved change. * * @param mixed ...$args delete_post_meta()'s arguments. * @return mixed What delete_post_meta() returns. */ public static function delete_post_meta( ...$args ) { self::guard(); return \delete_post_meta( ...$args ); } /** * Call wp_update_post() after checking a running approved change. * * @param mixed ...$args wp_update_post()'s arguments. * @return mixed What wp_update_post() returns. */ public static function wp_update_post( ...$args ) { self::guard(); return \wp_update_post( ...$args ); } /** * Call wp_insert_attachment() after checking a running approved change. * * @param mixed ...$args wp_insert_attachment()'s arguments. * @return mixed What wp_insert_attachment() returns. */ public static function wp_insert_attachment( ...$args ) { self::guard(); return \wp_insert_attachment( ...$args ); } /** * Call wp_delete_attachment() after checking a running approved change. * * @param mixed ...$args wp_delete_attachment()'s arguments. * @return mixed What wp_delete_attachment() returns. */ public static function wp_delete_attachment( ...$args ) { self::guard(); return \wp_delete_attachment( ...$args ); } /** * Call wp_set_object_terms() after checking a running approved change. * * @param mixed ...$args wp_set_object_terms()'s arguments. * @return mixed What wp_set_object_terms() returns. */ public static function wp_set_object_terms( ...$args ) { self::guard(); return \wp_set_object_terms( ...$args ); } /** * Call wp_remove_object_terms() after checking a running approved change. * * @param mixed ...$args wp_remove_object_terms()'s arguments. * @return mixed What wp_remove_object_terms() returns. */ public static function wp_remove_object_terms( ...$args ) { self::guard(); return \wp_remove_object_terms( ...$args ); } /** * Call wp_insert_term() after checking a running approved change. * * @param mixed ...$args wp_insert_term()'s arguments. * @return mixed What wp_insert_term() returns. */ public static function wp_insert_term( ...$args ) { self::guard(); return \wp_insert_term( ...$args ); } /** * Call wp_update_term() after checking a running approved change. * * @param mixed ...$args wp_update_term()'s arguments. * @return mixed What wp_update_term() returns. */ public static function wp_update_term( ...$args ) { self::guard(); return \wp_update_term( ...$args ); } /** * Call wp_delete_term() after checking a running approved change. * * @param mixed ...$args wp_delete_term()'s arguments. * @return mixed What wp_delete_term() returns. */ public static function wp_delete_term( ...$args ) { self::guard(); return \wp_delete_term( ...$args ); } /** * Call update_term_meta() after checking a running approved change. * * @param mixed ...$args update_term_meta()'s arguments. * @return mixed What update_term_meta() returns. */ public static function update_term_meta( ...$args ) { self::guard(); return \update_term_meta( ...$args ); } /** * Call delete_term_meta() after checking a running approved change. * * @param mixed ...$args delete_term_meta()'s arguments. * @return mixed What delete_term_meta() returns. */ public static function delete_term_meta( ...$args ) { self::guard(); return \delete_term_meta( ...$args ); } /** * Call wp_update_attachment_metadata() after checking a running approved change. * * @param mixed ...$args wp_update_attachment_metadata()'s arguments. * @return mixed What wp_update_attachment_metadata() returns. */ public static function wp_update_attachment_metadata( ...$args ) { self::guard(); return \wp_update_attachment_metadata( ...$args ); } /** * Call wp_generate_attachment_metadata() after checking a running approved change. * * @param mixed ...$args wp_generate_attachment_metadata()'s arguments. * @return mixed What wp_generate_attachment_metadata() returns. */ public static function wp_generate_attachment_metadata( ...$args ) { self::guard(); return \wp_generate_attachment_metadata( ...$args ); } /** * Call set_transient() after checking a running approved change. * * @param mixed ...$args set_transient()'s arguments. * @return mixed What set_transient() returns. */ public static function set_transient( ...$args ) { self::guard(); return \set_transient( ...$args ); } /** * Call delete_transient() after checking a running approved change. * * @param mixed ...$args delete_transient()'s arguments. * @return mixed What delete_transient() returns. */ public static function delete_transient( ...$args ) { self::guard(); return \delete_transient( ...$args ); } /** * Call wp_schedule_event() after checking a running approved change. * * @param mixed ...$args wp_schedule_event()'s arguments. * @return mixed What wp_schedule_event() returns. */ public static function wp_schedule_event( ...$args ) { self::guard(); return \wp_schedule_event( ...$args ); } /** * Call wp_schedule_single_event() after checking a running approved change. * * @param mixed ...$args wp_schedule_single_event()'s arguments. * @return mixed What wp_schedule_single_event() returns. */ public static function wp_schedule_single_event( ...$args ) { self::guard(); return \wp_schedule_single_event( ...$args ); } /** * Call wp_clear_scheduled_hook() after checking a running approved change. * * @param mixed ...$args wp_clear_scheduled_hook()'s arguments. * @return mixed What wp_clear_scheduled_hook() returns. */ public static function wp_clear_scheduled_hook( ...$args ) { self::guard(); return \wp_clear_scheduled_hook( ...$args ); } /** * Call wp_unschedule_event() after checking a running approved change. * * @param mixed ...$args wp_unschedule_event()'s arguments. * @return mixed What wp_unschedule_event() returns. */ public static function wp_unschedule_event( ...$args ) { self::guard(); return \wp_unschedule_event( ...$args ); } /** * Call wp_remote_post() after checking a running approved change. * * @param mixed ...$args wp_remote_post()'s arguments. * @return mixed What wp_remote_post() returns. */ public static function wp_remote_post( ...$args ) { self::guard(); return \wp_remote_post( ...$args ); } /** * Call wp_remote_get() after checking a running approved change. * * @param mixed ...$args wp_remote_get()'s arguments. * @return mixed What wp_remote_get() returns. */ public static function wp_remote_get( ...$args ) { self::guard(); return \wp_remote_get( ...$args ); } /** * Call wp_remote_request() after checking a running approved change. * * @param mixed ...$args wp_remote_request()'s arguments. * @return mixed What wp_remote_request() returns. */ public static function wp_remote_request( ...$args ) { self::guard(); return \wp_remote_request( ...$args ); } /** * Call copy() after checking a running approved change. * * @param string $from The file. * @param string $to Where it goes. * @param resource ...$context A stream context, when one is given. * @return bool What copy() returns. */ public static function copy( $from, $to, ...$context ) { self::guard(); return \copy( $from, $to, ...$context ); } /** * Call rename() after checking a running approved change. * * @param string $from The file. * @param string $to Where it goes. * @param resource ...$context A stream context, when one is given. * @return bool What rename() returns. */ public static function rename( $from, $to, ...$context ) { self::guard(); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- guarded pass-through shim that must keep native rename() semantics and return value for existing callers on plugin-private files return \rename( $from, $to, ...$context ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- guarded pass-through shim that must keep native rename() semantics and return value for existing callers on plugin-private files /** * Call unlink() after checking a running approved change. * * @param mixed ...$args unlink()'s arguments. * @return mixed What unlink() returns. */ public static function unlink( ...$args ) { self::guard(); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- guarded pass-through shim that must keep native unlink() return value for existing callers; a separate wp_delete_file() shim exists return \unlink( ...$args ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- guarded pass-through shim that must keep native unlink() return value for existing callers; a separate wp_delete_file() shim exists /** * Call file_put_contents() after checking a running approved change. * * @param mixed ...$args file_put_contents()'s arguments. * @return mixed What file_put_contents() returns. */ public static function file_put_contents( ...$args ) { self::guard(); return \file_put_contents( ...$args ); } /** * Call fwrite() after checking a running approved change. * * @param mixed ...$args fwrite()'s arguments. * @return mixed What fwrite() returns. */ public static function fwrite( ...$args ) { self::guard(); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- guarded pass-through shim for already-open stream handles (encoder pipes, backup lists); WP_Filesystem has no stream API return \fwrite( ...$args ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- guarded pass-through shim for already-open stream handles (encoder pipes, backup lists); WP_Filesystem has no stream API /** * Call ftruncate() after checking a running approved change. * * @param mixed ...$args ftruncate()'s arguments. * @return mixed What ftruncate() returns. */ public static function ftruncate( ...$args ) { self::guard(); return \ftruncate( ...$args ); } /** * Call wp_delete_file() after checking a running approved change. * * @param mixed ...$args wp_delete_file()'s arguments. * @return mixed What wp_delete_file() returns. */ public static function wp_delete_file( ...$args ) { self::guard(); return \wp_delete_file( ...$args ); } /** * Call $wpdb->query() after checking a running approved change. * * @param mixed ...$args $wpdb->query()'s arguments. * @return int|bool What $wpdb->query() returns. */ public static function database_query( ...$args ) { self::guard(); global $wpdb; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- guarded pass-through; every caller passes a literal statement or a $wpdb->prepare() result return $wpdb->query( ...$args ); } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- guarded pass-through; every caller passes a literal statement or a $wpdb->prepare() result /** * Call $wpdb->insert() after checking a running approved change. * * @param mixed ...$args $wpdb->insert()'s arguments. * @return int|bool What $wpdb->insert() returns. */ public static function database_insert( ...$args ) { self::guard(); global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- guarded pass-through to the $wpdb insert/update/delete helpers for this plugin's custom tables return $wpdb->insert( ...$args ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- guarded pass-through to the $wpdb insert/update/delete helpers for this plugin's custom tables /** * Call $wpdb->update() after checking a running approved change. * * @param mixed ...$args $wpdb->update()'s arguments. * @return int|bool What $wpdb->update() returns. */ public static function database_update( ...$args ) { self::guard(); global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- guarded pass-through to the $wpdb insert/update/delete helpers for this plugin's custom tables return $wpdb->update( ...$args ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- guarded pass-through to the $wpdb insert/update/delete helpers for this plugin's custom tables /** * Call $wpdb->delete() after checking a running approved change. * * @param mixed ...$args $wpdb->delete()'s arguments. * @return int|bool What $wpdb->delete() returns. */ public static function database_delete( ...$args ) { self::guard(); global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- guarded pass-through to the $wpdb insert/update/delete helpers for this plugin's custom tables return $wpdb->delete( ...$args ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- guarded pass-through to the $wpdb insert/update/delete helpers for this plugin's custom tables }