unt( $entry ) && array_key_exists( 'service', $entry ) && array_key_exists( 'since', $entry ) && is_string( $entry['service'] ) && is_int( $entry['since'] ); } private static function invalidate_option_cache( $option_name ) { if ( ! function_exists( 'wp_cache_delete' ) ) { return; } wp_cache_delete( $option_name, 'options' ); wp_cache_delete( 'alloptions', 'options' ); wp_cache_delete( 'notoptions', 'options' ); } private static function known_project_id( array $history, $key ) { if ( ! isset( $history[ $key ]['project_id'] ) ) { return '?'; } $project_id = self::normalize_project_id( $history[ $key ]['project_id'] ); return $project_id ? $project_id : '?'; } private static function normalize_project_id( $project_id ) { if ( ! is_int( $project_id ) && ! is_string( $project_id ) ) { return ''; } $project_id = trim( (string) $project_id ); return '' !== $project_id && '0' !== $project_id ? $project_id : ''; } private static function has_access_key( $access_key ) { return is_string( $access_key ) && '' !== trim( $access_key ); } private static function is_malformed_access_key( $access_key ) { return null !== $access_key && ! is_string( $access_key ); } private static function get_stored_project( $service ) { global $sitepress; if ( ! is_object( $service ) || ! is_object( $sitepress ) || ! method_exists( $sitepress, 'get_setting' ) || ! class_exists( 'TranslationProxy_Project' ) ) { return null; } $projects = $sitepress->get_setting( 'icl_translation_projects' ); if ( ! is_array( $projects ) ) { return null; } $service_for_index = clone $service; $project_index = TranslationProxy_Project::generate_service_index( $service_for_index ); if ( ! is_string( $project_index ) || '' === $project_index || ! array_key_exists( $project_index, $projects ) ) { return null; } return $projects[ $project_index ]; } private static function has_partial_project_credentials( $project ) { if ( null === $project ) { return false; } if ( ! is_array( $project ) ) { return true; } if ( array() === $project ) { return false; } if ( ! array_key_exists( 'id', $project ) ) { return true; } return array( 'id' ) !== array_keys( $project ) || ! is_string( $project['id'] ) || '' !== $project['id']; } private static function stored_project_has_credentials( $project, $expected_project_id ) { return is_array( $project ) && isset( $project['id'], $project['access_key'] ) && self::normalize_project_id( $project['id'] ) === self::normalize_project_id( $expected_project_id ) && self::has_access_key( $project['access_key'] ); } private static function block( $service, $known_project_id, $reason ) { $service_id = self::service_key( $service ); $project_id = self::normalize_project_id( $known_project_id ); $state = array( 'project_id' => $project_id ? $project_id : '?', 'service_id' => $service_id, 'reason' => $reason, 'time' => time(), ); $status = self::persist_blocked_state( $state ); if ( 'unreadable' === $status ) { self::surface_unreadable_blocked_state(); return; } if ( 'changed' === $status ) { error_log( 'automatic_project_creation' === $reason ? sprintf( 'WPML: BLOCKED automatic Translation Proxy project creation. This site previously used TP project "%s" for service "%s"; reconnect the service explicitly before creating another project.', $known_project_id, $service_id ? $service_id : '?' ) : ( 'backfill' === $reason ? sprintf( 'WPML: Current Translation Proxy credentials do not match the independently recorded project identity for service "%s" (previous project "%s"). No send or project creation was attempted; reconnect the service explicitly before continuing.', $service_id ? $service_id : '?', $known_project_id ) : sprintf( 'WPML: BLOCKED a Translation Proxy send with missing or mismatched project credentials. This site previously used TP project "%s"; sending now could silently create a new, empty project or target the wrong project. Restore the translation service configuration (or reconnect the service) before sending.', $known_project_id ) ) ); } add_action( 'admin_notices', [ self::class, 'render_blocked_notice' ] ); } private static function persist_blocked_state( array $state ) { global $wpdb; if ( ! self::can_compare_and_swap( $wpdb ) ) { $missing = new stdClass(); try { $existing = get_option( self::BLOCKED_OPTION, $missing ); } catch ( Throwable $e ) { return 'unreadable'; } if ( $missing !== $existing ) { $states = self::decode_blocked_states( $existing ); if ( null === $states ) { return 'unreadable'; } if ( self::blocked_states_contain_identity( $states, $state ) ) { return 'same'; } $states[] = $state; return update_option( self::BLOCKED_OPTION, self::encode_blocked_states( $states ), false ) ? 'changed' : 'unreadable'; } return update_option( self::BLOCKED_OPTION, $state, false ) ? 'changed' : 'unreadable'; } for ( $attempt = 0; $attempt < self::CAS_ATTEMPTS; $attempt++ ) { $wpdb->last_error = ''; $raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", self::BLOCKED_OPTION ) ); if ( $wpdb->last_error ) { return 'unreadable'; } $exists = null !== $raw; if ( $exists ) { try { $value = self::unserialize_safety_state( $raw ); } catch ( Throwable $e ) { return 'unreadable'; } $states = self::decode_blocked_states( $value ); if ( null === $states ) { return 'unreadable'; } if ( self::blocked_states_contain_identity( $states, $state ) ) { return 'same'; } $states[] = $state; } else { $states = array( $state ); } $serialized = serialize( self::encode_blocked_states( $states ) ); if ( $exists ) { $written = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->options} SET option_value = %s, autoload = 'no' WHERE option_name = %s AND BINARY option_value = BINARY %s", $serialized, self::BLOCKED_OPTION, $raw ) ); } else { $written = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')", self::BLOCKED_OPTION, $serialized ) ); } if ( false === $written ) { return 'unreadable'; } self::invalidate_option_cache( self::BLOCKED_OPTION ); if ( 1 === (int) $written ) { return 'changed'; } } $record = self::read_blocked_state_record(); return 'valid' === $record['status'] && self::blocked_states_contain_identity( $record['states'], $state ) ? 'same' : 'unreadable'; } private static function blocked_states_contain_identity( array $states, array $needle ) { $identity = self::blocked_state_identity( $needle ); foreach ( $states as $state ) { if ( self::blocked_state_identity( $state ) === $identity ) { return true; } } return false; } private static function blocked_state_identity( array $state ) { return serialize( array( $state['service_id'], $state['project_id'] ) ); } private static function surface_unreadable_blocked_state() { error_log( 'WPML: Translation Proxy operations remain blocked because the independent safety record is unreadable. The original value was preserved; reconnect the service or contact WPML support.' ); add_action( 'admin_notices', [ self::class, 'render_unreadable_blocked_notice' ] ); } }