dbh) for established mysql connections * @var array */ public $dbhs; /** * The multi-dimensional array of datasets and servers * @var array */ public $hyper_servers = array(); /** * Optional directory of tables and their datasets * @var array */ public $hyper_tables = array(); /** * Optional directory of callbacks to determine datasets from queries * @var array */ public $hyper_callbacks = array(); /** * Custom callback to save debug info in $this->queries * @var callable */ public $save_query_callback = null; /** * Whether to use persistent connections * @var bool */ public $persistent = false; /** * The maximum number of db links to keep open. The least-recently used * link will be closed when the number of links exceeds this. * @var int */ public $max_connections = 10; /** * Whether to check with fsockopen prior to connecting to mysql. * @var bool */ public $check_tcp_responsiveness = true; /** * Minimum number of connections to try before bailing * @var int */ public $min_tries = 3; /** * Send Reads To Masters. This disables slave connections while true. * Otherwise it is an array of written tables. * @var array */ public $srtm = array(); /** * The log of db connections made and the time each one took * @var array */ public $db_connections; /** * The list of unclosed connections sorted by LRU */ public $open_connections = array(); /** * The last server used and the database name selected * @var array */ public $last_used_server; /** * Lookup array (dbhname => (server, db name) ) for re-selecting the db * when a link is re-used. * @var array */ public $used_servers = array(); /** * Lookup array (dbhname => (group, key)) prioritized for connections * @var array */ public $unused_servers = array(); /** * Lookup array (dbhname => int) of the number of unique servers in the config * @var array */ public $unique_servers = array(); /** * Whether to save debug_backtrace in save_query_callback. You may wish * to disable this, e.g. when tracing out-of-memory problems. */ public $save_backtrace = true; /** * Maximum lag in seconds. Set null to disable. Requires callbacks. * @var integer */ public $default_lag_threshold = null; /** * Lookup array (dbhname => host:port) * @var array */ public $dbh2host = array(); /** * Keeps track of the dbhname usage and errors. */ public $dbhname_heartbeats = array(); /** * Counter for how many queries have failed during the life of the $wpdb object */ public $num_failed_queries = 0; /** * Gets ready to make database connections * @param array db class vars */ public function __construct( $args = null ) { if ( is_array( $args ) ) { foreach ( get_class_vars( __CLASS__ ) as $var => $value ) { if ( isset( $args[ $var ] ) ) { $this->$var = $args[ $var ]; } } } $this->use_mysqli = $this->should_use_mysqli(); $this->init_charset(); } /** * Add the connection parameters for a database */ public function add_database( $db ) { extract( $db, EXTR_SKIP ); if ( ! isset( $dataset ) ) { $dataset = 'global'; } if ( ! isset( $read ) ) { $read = 1; } if ( ! isset( $write ) ) { $write = 1; } unset( $db['dataset'] ); if ( $read ) { $this->hyper_servers[ $dataset ]['read'][ $read ][] = $db; } if ( $write ) { $this->hyper_servers[ $dataset ]['write'][ $write ][] = $db; } } /** * Specify the dateset where a table is found */ public function add_table( $dataset, $table ) { $this->hyper_tables[ $table ] = $dataset; } /** * Add a callback to a group of callbacks. * The default group is 'dataset', used to examine * queries and determine dataset. */ public function add_callback( $callback, $group = 'dataset' ) { $this->hyper_callbacks[ $group ][] = $callback; } /** * Find the table to be used for query routing. Falls back on * core get_table_from_query after checking for special cases. * @param string query * @return string table */ public function get_table_from_query( $q ) { // Remove characters that can legally trail the table name $q = rtrim( $q, ';/-#' ); // allow (select...) union [...] style queries. Use the first queries table name. $q = ltrim( $q, "\t (" ); // Strip everything between parentheses except nested // selects and use only 1500 chars of the query $q = preg_replace( '/\((?!\s*select)[^(]*?\)/is', '()', substr( $q, 0, 1500 ) ); // SELECT FOUND_ROWS() refers to the previous SELECT query if ( preg_match( '/^\s*SELECT.*?\s+FOUND_ROWS\(\)/is', $q ) ) { return $this->last_table; } // SELECT FROM information_schema.* WHERE TABLE_NAME = 'wp_12345_foo' if ( preg_match('/^\s*' . 'SELECT.*?\s+FROM\s+`?information_schema`?\.' . '.*\s+TABLE_NAME\s*=\s*["\']([\w-]+)["\']/is', $q, $maybe) ) { return $maybe[1]; } // Transaction support, requires a table hint via comment: IN_TABLE=table_name if ( preg_match('/^\s*' . '(?:START\s+TRANSACTION|COMMIT|ROLLBACK)\s*\/[*]\s*IN_TABLE\s*=\s*' . "'?([\w-]+)'?/is", $q, $maybe) ) { return $maybe[1]; } $this->last_table = parent::get_table_from_query( $q ); return $this->last_table; } /** * Determine the likelihood that this query could alter anything * @param string query * @return bool */ public function is_write_query( $q ) { // Quick and dirty: only SELECT statements are considered read-only. $q = ltrim( $q, "\r\n\t (" ); return ! preg_match( '/^(?:SELECT|SHOW|DESCRIBE|DESC|EXPLAIN)\s/i', $q ); } /** * Set a flag to prevent reading from slaves which might be lagging after a write */ public function send_reads_to_masters() { $this->srtm = true; } /** * Callbacks are executed in the order in which they are registered until one * of them returns something other than null. */ public function run_callbacks( $group, $args = null ) { if ( ! isset( $this->hyper_callbacks[ $group ] ) || ! is_array( $this->hyper_callbacks[ $group ] ) ) { return null; } if ( ! isset( $args ) ) { $args = array( $this ); } elseif ( is_array( $args ) ) { $args[] = $this; } else { $args = array( $args, $this ); } foreach ( $this->hyper_callbacks[ $group ] as $func ) { $result = call_user_func_array( $func, $args ); if ( isset( $result ) ) { return $result; } } } /** * Figure out which database server should handle the query, and connect to it. * @param string query * @return resource mysql database connection */ public function db_connect( $query = '' ) { if ( empty( $query ) ) { return false; } $this->table = $this->get_table_from_query( $query ); if ( isset( $this->hyper_tables[ $this->table ] ) ) { $dataset = $this->hyper_tables[ $this->table ]; $this->callback_result = null; } else { $this->callback_result = $this->run_callbacks( 'dataset', $query ); if ( null !== $this->callback_result ) { if ( is_array( $this->callback_result ) ) { extract( $this->callback_result, EXTR_OVERWRITE ); } else { $dataset = $this->callback_result; } } } if ( ! isset( $dataset ) ) { $dataset = 'global'; } if ( ! $dataset ) { return $this->log_and_bail( "Unable to determine dataset (for table: $this->table)" ); } else { $this->dataset = $dataset; } $this->run_callbacks( 'dataset_found', $dataset ); if ( empty( $this->hyper_servers ) ) { if ( $this->is_mysql_connection( $this->dbh ) ) { return $this->dbh; } if ( ! defined( 'DB_HOST' ) || ! defined( 'DB_USER' ) || ! defined( 'DB_PASSWORD' ) || ! defined( 'DB_NAME' ) ) { return $this->log_and_bail( 'We were unable to query because there was no database defined' ); } $this->dbh = $this->ex_mysql_connect( DB_HOST, DB_USER, DB_PASSWORD, $this->persistent ); if ( ! $this->is_mysql_connection( $this->dbh ) ) { return $this->log_and_bail( 'We were unable to connect to the database. (DB_HOST)' ); } if ( ! $this->ex_mysql_select_db( DB_NAME, $this->dbh ) ) { return $this->log_and_bail( 'We were unable to select the database' ); } if ( ! empty( $this->charset ) ) { $collation_query = "SET NAMES '$this->charset'"; if ( ! empty( $this->collate ) ) { $collation_query .= " COLLATE '$this->collate'"; } $this->ex_mysql_query( $collation_query, $this->dbh ); } return $this->dbh; } // Determine whether the query must be sent to the master (a writable server) if ( ! empty( $use_master ) || true === $this->srtm || isset( $this->srtm[ $this->table ] ) ) { $use_master = true; } elseif ( $this->is_write_query( $query ) ) { $use_master = true; if ( is_array( $this->srtm ) ) { $this->srtm[ $this->table ] = true; } } elseif ( ! isset( $use_master ) && is_array( $this->srtm ) && ! empty( $this->srtm ) ) { // Detect queries that have a join in the srtm array. $use_master = false; $query_match = substr( $query, 0, 1000 ); foreach ( $this->srtm as $key => $value ) { if ( false !== stripos( $query_match, $key ) ) { $use_master = true; break; } } } else { $use_master = false; } if ( $use_master ) { $dbhname = $dataset . '__w'; $this->dbhname = $dbhname; $operation = 'write'; } else { $dbhname = $dataset . '__r'; $this->dbhname = $dbhname; $operation = 'read'; } // Try to reuse an existing connection while ( isset( $this->dbhs[ $dbhname ] ) && $this->is_mysql_connection( $this->dbhs[ $dbhname ] ) ) { // Find the connection for incrementing counters foreach ( array_keys( $this->db_connections ) as $i ) { if ( $this->db_connections[ $i ]['dbhname'] == $dbhname ) { $conn =& $this->db_connections[ $i ]; } } if ( isset( $server['name'] ) ) { $name = $server['name']; // A callback has specified a database name so it's possible the existing connection selected a different one. if ( $name != $this->used_servers[ $dbhname ]['name'] ) { if ( ! $this->ex_mysql_select_db( $name, $this->dbhs[ $dbhname ] ) ) { // this can happen when the user varies and lacks permission on the $name database if ( isset( $conn['disconnect (select failed)'] ) ) { ++$conn['disconnect (select failed)']; } else { $conn['disconnect (select failed)'] = 1; } $this->disconnect( $dbhname ); break; } $this->used_servers[ $dbhname ]['name'] = $name; } } else { $name = $this->used_servers[ $dbhname ]['name']; } $this->current_host = $this->dbh2host[ $dbhname ]; // Keep this connection at the top of the stack to prevent disconnecting frequently-used connections $k = array_search( $dbhname, $this->open_connections ); if ( $k ) { unset( $this->open_connections[ $k ] ); $this->open_connections[] = $dbhname; } $this->last_used_server = $this->used_servers[ $dbhname ]; $this->last_connection = compact( 'dbhname', 'name' ); if ( $this->should_mysql_ping() && ! $this->ex_mysql_ping( $this->dbhs[ $dbhname ] ) ) { if ( isset( $conn['disconnect (ping failed)'] ) ) { ++$conn['disconnect (ping failed)']; } else { $conn['disconnect (ping failed)'] = 1; } $this->disconnect( $dbhname ); break; } if ( isset( $conn['queries'] ) ) { ++$conn['queries']; } else { $conn['queries'] = 1; } return $this->dbhs[ $dbhname ]; } if ( $use_master && defined( 'MASTER_DB_DEAD' ) ) { return $this->bail( "We're updating the database, please try back in 5 minutes. If you are posting to your blog please hit the refresh button on your browser in a few minutes to post the data again. It will be posted as soon as the database is back online again." ); } if ( empty( $this->hyper_servers[ $dataset ][ $operation ] ) ) { return $this->log_and_bail( "No databases available with $this->table ($dataset)" ); } // Make a list of at least $this->min_tries connections to try, repeating as necessary. if ( ! isset( $this->unused_servers[ $dbhname ] ) ) { // Put the groups in order by priority ksort( $this->hyper_servers[ $dataset ][ $operation ] ); $servers = array(); foreach ( $this->hyper_servers[ $dataset ][ $operation ] as $group => $items ) { $keys = array_keys( $items ); shuffle( $keys ); foreach ( $keys as $key ) { $servers[] = compact( 'group', 'key' ); } } $this->unique_servers[ $dbhname ] = count( $servers ); if ( 0 === $this->unique_servers[ $dbhname ] ) { return $this->log_and_bail( "No database servers were found to match the query ($this->table, $dataset)" ); } $this->unused_servers[ $dbhname ] = $servers; // Repeat the server list until there are enough for retries (unless master has fallbacks) if ( ! ( $use_master && $this->unique_servers[ $dbhname ] > 1 ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found while ( count( $this->unused_servers[ $dbhname ] ) < $this->min_tries ) { $this->unused_servers[ $dbhname ] = array_merge( $this->unused_servers[ $dbhname ], $servers ); } } } // Connect to a database server do { $unique_lagged_slaves = array(); $success = false; // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition while ( ( $group_key = array_shift( $this->unused_servers[ $dbhname ] ) ) ) { $tries_remaining = count( $this->unused_servers[ $dbhname ] ); // If all servers are lagged, we need to start ignoring the lag and retry if ( count( $unique_lagged_slaves ) == $this->unique_servers[ $dbhname ] ) { break; } // Variables: $group, $key extract( $group_key, EXTR_OVERWRITE ); // Variables: $host, $user, $password, $name, $read, $write [, $lag_threshold, $timeout ] extract( $this->hyper_servers[ $dataset ][ $operation ][ $group ][ $key ], EXTR_OVERWRITE ); $port = null; // Split host:port into $host and $port if ( strpos( $host, ':' ) ) { list($host, $port) = explode( ':', $host ); } // Overlay $server if it was extracted from a callback if ( isset( $server ) && is_array( $server ) ) { extract( $server, EXTR_OVERWRITE ); } // Split again in case $server had host:port if ( strpos( $host, ':' ) ) { list($host, $port) = explode( ':', $host ); } // Make sure there's always a port number if ( empty( $port ) ) { $port = 3306; } // Use a default timeout of 200ms if ( ! isset( $timeout ) ) { $timeout = 0.2; } // Get the minimum group here, in case $server rewrites it if ( ! isset( $min_group ) || $min_group > $group ) { $min_group = $group; } // Skip master if read-only and fallback masters are configured if ( $use_master && $this->unique_servers[ $dbhname ] > 1 ) { // Load server state from acpu, bypassing check_tcp_responsiveness() if ( $this->server_marked_read_only( $host, $port, $dbhname ) ) { continue; } } // Can be used by the lag callbacks $this->lag_cache_key = "$host:$port"; $this->lag_threshold = isset( $lag_threshold ) ? $lag_threshold : $this->default_lag_threshold; // Check for a lagged slave, if applicable if ( ! $use_master && ! $write && ! isset( $ignore_slave_lag ) && isset( $this->lag_threshold ) && ! isset( $server['host'] ) // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure && ( $lagged_status = $this->get_lag_cache() ) === HYPERDB_LAG_BEHIND ) { // If it is the last lagged slave and it is with the best preference we will ignore its lag if ( ! isset( $unique_lagged_slaves[ "$host:$port" ] ) && count( $unique_lagged_slaves ) + 1 == $this->unique_servers[ $dbhname ] && $group == $min_group ) { $this->lag_threshold = null; } else { $unique_lagged_slaves[ "$host:$port" ] = $this->lag; continue; } } $this->timer_start(); // Connect if necessary or possible $server_state = null; if ( $use_master || ! $tries_remaining || // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure 'up' === ( $server_state = $this->get_server_state( $host, $port, $dbhname, $timeout ) ) ) { $this->set_connect_timeout( 'pre_connect', $use_master, $tries_remaining ); $this->dbhs[ $dbhname ] = $this->ex_mysql_connect( "$host:$port", $user, $password, $this->persistent ); $this->set_connect_timeout( 'post_connect', $use_master, $tries_remaining ); } else { $this->dbhs[ $dbhname ] = false; } $elapsed = $this->timer_stop(); if ( $this->is_mysql_connection( $this->dbhs[ $dbhname ] ) ) { /** * If we care about lag, disconnect lagged slaves and try to find others. * We don't disconnect if it is the last lagged slave and it is with the best preference. */ if ( ! $use_master && ! $write && ! isset( $ignore_slave_lag ) && isset( $this->lag_threshold ) && ! isset( $server['host'] ) && HYPERDB_LAG_OK !== $lagged_status && ( $lagged_status = $this->get_lag() ) === HYPERDB_LAG_BEHIND // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure && ! ( ! isset( $unique_lagged_slaves[ "$host:$port" ] ) && count( $unique_lagged_slaves ) + 1 === $this->unique_servers[ $dbhname ] && $group == $min_group ) ) { $success = false; $unique_lagged_slaves[ "$host:$port" ] = $this->lag; $this->disconnect( $dbhname ); $this->dbhs[ $dbhname ] = false; $msg = "Replication lag of {$this->lag}s on $host:$port ($dbhname)"; $this->print_error( $msg ); continue; } elseif ( $this->ex_mysql_select_db( $name, $this->dbhs[ $dbhname ] ) ) { $success = true; $this->current_host = "$host:$port"; $this->dbh2host[ $dbhname ] = "$host:$port"; $queries = 1; $lag = isset( $this->lag ) ? $this->lag : 0; $this->last_connection = compact( 'dbhname', 'host', 'port', 'user', 'name', 'server_state', 'elapsed', 'success', 'queries', 'lag' ); $this->db_connections[] = $this->last_connection; $this->open_connections[] = $dbhname; break; } } if ( 'down' == $server_state ) { continue; // don't flood the logs if already down } $errno = $this->ex_mysql_errno(); if ( ( HYPERDB_CONN_HOST_ERROR == $errno || HYPERDB_CONNNECTION_ERROR == $errno ) && ( 'up' == $server_state || ! $tries_remaining ) ) { $this->mark_server_as_down( $host, $port, $dbhname ); $server_state = 'down'; } $referrer = ''; if ( ! empty( $_SERVER['HTTP_HOST'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- print_error() will sanitize the entire message $referrer .= (string) $_SERVER['HTTP_HOST']; } if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- print_error() will sanitize the entire message $referrer .= (string) $_SERVER['REQUEST_URI']; } $success = false; $this->last_connection = compact( 'dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success' ); $this->db_connections[] = $this->last_connection; $msg = date( 'Y-m-d H:i:s' ) . " Can't select $dbhname - \n"; // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $msg .= "'referrer' => '{$referrer}',\n"; $msg .= "'server' => {$server},\n"; $msg .= "'host' => {$host},\n"; $msg .= "'error' => " . $this->ex_mysql_error() . ",\n"; $msg .= "'errno' => " . $this->ex_mysql_errno() . ",\n"; $msg .= "'server_state' => $server_state\n"; $msg .= "'lagged_status' => " . ( isset( $lagged_status ) ? $lagged_status : HYPERDB_LAG_UNKNOWN ); $this->print_error( $msg ); } if ( ! $success || ! isset( $this->dbhs[ $dbhname ] ) || ! $this->is_mysql_connection( $this->dbhs[ $dbhname ] ) ) { if ( ! isset( $ignore_slave_lag ) && count( $unique_lagged_slaves ) ) { // Lagged slaves were not used. Ignore the lag for this connection attempt and retry. $ignore_slave_lag = true; continue; } $error_details = array( 'host' => $host, 'port' => $port, 'operation' => $operation, 'table' => $this->table, 'dataset' => $dataset, 'dbhname' => $dbhname, ); $this->run_callbacks( 'db_connection_error', $error_details ); return $this->bail( "Unable to connect to $host:$port to $operation table '$this->table' ($dataset)" ); } break; } while ( true ); if ( ! isset( $charset ) ) { $charset = null; } if ( ! isset( $collate ) ) { $collate = null; } $this->set_charset( $this->dbhs[ $dbhname ], $charset, $collate ); $this->dbh = $this->dbhs[ $dbhname ]; // needed by $wpdb->_real_escape() $this->last_used_server = compact( 'host', 'user', 'name', 'read', 'write' ); $this->used_servers[ $dbhname ] = $this->last_used_server; // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found while ( ! $this->persistent && count( $this->open_connections ) > $this->max_connections ) { $oldest_connection = array_shift( $this->open_connections ); if ( $this->dbhs[ $oldest_connection ] != $this->dbhs[ $dbhname ] ) { $this->disconnect( $oldest_connection ); } } return $this->dbhs[ $dbhname ]; } /** * Sets the connection's character set. * @param resource $dbh The resource given by ex_mysql_connect * @param string $charset The character set (optional) * @param string $collate The collation (optional) */ public function set_charset( $dbh, $charset = null, $collate = null ) { if ( ! isset( $charset ) ) { $charset = $this->charset; } if ( ! isset( $collate ) ) { $collate = $this->collate; } if ( ! $this->has_cap( 'collation', $dbh ) || empty( $charset ) ) { return; } if ( ! in_array( strtolower( $charset ), array( 'utf8', 'utf8mb4', 'latin1' ) ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_die( sprintf( "%s charset isn't supported in HyperDB for security reasons", htmlspecialchars( $charset ) ) ); } if ( $this->is_mysql_set_charset_callable() && $this->has_cap( 'set_charset', $dbh ) ) { $this->ex_mysql_set_charset( $charset, $dbh ); $this->real_escape = true; } else { $query = $this->prepare( 'SET NAMES %s', $charset ); if ( ! empty( $collate ) ) { $query .= $this->prepare( ' COLLATE %s', $collate ); } $this->ex_mysql_query( $query, $dbh ); } } /** * Force addslashes() for the escapes. * * HyperDB makes connections when a query is made * which is why we can't use mysql_real_escape_string() for escapes. * This is also the reason why we don't allow certain charsets. See set_charset(). */ public function _real_escape( $string ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore $escaped = addslashes( $string ); if ( method_exists( get_parent_class( $this ), 'add_placeholder_escape' ) ) { $escaped = $this->add_placeholder_escape( $escaped ); } return $escaped; } /** * Disconnect and remove connection from open connections list * @param string $tdbhname */ public function disconnect( $dbhname ) { $k = array_search( $dbhname, $this->open_connections ); if ( false !== $k ) { unset( $this->open_connections[ $k ] ); } foreach ( array_keys( $this->db_connections ) as $i ) { if ( $this->db_connections[ $i ]['dbhname'] == $dbhname ) { unset( $this->db_connections[ $i ] ); } } if ( $this->is_mysql_connection( $this->dbhs[ $dbhname ] ) ) { $this->ex_mysql_close( $this->dbhs[ $dbhname ] ); } unset( $this->dbhs[ $dbhname ] ); } /** * Kill cached query results */ public function flush() { $this->last_error = ''; $this->last_errno = 0; $this->num_rows = 0; parent::flush(); } /** * Basic query. See docs for more details. * @param string $query * @return int number of rows */ public function query( $query ) { // some queries are made before the plugins have been loaded, and thus cannot be filtered with this method if ( function_exists( 'apply_filters' ) ) { $query = apply_filters( 'query', $query ); } // initialise return $return_val = 0; $this->flush(); // Log how the function was called $this->func_call = "\$db->query(\"$query\")"; // Keep track of the last query for debug.. $this->last_query = $query; if ( preg_match( '/^\s*SELECT\s+FOUND_ROWS(\s*)/i', $query ) ) { if ( $this->is_mysql_result( $this->last_found_rows_result ) ) { $this->result = $this->last_found_rows_result; $elapsed = 0; } else { $this->print_error( 'Attempted SELECT FOUND_ROWS() without prior SQL_CALC_FOUND_ROWS.' ); return false; } } else { $this->dbh = $this->db_connect( $query ); if ( ! $this->is_mysql_connection( $this->dbh ) ) { $this->check_current_query = true; $this->last_error = 'Database connection failed'; $this->num_failed_queries++; if ( function_exists( 'do_action' ) ) { do_action( 'sql_query_log', $query, false, $this->last_error ); } return false; } $query_comment = $this->run_callbacks( 'get_query_comment', $query ); if ( ! empty( $query_comment ) ) { $query = rtrim( $query, ";\t \n\r" ) . ' /* ' . $query_comment . ' */'; } // If we're writing to the database, make sure the query will write safely. if ( $this->check_current_query && method_exists( $this, 'check_ascii' ) && ! $this->check_ascii( $query ) ) { $stripped_query = $this->strip_invalid_text_from_query( $query ); if ( $stripped_query !== $query ) { $this->insert_id = 0; $this->last_error = 'Invalid query'; $this->num_failed_queries++; if ( function_exists( 'do_action' ) ) { do_action( 'sql_query_log', $query, false, $this->last_error ); } return false; } } $this->check_current_query = true; // Inject setup and teardown statements $statement_before_query = $this->run_callbacks( 'statement_before_query' ); $statement_after_query = $this->run_callbacks( 'statement_after_query' ); $query_for_log = $query; $this->timer_start(); if ( $statement_before_query ) { $query_for_log = "$statement_before_query; $query_for_log"; $this->ex_mysql_query( $statement_before_query, $this->dbh ); } $this->result = $this->ex_mysql_query( $query, $this->dbh ); if ( $statement_after_query ) { $query_for_log = "$query_for_log; $statement_after_query"; $this->ex_mysql_query( $statement_after_query, $this->dbh ); } $elapsed = $this->timer_stop(); ++$this->num_queries; if ( preg_match( '/^\s*SELECT\s+SQL_CALC_FOUND_ROWS\s/i', $query ) ) { if ( false === strpos( $query, 'NO_SELECT_FOUND_ROWS' ) ) { $this->timer_start(); $this->last_found_rows_result = $this->ex_mysql_query( 'SELECT FOUND_ROWS()', $this->dbh ); $elapsed += $this->timer_stop(); ++$this->num_queries; $query .= '; SELECT FOUND_ROWS()'; } } else { $this->last_found_rows_result = null; } $this->dbhname_heartbeats[ $this->dbhname ]['last_used'] = microtime( true ); if ( $this->save_queries ) { if ( is_callable( $this->save_query_callback ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace $saved_query = call_user_func_array( $this->save_query_callback, array( $query_for_log, $elapsed, $this->save_backtrace ? debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ) : null, $this ) ); if ( null !== $saved_query ) { $this->queries[] = $saved_query; } } else { $this->queries[] = array( $query_for_log, $elapsed, $this->get_caller() ); } } } $this->last_error = $this->ex_mysql_error( $this->dbh ); if ( $this->last_error ) { $this->last_errno = $this->ex_mysql_errno( $this->dbh ); $this->dbhname_heartbeats[ $this->dbhname ]['last_errno'] = $this->last_errno; // Write failed, use fallback master connection if ( HYPERDB_ER_OPTION_PREVENTS_STATEMENT == $this->last_errno && $this->unique_servers[ $this->dbhname ] > 1 && ! empty( $this->unused_servers[ $this->dbhname ] ) ) { // Stay away from this server $this->disconnect( $this->dbhname ); $this->mark_server_read_only(); $msg = "Can't write to $this->dbhname - server switched to read-only mode. Retrying on configured fallback connection."; $this->print_error( $msg ); // Try again and db_connect will use the next unused server return $this->query( $query ); } $this->print_error( $this->last_error ); $this->num_failed_queries++; if ( function_exists( 'do_action' ) ) { do_action( 'sql_query_log', $query, false, $this->last_error ); } return false; } if ( preg_match( '/^\s*(insert|delete|update|replace|alter)\s/i', $query ) ) { $this->rows_affected = $this->ex_mysql_affected_rows( $this->dbh ); // Take note of the insert_id if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { $this->insert_id = $this->ex_mysql_insert_id( $this->dbh ); } // Return number of rows affected $return_val = $this->rows_affected; } elseif ( is_bool( $this->result ) ) { $return_val = $this->result; $this->result = null; } else { $i = 0; $this->col_info = array(); while ( $i < $this->ex_mysql_num_fields( $this->result ) ) { $this->col_info[ $i ] = $this->ex_mysql_fetch_field( $this->result ); $i++; } $num_rows = 0; $this->last_result = array(); // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition while ( ( $row = $this->ex_mysql_fetch_object( $this->result ) ) ) { $this->last_result[ $num_rows ] = $row; $num_rows++; } $this->ex_mysql_free_result( $this->result ); $this->result = null; // Log number of rows the query returned $this->num_rows = $num_rows; // Return number of rows selected $return_val = $this->num_rows; } if ( function_exists( 'do_action' ) ) { do_action( 'sql_query_log', $query, $return_val, $this->last_error ); } return $return_val; } /** * Whether or not MySQL database is at least the required minimum version. * The additional argument allows the caller to check a specific database. * * @since 2.5.0 * @uses $wp_version * * @return WP_Error */ public function check_database_version( $dbh_or_table = false ) { global $wp_version; // Make sure the server has MySQL 4.1.2 $mysql_version = preg_replace( '|[^0-9\.]|', '', $this->db_version( $dbh_or_table ) ); if ( version_compare( $mysql_version, '4.1.2', '<' ) ) { // translators: 1 - WordPress version return new WP_Error( 'database_version', sprintf( __( 'ERROR: WordPress %s requires MySQL 4.1.2 or higher' ), $wp_version ) ); } } /** * This function is called when WordPress is generating the table schema to determine wether or not the current database * supports or needs the collation statements. * The additional argument allows the caller to check a specific database. * @return bool */ public function supports_collation( $dbh_or_table = false ) { return $this->has_cap( 'collation', $dbh_or_table ); } /** * Generic function to determine if a database supports a particular feature * The additional argument allows the caller to check a specific database. * @param string $db_cap the feature * @param false|string|resource $dbh_or_table the databaese (the current database, the database housing the specified table, or the database of the mysql resource) * @return bool */ public function has_cap( $db_cap, $dbh_or_table = false ) { $version = $this->db_version( $dbh_or_table ); switch ( strtolower( $db_cap ) ) : case 'collation': case 'group_concat': case 'subqueries': return version_compare( $version, '4.1', '>=' ); case 'set_charset': return version_compare( $version, '5.0.7', '>=' ); case 'utf8mb4': // @since WP 4.1.0 if ( version_compare( $version, '5.5.3', '<' ) ) { return false; } if ( $this->use_mysqli ) { $client_version = mysqli_get_client_info(); } else { $client_version = mysql_get_client_info(); } /* * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server. * mysqlnd has supported utf8mb4 since 5.0.9. */ if ( false !== strpos( $client_version, 'mysqlnd' ) ) { $client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version ); return version_compare( $client_version, '5.0.9', '>=' ); } else { return version_compare( $client_version, '5.5.3', '>=' ); } case 'utf8mb4_520': // since WP 4.6 return $this->has_cap( 'utf8mb4', $dbh_or_table ) && version_compare( $version, '5.6', '>=' ); endswitch; return false; } /** * The database version number * @param false|string|resource $dbh_or_table the database (the current database, the database housing the specified table, or the database of the mysql resource) * @return false|string false on failure, version number on success */ public function db_version( $dbh_or_table = false ) { if ( ! $dbh_or_table && $this->dbh ) { $dbh =& $this->dbh; } elseif ( $this->is_mysql_connection( $dbh_or_table ) ) { $dbh =& $dbh_or_table; } else { $dbh = $this->db_connect( "SELECT FROM $dbh_or_table $this->users" ); } if ( $dbh ) { return preg_replace( '/[^0-9.].*/', '', $this->ex_mysql_get_server_info( $dbh ) ); } return false; } /** * Get the name of the function that called wpdb. * @return string the name of the calling function */ public function get_caller() { // requires PHP 4.3+ if ( ! is_callable( 'debug_backtrace' ) ) { return ''; } $hyper_callbacks = array(); foreach ( $this->hyper_callbacks as $group_callbacks ) { $hyper_callbacks = array_merge( $hyper_callbacks, $group_callbacks ); } // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace $bt = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); $caller = ''; foreach ( (array) $bt as $trace ) { if ( isset( $trace['class'] ) && is_a( $this, $trace['class'] ) ) { continue; } elseif ( ! isset( $trace['function'] ) ) { continue; } elseif ( strtolower( $trace['function'] ) == 'call_user_func_array' ) { continue; } elseif ( strtolower( $trace['function'] ) == 'apply_filters' ) { continue; } elseif ( strtolower( $trace['function'] ) == 'do_action' ) { continue; } if ( in_array( strtolower( $trace['function'] ), $hyper_callbacks ) ) { continue; } if ( isset( $trace['class'] ) ) { $caller = $trace['class'] . '::' . $trace['function']; } else { $caller = $trace['function']; } break; } return $caller; } public function log_and_bail( $msg ) { $logged = $this->run_callbacks( 'log_and_bail', $msg ); if ( ! $logged ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( "WordPress database error $msg for query {$this->last_query} made by " . $this->get_caller() ); } return $this->bail( $msg ); } /** * Check the responsiveness of a tcp/ip daemon * @return (string) 'up' when $host:$post responds within $float_timeout seconds, * otherwise a string with details about the failure. */ public function check_tcp_responsiveness( $host, $port, $float_timeout ) { if ( function_exists( 'apcu_store' ) ) { $use_apc = true; $apcu_key = "tcp_responsive_{$host}{$port}"; $apcu_ttl = 10; } else { $use_apc = false; } if ( $use_apc ) { $server_state = apcu_fetch( $apcu_key ); if ( $server_state ) { return $server_state; } } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fsockopen $socket = @ fsockopen( $host, $port, $errno, $errstr, $float_timeout ); if ( false === $socket ) { $server_state = "down [ > $float_timeout ] ($errno) '$errstr'"; if ( $use_apc ) { apcu_store( $apcu_key, $server_state, $apcu_ttl ); } return $server_state; } fclose( $socket ); if ( $use_apc ) { apcu_store( $apcu_key, 'up', $apcu_ttl ); } return 'up'; } public function get_server_state( $host, $port, $dbhname, $timeout ) { // We still do the check_tcp_responsiveness() until we have // mysql connect function with less than 1 second timeout if ( $this->check_tcp_responsiveness && $timeout > 0 ) { $server_state = $this->check_tcp_responsiveness( $host, $port, $timeout ); if ( 'up' !== $server_state ) { return $server_state; } } if ( ! function_exists( 'apcu_store' ) ) { return 'up'; } $server_state = apcu_fetch( "server_state_$host$port$dbhname" ); if ( ! $server_state ) { return 'up'; } return $server_state; } // Signal to other PHP workers that a server can not take writes public function mark_server_read_only() { if ( ! function_exists( 'apcu_store' ) ) { return; } $host = $this->last_connection['host']; $port = $this->last_connection['port']; $dbhname = $this->dbhname; apcu_add( "server_readonly_$host$port$dbhname", 'read_only', 120 ); } public function server_marked_read_only( $host, $port, $dbhname ) { if ( ! function_exists( 'apcu_store' ) ) { return false; } return 'read_only' === apcu_fetch( "server_readonly_$host$port$dbhname" ); } public function mark_server_as_down( $host, $port, $dbhname, $apcu_ttl = 10 ) { if ( ! function_exists( 'apcu_store' ) ) { return; } apcu_add( "server_readonly_$host$port$dbhname", 'down', $apcu_ttl ); } public function set_connect_timeout( $tag, $use_master, $tries_remaining ) { static $default_connect_timeout; if ( ! isset( $default_connect_timeout ) ) { $default_connect_timeout = $this->ex_mysql_connect_timeout(); } switch ( $tag ) { case 'pre_connect': if ( ! $use_master && $tries_remaining ) { $this->ex_mysql_connect_timeout( 1 ); } break; case 'post_connect': default: if ( ! $use_master && $tries_remaining ) { $this->ex_mysql_connect_timeout( $default_connect_timeout ); } break; } } public function get_lag_cache() { $this->lag = $this->run_callbacks( 'get_lag_cache' ); return $this->check_lag(); } public function get_lag() { $this->lag = $this->run_callbacks( 'get_lag' ); return $this->check_lag(); } public function check_lag() { if ( false === $this->lag ) { return HYPERDB_LAG_UNKNOWN; } if ( $this->lag > $this->lag_threshold ) { return HYPERDB_LAG_BEHIND; } return HYPERDB_LAG_OK; } public function should_use_mysqli() { if ( ! function_exists( 'mysqli_connect' ) ) { return false; } if ( defined( 'WP_USE_EXT_MYSQL' ) && WP_USE_EXT_MYSQL ) { return false; } return true; } public function should_mysql_ping() { // Shouldn't happen if ( ! isset( $this->dbhname_heartbeats[ $this->dbhname ] ) ) { return true; } // MySQL server has gone away if ( isset( $this->dbhname_heartbeats[ $this->dbhname ]['last_errno'] ) && HYPERDB_SERVER_GONE_ERROR == $this->dbhname_heartbeats[ $this->dbhname ]['last_errno'] ) { unset( $this->dbhname_heartbeats[ $this->dbhname ]['last_errno'] ); return true; } // More than 0.1 seconds of inactivity on that dbhname if ( microtime( true ) - $this->dbhname_heartbeats[ $this->dbhname ]['last_used'] > 0.1 ) { return true; } return false; } public function is_mysql_connection( $dbh ) { if ( ! $this->use_mysqli ) { return is_resource( $dbh ); } return $dbh instanceof mysqli; } public function is_mysql_result( $result ) { if ( ! $this->use_mysqli ) { return is_resource( $result ); } return $result instanceof mysqli_result; } public function is_mysql_set_charset_callable() { if ( ! $this->use_mysqli ) { return function_exists( 'mysql_set_charset' ); } return function_exists( 'mysqli_set_charset' ); } // MySQL execution functions. // They perform the appropriate calls based on whether we use MySQLi. public function ex_mysql_query( $query, $dbh ) { if ( ! $this->use_mysqli ) { return mysql_query( $query, $dbh ); } return mysqli_query( $dbh, $query ); } public function ex_mysql_unbuffered_query( $query, $dbh ) { if ( ! $this->use_mysqli ) { return mysql_unbuffered_query( $query, $dbh ); } return mysqli_query( $dbh, $query, MYSQLI_USE_RESULT ); } public function ex_mysql_connect( $db_host, $db_user, $db_password, $persistent ) { $client_flags = defined( 'MYSQL_CLIENT_FLAGS' ) ? MYSQL_CLIENT_FLAGS : 0; if ( ! $this->use_mysqli ) { $connect_function = $persistent ? 'mysql_pconnect' : 'mysql_connect'; return @$connect_function( $db_host, $db_user, $db_password, true ); } $dbh = mysqli_init(); // mysqli_real_connect doesn't support the host param including a port or socket // like mysql_connect does. This duplicates how mysql_connect detects a port and/or socket file. $port = null; $socket = null; $port_or_socket = strstr( $db_host, ':' ); if ( ! empty( $port_or_socket ) ) { $db_host = substr( $db_host, 0, strpos( $db_host, ':' ) ); $port_or_socket = substr( $port_or_socket, 1 ); if ( 0 !== strpos( $port_or_socket, '/' ) ) { $port = intval( $port_or_socket ); $maybe_socket = strstr( $port_or_socket, ':' ); if ( ! empty( $maybe_socket ) ) { $socket = substr( $maybe_socket, 1 ); } } else { $socket = $port_or_socket; } } if ( $persistent ) { $db_host = "p:{$db_host}"; } $retval = mysqli_real_connect( $dbh, $db_host, $db_user, $db_password, null, $port, $socket, $client_flags ); if ( ! $retval || $dbh->connect_errno ) { return false; } return $dbh; } public function ex_mysql_select_db( $db_name, $dbh ) { if ( ! $this->use_mysqli ) { return @mysql_select_db( $db_name, $dbh ); } return @mysqli_select_db( $dbh, $db_name ); } public function ex_mysql_close( $dbh ) { if ( ! $this->use_mysqli ) { return mysql_close( $dbh ); } return mysqli_close( $dbh ); } public function ex_mysql_set_charset( $charset, $dbh ) { if ( ! $this->use_mysqli ) { return mysql_set_charset( $charset, $dbh ); } return mysqli_set_charset( $dbh, $charset ); } public function ex_mysql_errno( $dbh = null ) { if ( ! $this->use_mysqli ) { return is_resource( $dbh ) ? mysql_errno( $dbh ) : mysql_errno(); } if ( is_null( $dbh ) ) { return mysqli_connect_errno(); } return mysqli_errno( $dbh ); } public function ex_mysql_error( $dbh = null ) { if ( ! $this->use_mysqli ) { return is_resource( $dbh ) ? mysql_error( $dbh ) : mysql_error(); } if ( is_null( $dbh ) ) { return mysqli_connect_error(); } if ( ! $this->is_mysql_connection( $dbh ) ) { return false; } return mysqli_error( $dbh ); } public function ex_mysql_ping( $dbh ) { if ( ! $this->use_mysqli ) { return @mysql_ping( $dbh ); } return @mysqli_ping( $dbh ); } public function ex_mysql_affected_rows( $dbh ) { if ( ! $this->use_mysqli ) { return mysql_affected_rows( $dbh ); } return mysqli_affected_rows( $dbh ); } public function ex_mysql_insert_id( $dbh ) { if ( ! $this->use_mysqli ) { return mysql_insert_id( $dbh ); } return mysqli_insert_id( $dbh ); } public function ex_mysql_num_fields( $result ) { if ( ! $this->use_mysqli ) { return @mysql_num_fields( $result ); } return @mysqli_num_fields( $result ); } public function ex_mysql_fetch_field( $result ) { if ( ! $this->use_mysqli ) { return @mysql_fetch_field( $result ); } return @mysqli_fetch_field( $result ); } public function ex_mysql_fetch_assoc( $result ) { if ( ! $this->use_mysqli ) { return mysql_fetch_assoc( $result ); } if ( ! $this->is_mysql_result( $result ) ) { return false; } $object = mysqli_fetch_assoc( $result ); return ! is_null( $object ) ? $object : false; } public function ex_mysql_fetch_object( $result ) { if ( ! $this->use_mysqli ) { return @mysql_fetch_object( $result ); } if ( ! $this->is_mysql_result( $result ) ) { return false; } $object = @mysqli_fetch_object( $result ); return ! is_null( $object ) ? $object : false; } public function ex_mysql_fetch_row( $result ) { if ( ! $this->use_mysqli ) { return mysql_fetch_row( $result ); } if ( ! $this->is_mysql_result( $result ) ) { return false; } $row = mysqli_fetch_row( $result ); return ! is_null( $row ) ? $row : false; } public function ex_mysql_num_rows( $result ) { if ( ! $this->use_mysqli ) { return mysql_num_rows( $result ); } return mysqli_num_rows( $result ); } public function ex_mysql_free_result( $result ) { if ( ! $this->use_mysqli ) { @mysql_free_result( $result ); } @mysqli_free_result( $result ); } public function ex_mysql_get_server_info( $dbh ) { if ( ! $this->use_mysqli ) { return mysql_get_server_info( $dbh ); } return mysqli_get_server_info( $dbh ); } public function ex_mysql_connect_timeout( $timeout = null ) { if ( is_null( $timeout ) ) { if ( ! $this->use_mysqli ) { return ini_get( 'mysql.connect_timeout' ); } return ini_get( 'default_socket_timeout' ); } if ( ! $this->use_mysqli ) { // phpcs:ignore WordPress.PHP.IniSet.Risky return ini_set( 'mysql.connect_timeout', $timeout ); } // phpcs:ignore WordPress.PHP.IniSet.Risky return ini_set( 'default_socket_timeout', $timeout ); } // Helper functions for configuration } // class hyperdb // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wpdb = new hyperdb(); /** @psalm-suppress UnresolvableInclude */ require DB_CONFIG_FILE;