connecting ) { $this->construction_failed = true; return false; } $this->connecting = true; $this->construction_failed = false; $this->construction_handle = null; $this->construction_thread = null; try { // Core's allow_bail path may load db-error.php and call die directly. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Keep optional private database connection failure local; return the native failure for caller checks instead of exposing connection warnings or rendering core fatal output. $connected = @parent::db_connect( false ); if ( $connected && ! $this->construction_failed && $this->construction_current() && $this->ready ) { // @phpstan-ignore booleanNot.alwaysTrue (Core connection setup calls this adapter and its callbacks, which can mark construction_failed before the parent call returns.) return true; } // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Optional worker failure already selects unavailable; continue fixed private-handle cleanup without adopting or closing a replacement. } catch ( \Throwable $error ) { // Optional connection errors are local; no borrowed handle is owned here. } finally { $this->connecting = false; } $this->ready = false; if ( $this->construction_handle instanceof \mysqli ) { // @phpstan-ignore instanceof.alwaysFalse (The parent connection flow calls set_sql_mode, which captures the owned handle; retain cleanup after a later refusal.) try { $this->construction_handle->close(); // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Optional worker failure already selects unavailable; continue fixed private-handle cleanup without adopting or closing a replacement. } catch ( \Throwable $error ) { // An already-closed original handle is still a refused connection. } } remove_filter( 'query', array( $this, 'remove_placeholder_escape' ), 0 ); $this->dbh = null; return false; } /** * Close this private adapter and detach only its core query callback. * * @return bool Whether the parent closed its native connection. */ public function close() { try { return parent::close(); } finally { remove_filter( 'query', array( $this, 'remove_placeholder_escape' ), 0 ); } } /** Check only captured native identities; this check invokes no callbacks. */ private function construction_current(): bool { try { return $this->construction_handle instanceof \mysqli && $this->dbh === $this->construction_handle && $this->construction_thread === $this->construction_handle->thread_id; } catch ( \Throwable $error ) { // @phpstan-ignore catch.neverThrown (Reading thread_id on a closed mysqli throws Error on PHP 8.3 and 8.5; this catch preserves local refusal.) return false; } } /** * Keep core's SQL-mode normalization while binding its callback to the original handle. * * @param array $modes Optional native mode list, as in core wpdb. * @return void * * @throws \RuntimeException When the private connection changes or its SQL modes cannot be read or normalized. */ public function set_sql_mode( $modes = array() ) { $handle = $this->dbh; if ( $this->setting_modes || ! $handle instanceof \mysqli ) { $this->construction_failed = true; throw new \RuntimeException( 'Optional connection setup changed.' ); } $thread = $handle->thread_id; if ( $this->connecting ) { $this->construction_handle = $handle; $this->construction_thread = $thread; } $this->setting_modes = true; try { if ( empty( $modes ) ) { $result = $handle->query( 'SELECT @@SESSION.sql_mode' ); if ( ! $result instanceof \mysqli_result ) { throw new \RuntimeException( 'Optional connection modes are unavailable.' ); } try { $row = $result->fetch_row(); } finally { $result->free(); } if ( empty( $row[0] ) ) { return; } $modes = explode( ',', $row[0] ); } $modes = array_change_key_case( $modes, CASE_UPPER ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Preserve core's incompatible_sql_modes contract while keeping the private connection identity pinned around the callback. $incompatible = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes ); // The callback may replace/close dbh. Never execute core's following SET on it. if ( $this->construction_failed || $handle !== $this->dbh || $thread !== $handle->thread_id ) { throw new \RuntimeException( 'Optional connection setup changed.' ); } foreach ( $modes as $index => $mode ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- Mirror core wpdb SQL-mode normalization, including the native incompatible_sql_modes filter comparison contract; connection identity is rechecked after that callback. if ( in_array( $mode, $incompatible ) ) { unset( $modes[ $index ] ); } } $normalized = self::sql_modes_value( $modes ); $sql = $this->prepare( 'SET SESSION sql_mode=%s', $normalized ); if ( $this->construction_failed || $handle !== $this->dbh || $thread !== $handle->thread_id ) { throw new \RuntimeException( 'Optional connection setup changed.' ); } if ( true !== $handle->query( $this->remove_placeholder_escape( $sql ) ) ) { throw new \RuntimeException( 'Optional connection modes are unavailable.' ); } } finally { $this->setting_modes = false; } } /** * Bound native SQL-mode names before placing their fixed grammar in SET. * * No WordPress prepare/query callbacks may run between the native handle check * and this session setup. Mode names are identifiers, never general SQL text. * * @param array $modes Individual mode names from core or the native session. * @return string Comma-separated ASCII mode names, or empty for no modes. * @throws \RuntimeException When a mode is not a plain supported token. */ private static function sql_modes_value( array $modes ): string { foreach ( $modes as $mode ) { if ( ! is_string( $mode ) || 1 !== preg_match( '/\A[A-Za-z0-9_]{1,64}\z/D', $mode ) ) { throw new \RuntimeException( 'Optional connection modes are invalid.' ); } } return implode( ',', $modes ); } /** * Select the worker database, marking failure without terminating the caller. * * @param string $db Database name supplied by the connection factory. * @param \mysqli|null $dbh Optional native handle; defaults to this connection. * @return void */ public function select( $db, $dbh = null ) { $handle = $dbh ?? $this->dbh; if ( $this->connecting && ( $this->construction_failed || ! $this->construction_current() || $handle !== $this->construction_handle ) ) { $this->ready = false; return; } if ( ! $handle instanceof \mysqli ) { $this->ready = false; return; } try { // Core selects the database; this adapter's bail() keeps failure local. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- An unavailable optional database must not expose native connection warnings. @parent::select( $db, $handle ); } catch ( \Throwable $error ) { $this->ready = false; } } /** * Keep optional-worker failure local instead of rendering a fatal error page. * * @param string $message Parent-compatible diagnostic; never rendered here. * @param string $error_code Parent-compatible status; never sent to the caller. * @return false */ public function bail( $message, $error_code = '500' ) { $this->ready = false; return false; } }