├── readme.txt ├── db-config.php └── db.php /readme.txt: -------------------------------------------------------------------------------- 1 | === HyperDB === 2 | Contributors: matt, andy, ryan, mdawaffe, vnsavage, barry, westi, automattic, wpcomvip 3 | Tags: mysql, scaling, performance, availability, WordPress.com 4 | Requires at least: 4.2 5 | Tested up to: 6.0.2 6 | Stable tag: 1.9 7 | License: GPLv2 or later 8 | 9 | HyperDB is an advanced database class that supports replication, failover, load balancing, and partitioning. 10 | 11 | == Description == 12 | 13 | HyperDB is a very advanced database class that replaces a few of the WordPress built-in database functions. The main differences are: 14 | * HyperDB can be connect to an arbitrary number of database servers, 15 | * HyperDB inspects each query to determine the appropriate database. 16 | 17 | It supports: 18 | 19 | * Read and write servers (replication) 20 | * Configurable priority for reading and writing 21 | * Local and remote datacenters 22 | * Private and public networks 23 | * Different tables on different databases/hosts 24 | * Smart post-write master reads 25 | * Failover for downed host 26 | * Advanced statistics for profiling 27 | * WordPress Multisite 28 | 29 | It is based on the code currently used in production on WordPress.com with many MySQL servers spanning multiple datacenters. 30 | 31 | == Installation == 32 | 33 | Nothing goes in the plugins directory. 34 | 35 | 1. Enter a configuration in `db-config.php`. 36 | 37 | 2. Deploy `db-config.php` in the directory that holds `wp-config.php`. This may be the WordPress root or one level above. It may also be anywhere else the web server can see it; in this case, define `DB_CONFIG_FILE` in `wp-config.php`. 38 | 39 | 3. Deploy `db.php` to the `/wp-content/` directory. Simply placing this file activates it. To deactivate it, move it from that location or move the config file. 40 | 41 | Any value of `WP_USE_MULTIPLE_DB` will be ignored by HyperDB. 42 | 43 | == Frequently Asked Questions == 44 | 45 | = What can I do with HyperDB that I can't do with WPDB? = 46 | 47 | WordPress.com, the most complex HyperDB installation, manages millions of tables spanning thousands of databases. Dynamic configuration logic allows HyperDB to compute the location of tables by querying a central database. Custom scripts constantly balance database server resources by migrating tables and updating their locations in the central database. 48 | 49 | Stretch your imagination. You could create a dynamic configuration using persistent caching to gather intelligence about the state of the network. The only constant is the name of the configuration file. The rest, as they say, is PHP. 50 | 51 | = How does HyperDB support replication? = 52 | 53 | HyperDB does not provide replication services. That is done by configuring MySQL servers for replication. HyperDB can then be configured to use these servers appropriately, e.g. by connecting to master servers to perform write queries. 54 | 55 | = How does HyperDB support load balancing? = 56 | 57 | HyperDB randomly selects database connections from priority groups that you configure. The most advantageous connections are tried first. Thus you can optimize your configuration for network topology, hardware capability, or any other scheme you invent. 58 | 59 | = How does HyperDB support failover? = 60 | 61 | Failover describes how HyperDB deals with connection failures. When HyperDB fails to connect to one database, it tries to connect to another database that holds the same data. If replication hasn't been set up, HyperDB tries reconnecting a few times before giving up. 62 | 63 | = How does HyperDB support partitioning? = 64 | 65 | HyperDB allows tables to be placed in arbitrary databases. It can use callbacks you write to compute the appropriate database for a given query. Thus you can partition your site's data according to your own scheme and configure HyperDB accordingly. 66 | 67 | = Is there any advantage to using HyperDB with just one database server? = 68 | 69 | None that has been measured. HyperDB does at least try again before giving up connecting, so it might help in cases where the web server is momentarily unable to connect to the database server. 70 | 71 | One way HyperDB differs from WPDB is that HyperDB does not attempt to connect to a database until a query is made. Thus a site with sufficiently aggressive persistent caching could remain read-only accessible despite the database becoming unreachable. 72 | 73 | = What if all database servers for a dataset go down? = 74 | 75 | Since HyperDB attempts a connection only when a query is made, your WordPress installation will not kill the site with a database error, but will let the code decide what to do next on an unsuccessful query. If you want to do something different, like setting a custom error page or kill the site, you need to define the 'db_connection_error' callback in your db-config.php. 76 | 77 | == Changelog == 78 | = 1.9 = 79 | * Restore the behavior where we retry failed connection attempts to masters; 80 | * Unbreak the logic for marking servers as down - in r2625068 the key for marking server as down was mistakenly changed to "server_readonly_$host$port" instead of "server_state_$host$port" 81 | * If all masters are marked as read-only, ignore the read-only flag and still try to connect; otherwise we might end up incorrectly breaking the master connections for 2 minutes (APCu cache time) if we had temporarily set read-only on them; 82 | * Fix a bug where having $dbhname in the server state keys can delay marking server as down or read_only by doing it once for all possible datasets and operations instead of once for the host+port; 83 | * Fix a bug where we could mark server as read-only even though the ER_OPTION_PREVENTS_STATEMENT error was returned for a different reason; now we match to make sure we actually have 'read-only' in the returned error; 84 | * Fix a bug where the correct tracking of the unique read-only servers or lagged slaves might fail if we have duplicate servers for the same dataset; 85 | * Fix a bug where we would not mark masters as read-only in HyperDB if they were set read-only on the server side after we already opened a connection; 86 | * Fix a bug where we might stop respecting the minimum configured amount of retries per dataset; 87 | * Revert the persistent unused_servers logic which was added in HyperDB 1.8 because: it can cause connection failures when the available servers are exhausted, we might use servers with wrong priorities, and not see newly added servers to HyperDB. Some examples of conditions which would trigger these behaviors include: server-side disconnects on timeout; when we manually disconnect open connections in long-running scripts; when we switch between different datasets on the same remote MySQL server; we've gone over the configured max_connections and started disconnecting existing connections to accomodate new ones; 88 | * Make sure the read_only and the downed logic still works in environments which don't have APCu or APCu is badly fragmented by adding local caching 89 | * Correct the dbh property type 90 | * PHP 8.0 compatibility for call_user_func_array() and db_connections attribute 91 | 92 | = 1.8 = 93 | * Support for fallback master connections 94 | * Add support for marking servers read-only 95 | * Fix the issue when `do_action()` is not available 96 | * Use APCu to cache the results of server responsiveness 97 | * Add support for the `utf8mb4` server capability 98 | 99 | = 1.7 = 100 | * Add support for information_schema and transactions 101 | * Requires WordPress 4.2 for wpdb::get_table_from_query() 102 | 103 | = 1.6 = 104 | * Add support for MYSQL_CLIENT_FLAGS which was added to wpdb in [21609] 105 | * Fix PHP 7.3 Notice 106 | 107 | = 1.5 = 108 | * Fix WordPress 4.8.3 SQLi vulnerability 109 | * Add action for SQL logging 110 | * Never db_connect for SELECT FOUND_ROWS() 111 | * Better cleanup when disconnecing db connections 112 | 113 | = 1.4 = 114 | * Additional logging for HyperDB failures and do not save "null" queries. 115 | 116 | = 1.3 = 117 | * Improved failed query tracking 118 | 119 | = 1.2 = 120 | * PHP7 compatability 121 | * MySQLi support 122 | * Allow utf8mb4 character set 123 | 124 | = 1.1 = 125 | * Extended callbacks functionality 126 | * Added connection error callback 127 | * Added replication lag detection support 128 | 129 | = 1.0 = 130 | * Removed support for WPMU and BackPress. 131 | * New class with inheritance: hyperdb extends wpdb. 132 | * New instantiation scheme: $wpdb = new hyperdb(); then include config. No more $db_* globals. 133 | * New configuration file name (db-config.php) and logic for locating it. (ABSPATH, dirname(ABSPATH), or pre-defined) 134 | * Added fallback to wpdb in case database config not found. 135 | * Renamed servers to databases in config in an attempt to reduce ambiguity. 136 | * Added config interface functions to hyperdb: add_database, add_table, add_callback. 137 | * Refactored db_server array to simplify finding a server. 138 | * Removed native support for datacenters and partitions. The same effects are accomplished by read/write parameters and dataset names. 139 | * Removed preg pattern support from $db_tables. Use callbacks instead. 140 | * Removed delay between connection retries and avoid immediate retry of same server when others are available to try. 141 | * Added connection stats. 142 | * Added save_query_callback for custom debug logging. 143 | * Refined SRTM granularity. Now only send reads to masters when the written table is involved. 144 | * Improved connection reuse logic and added mysql_ping to recover from "server has gone away". 145 | * Added min_tries to configure the minimum number of connection attempts before bailing. 146 | * Added WPDB_PATH constant. Define this if you'd rather not use ABSPATH . WPINC . '/wp-db.php'. 147 | -------------------------------------------------------------------------------- /db-config.php: -------------------------------------------------------------------------------- 1 | queries. It is not 37 | * a constant because you might want to use it momentarily. 38 | * Default: false 39 | */ 40 | $wpdb->save_queries = false; 41 | 42 | /** 43 | * persistent (bool) 44 | * This determines whether to use mysql_connect or mysql_pconnect. The effects 45 | * of this setting may vary and should be carefully tested. 46 | * Default: false 47 | */ 48 | $wpdb->persistent = false; 49 | 50 | /** 51 | * max_connections (int) 52 | * This is the number of mysql connections to keep open. Increase if you expect 53 | * to reuse a lot of connections to different servers. This is ignored if you 54 | * enable persistent connections. 55 | * Default: 10 56 | */ 57 | $wpdb->max_connections = 10; 58 | 59 | /** 60 | * check_tcp_responsiveness 61 | * Enables checking TCP responsiveness by fsockopen prior to mysql_connect or 62 | * mysql_pconnect. This was added because PHP's mysql functions do not provide 63 | * a variable timeout setting. Disabling it may improve average performance by 64 | * a very tiny margin but lose protection against connections failing slowly. 65 | * Default: true 66 | */ 67 | $wpdb->check_tcp_responsiveness = true; 68 | 69 | /** Configuration Functions **/ 70 | 71 | /** 72 | * $wpdb->add_database( $database ); 73 | * 74 | * $database is an associative array with these parameters: 75 | * host (required) Hostname with optional :port. Default port is 3306. 76 | * user (required) MySQL user name. 77 | * password (required) MySQL user password. 78 | * name (required) MySQL database name. 79 | * read (optional) Whether server is readable. Default is 1 (readable). 80 | * Also used to assign preference. See "Network topology". 81 | * write (optional) Whether server is writable. Default is 1 (writable). 82 | * Also used to assign preference in multi-master mode. 83 | * dataset (optional) Name of dataset. Default is 'global'. 84 | * timeout (optional) Seconds to wait for TCP responsiveness. Default is 0.2 85 | * lag_threshold (optional) The minimum lag on a slave in seconds before we consider it lagged. 86 | * Set null to disable. When not set, the value of $wpdb->default_lag_threshold is used. 87 | */ 88 | 89 | /** 90 | * $wpdb->add_table( $dataset, $table ); 91 | * 92 | * $dataset and $table are strings. 93 | */ 94 | 95 | /** 96 | * $wpdb->add_callback( $callback, $callback_group = 'dataset' ); 97 | * 98 | * $callback is a callable function or method. $callback_group is the 99 | * group of callbacks, this $callback belongs to. 100 | * 101 | * Callbacks are executed in the order in which they are registered until one 102 | * of them returns something other than null. 103 | * 104 | * The default $callback_group is 'dataset'. Callback in this group 105 | * will be called with two arguments and expected to compute a dataset or return null. 106 | * $dataset = $callback($table, &$wpdb); 107 | * 108 | * Anything evaluating to false will cause the query to be aborted. 109 | * 110 | * For more complex setups, the callback may be used to overwrite properties of 111 | * $wpdb or variables within hyperdb::connect_db(). If a callback returns an 112 | * array, HyperDB will extract the array. It should be an associative array and 113 | * it should include a $dataset value corresponding to a database added with 114 | * $wpdb->add_database(). It may also include $server, which will be extracted 115 | * to overwrite the parameters of each randomly selected database server prior 116 | * to connection. This allows you to dynamically vary parameters such as the 117 | * host, user, password, database name, lag_threshold and TCP check timeout. 118 | */ 119 | 120 | /** Masters and slaves 121 | * 122 | * A database definition can include 'read' and 'write' parameters. These 123 | * operate as boolean switches but they are typically specified as integers. 124 | * They allow or disallow use of the database for reading or writing. 125 | * 126 | * A master database might be configured to allow reading and writing: 127 | * 'write' => 1, 128 | * 'read' => 1, 129 | * while a slave would be allowed only to read: 130 | * 'write' => 0, 131 | * 'read' => 1, 132 | * 133 | * It might be advantageous to disallow reading from the master, such as when 134 | * there are many slaves available and the master is very busy with writes. 135 | * 'write' => 1, 136 | * 'read' => 0, 137 | * HyperDB tracks the tables that it has written since instantiation and sending 138 | * subsequent read queries to the same server that received the write query. 139 | * Thus a master set up this way will still receive read queries, but only 140 | * subsequent to writes. 141 | */ 142 | 143 | 144 | /** 145 | * Network topology / Datacenter awareness 146 | * 147 | * When your databases are located in separate physical locations there is 148 | * typically an advantage to connecting to a nearby server instead of a more 149 | * distant one. The read and write parameters can be used to place servers into 150 | * logical groups of more or less preferred connections. Lower numbers indicate 151 | * greater preference. 152 | * 153 | * This configuration instructs HyperDB to try reading from one of the local 154 | * slaves at random. If that slave is unreachable or refuses the connection, 155 | * the other slave will be tried, followed by the master, and finally the 156 | * remote slaves in random order. 157 | * Local slave 1: 'write' => 0, 'read' => 1, 158 | * Local slave 2: 'write' => 0, 'read' => 1, 159 | * Local master: 'write' => 1, 'read' => 2, 160 | * Remote slave 1: 'write' => 0, 'read' => 3, 161 | * Remote slave 2: 'write' => 0, 'read' => 3, 162 | * 163 | * In the other datacenter, the master would be remote. We would take that into 164 | * account while deciding where to send reads. Writes would always be sent to 165 | * the master, regardless of proximity. 166 | * Local slave 1: 'write' => 0, 'read' => 1, 167 | * Local slave 2: 'write' => 0, 'read' => 1, 168 | * Remote slave 1: 'write' => 0, 'read' => 2, 169 | * Remote slave 2: 'write' => 0, 'read' => 2, 170 | * Remote master: 'write' => 1, 'read' => 3, 171 | * 172 | * There are many ways to achieve different configurations in different 173 | * locations. You can deploy different config files. You can write code to 174 | * discover the web server's location, such as by inspecting $_SERVER or 175 | * php_uname(), and compute the read/write parameters accordingly. An example 176 | * appears later in this file using the legacy function add_db_server(). 177 | */ 178 | 179 | /** 180 | * Slaves lag awareness 181 | * 182 | * HyperDB accommodates slave lag by making decisions, based on the defined lag 183 | * threshold. If the lag threshold is not set, it will ignore the slave lag. 184 | * Otherwise, it will try to find a non-lagged slave, before connecting to a lagged one. 185 | * 186 | * A slave is considered lagged, if it's replication lag is bigger than the lag threshold 187 | * you have defined in $wpdb->$default_lag_threshold or in the per-database settings, using 188 | * add_database(). You can also rewrite the lag threshold, by returning 189 | * $server['lag_threshold'] variable with the 'dataset' group callbacks. 190 | * 191 | * HyperDB does not check the lag on the slaves. You have to define two callbacks 192 | * callbacks to do that: 193 | * 194 | * $wpdb->add_callback( $callback, 'get_lag_cache' ); 195 | * 196 | * and 197 | * 198 | * $wpdb->add_callback( $callback, 'get_lag' ); 199 | * 200 | * The first one is called, before connecting to a slave and should return 201 | * the replication lag in seconds or false, if unknown, based on $wpdb->lag_cache_key. 202 | * 203 | * The second callback is called after a connection to a slave is established. 204 | * It should return it's replication lag or false, if unknown, 205 | * based on the connection in $wpdb->dbhs[ $wpdb->dbhname ]. 206 | */ 207 | 208 | /** Sample Configuration 1: Using the Default Server **/ 209 | /** NOTE: THIS IS ACTIVE BY DEFAULT. COMMENT IT OUT. **/ 210 | 211 | /** 212 | * This is the most basic way to add a server to HyperDB using only the 213 | * required parameters: host, user, password, name. 214 | * This adds the DB defined in wp-config.php as a read/write server for 215 | * the 'global' dataset. (Every table is in 'global' by default.) 216 | */ 217 | $wpdb->add_database(array( 218 | 'host' => DB_HOST, // If port is other than 3306, use host:port. 219 | 'user' => DB_USER, 220 | 'password' => DB_PASSWORD, 221 | 'name' => DB_NAME, 222 | )); 223 | 224 | /** 225 | * This adds the same server again, only this time it is configured as a slave. 226 | * The last three parameters are set to the defaults but are shown for clarity. 227 | */ 228 | $wpdb->add_database(array( 229 | 'host' => DB_HOST, // If port is other than 3306, use host:port. 230 | 'user' => DB_USER, 231 | 'password' => DB_PASSWORD, 232 | 'name' => DB_NAME, 233 | 'write' => 0, 234 | 'read' => 1, 235 | 'dataset' => 'global', 236 | 'timeout' => 0.2, 237 | )); 238 | 239 | /** Sample Configuration 2: Partitioning **/ 240 | 241 | /** 242 | * This example shows a setup where the multisite blog tables have been 243 | * separated from the global dataset. 244 | */ 245 | /* 246 | $wpdb->add_database(array( 247 | 'host' => 'global.db.example.com', 248 | 'user' => 'globaluser', 249 | 'password' => 'globalpassword', 250 | 'name' => 'globaldb', 251 | )); 252 | $wpdb->add_database(array( 253 | 'host' => 'blog.db.example.com', 254 | 'user' => 'bloguser', 255 | 'password' => 'blogpassword', 256 | 'name' => 'blogdb', 257 | 'dataset' => 'blog', 258 | )); 259 | $wpdb->add_callback('my_db_callback'); 260 | function my_db_callback($query, $wpdb) { 261 | // Multisite blog tables are "{$base_prefix}{$blog_id}_*" 262 | if ( preg_match("/^{$wpdb->base_prefix}\d+_/i", $wpdb->table) ) 263 | return 'blog'; 264 | } 265 | */ 266 | 267 | 268 | /** Sample helper functions from WordPress.com **/ 269 | 270 | /** 271 | * This is back-compatible with an older config style. It is for convenience. 272 | * lhost, part, and dc were removed from hyperdb because the read and write 273 | * parameters provide enough power to achieve the desired effects via config. 274 | * 275 | * @param string $dataset Datset: the name of the dataset. Just use "global" if you don't need horizontal partitioning. 276 | * @param int $part Partition: the vertical partition number (1, 2, 3, etc.). Use "0" if you don't need vertical partitioning. 277 | * @param string $dc Datacenter: where the database server is located. Airport codes are convenient. Use whatever. 278 | * @param int $read Read group: tries all servers in lowest number group before trying higher number group. Typical: 1 for slaves, 2 for master. This will cause reads to go to slaves unless all slaves are unreachable. Zero for no reads. 279 | * @param bool $write Write flag: is this server writable? Works the same as $read. Typical: 1 for master, 0 for slaves. 280 | * @param string $host Internet address: host:port of server on internet. 281 | * @param string $lhost Local address: host:port of server for use when in same datacenter. Leave empty if no local address exists. 282 | * @param string $name Database name. 283 | * @param string $user Database user. 284 | * @param string $password Database password. 285 | */ 286 | /* 287 | function add_db_server( $dataset, $part, $dc, $read, $write, $host, $lhost, $name, $user, $password, $timeout = 0.2 ) { 288 | global $wpdb; 289 | 290 | // dc is not used in hyperdb. This produces the desired effect of 291 | // trying to connect to local servers before remote servers. Also 292 | // increases time allowed for TCP responsiveness check. 293 | if ( ! empty( $dc ) && defined( 'DATACENTER' ) && $dc != DATACENTER ) { 294 | if ( $read ) { 295 | $read += 10000; 296 | } 297 | 298 | if ( $write ) { 299 | $write += 10000; 300 | } 301 | 302 | $timeout = 0.7; 303 | } 304 | 305 | // You'll need a hyperdb::add_callback() callback function to use partitioning. 306 | // $wpdb->add_callback( 'my_func' ); 307 | if ( $part ) { 308 | $dataset = $dataset . '_' . $part; 309 | } 310 | 311 | $database = compact( 'dataset', 'read', 'write', 'host', 'name', 'user', 'password', 'timeout' ); 312 | 313 | $wpdb->add_database( $database ); 314 | 315 | // lhost is not used in hyperdb. This configures hyperdb with an 316 | // additional server to represent the local hostname so it tries to 317 | // connect over the private interface before the public one. 318 | if ( ! empty( $lhost ) && $lhost !== $host ) { 319 | $database['host'] = $lhost; 320 | 321 | if ( $read ) { 322 | $database['read'] = $read - 1; 323 | } 324 | 325 | if ( $write ) { 326 | $database['write'] = $write - 1; 327 | } 328 | 329 | $wpdb->add_database( $database ); 330 | } 331 | } 332 | */ 333 | 334 | /** 335 | * Sample replication lag detection configuration. 336 | * 337 | * We use mk-heartbeat (http://www.maatkit.org/doc/mk-heartbeat.html) 338 | * to detect replication lag. 339 | * 340 | * This implementation requires the database user 341 | * to have read access to the heartbeat table. 342 | * 343 | * The cache uses shared memory for portability. 344 | * Can be modified to work with Memcached, APC and etc. 345 | */ 346 | 347 | /* 348 | 349 | $wpdb->lag_cache_ttl = 30; 350 | $wpdb->shmem_key = ftok( __FILE__, "Y" ); 351 | $wpdb->shmem_size = 128 * 1024; 352 | 353 | $wpdb->add_callback( 'get_lag_cache', 'get_lag_cache' ); 354 | $wpdb->add_callback( 'get_lag', 'get_lag' ); 355 | 356 | function get_lag_cache( $wpdb ) { 357 | $segment = shm_attach( $wpdb->shmem_key, $wpdb->shmem_size, 0600 ); 358 | $lag_data = @shm_get_var( $segment, 0 ); 359 | shm_detach( $segment ); 360 | 361 | if ( !is_array( $lag_data ) || !is_array( $lag_data[ $wpdb->lag_cache_key ] ) ) 362 | return false; 363 | 364 | if ( $wpdb->lag_cache_ttl < time() - $lag_data[ $wpdb->lag_cache_key ][ 'timestamp' ] ) 365 | return false; 366 | 367 | return $lag_data[ $wpdb->lag_cache_key ][ 'lag' ]; 368 | } 369 | 370 | function get_lag( $wpdb ) { 371 | $dbh = $wpdb->dbhs[ $wpdb->dbhname ]; 372 | 373 | if ( !mysql_select_db( 'heartbeat', $dbh ) ) 374 | return false; 375 | 376 | $result = mysql_query( "SELECT UNIX_TIMESTAMP() - UNIX_TIMESTAMP(ts) AS lag FROM heartbeat LIMIT 1", $dbh ); 377 | 378 | if ( !$result || false === $row = mysql_fetch_assoc( $result ) ) 379 | return false; 380 | 381 | // Cache the result in shared memory with timestamp 382 | $sem_id = sem_get( $wpdb->shmem_key, 1, 0600, 1 ) ; 383 | sem_acquire( $sem_id ); 384 | $segment = shm_attach( $wpdb->shmem_key, $wpdb->shmem_size, 0600 ); 385 | $lag_data = @shm_get_var( $segment, 0 ); 386 | 387 | if ( !is_array( $lag_data ) ) 388 | $lag_data = array(); 389 | 390 | $lag_data[ $wpdb->lag_cache_key ] = array( 'timestamp' => time(), 'lag' => $row[ 'lag' ] ); 391 | shm_put_var( $segment, 0, $lag_data ); 392 | shm_detach( $segment ); 393 | sem_release( $sem_id ); 394 | 395 | return $row[ 'lag' ]; 396 | } 397 | 398 | */ 399 | 400 | // The ending PHP tag is omitted. This is actually safer than including it. 401 | -------------------------------------------------------------------------------- /db.php: -------------------------------------------------------------------------------- 1 | dbh) for established mysql connections 105 | * @var array 106 | */ 107 | public $dbhs; 108 | 109 | /** 110 | * The multi-dimensional array of datasets and servers 111 | * @var array 112 | */ 113 | public $hyper_servers = array(); 114 | 115 | /** 116 | * Optional directory of tables and their datasets 117 | * @var array 118 | */ 119 | public $hyper_tables = array(); 120 | 121 | /** 122 | * Optional directory of callbacks to determine datasets from queries 123 | * @var array 124 | */ 125 | public $hyper_callbacks = array(); 126 | 127 | /** 128 | * Custom callback to save debug info in $this->queries 129 | * @var callable 130 | */ 131 | public $save_query_callback = null; 132 | 133 | /** 134 | * Whether to use persistent connections 135 | * @var bool 136 | */ 137 | public $persistent = false; 138 | 139 | /** 140 | * The maximum number of db links to keep open. The least-recently used 141 | * link will be closed when the number of links exceeds this. 142 | * @var int 143 | */ 144 | public $max_connections = 10; 145 | 146 | /** 147 | * Whether to check with fsockopen prior to connecting to mysql. 148 | * @var bool 149 | */ 150 | public $check_tcp_responsiveness = true; 151 | 152 | /** 153 | * Minimum number of connections to try before bailing 154 | * @var int 155 | */ 156 | public $min_tries = 3; 157 | 158 | /** 159 | * Send Reads To Masters. This disables slave connections while true. 160 | * Otherwise it is an array of written tables. 161 | * @var array 162 | */ 163 | public $srtm = array(); 164 | 165 | /** 166 | * The log of db connections made and the time each one took 167 | * @var array 168 | */ 169 | public $db_connections = array(); 170 | 171 | /** 172 | * The list of unclosed connections sorted by LRU 173 | */ 174 | public $open_connections = array(); 175 | 176 | /** 177 | * The last server used and the database name selected 178 | * @var array 179 | */ 180 | public $last_used_server; 181 | 182 | /** 183 | * Lookup array (dbhname => (server, db name) ) for re-selecting the db 184 | * when a link is re-used. 185 | * @var array 186 | */ 187 | public $used_servers = array(); 188 | 189 | /** 190 | * Lookup array (dbhname => int) of the number of unique servers in the config 191 | * @var array 192 | */ 193 | public $unique_servers = array(); 194 | 195 | /** 196 | * A list of servers we found to be read-only 197 | * @var array 198 | */ 199 | public $read_only_servers = array(); 200 | 201 | /** 202 | * A list of servers' state 203 | * @var array 204 | */ 205 | public $servers_state = array(); 206 | 207 | /** 208 | * Whether to save debug_backtrace in save_query_callback. You may wish 209 | * to disable this, e.g. when tracing out-of-memory problems. 210 | */ 211 | public $save_backtrace = true; 212 | 213 | /** 214 | * Maximum lag in seconds. Set null to disable. Requires callbacks. 215 | * @var integer 216 | */ 217 | public $default_lag_threshold = null; 218 | 219 | /** 220 | * Lookup array (dbhname => host:port) 221 | * @var array 222 | */ 223 | public $dbh2host = array(); 224 | 225 | /** 226 | * Keeps track of the dbhname usage and errors. 227 | */ 228 | public $dbhname_heartbeats = array(); 229 | 230 | /** 231 | * Counter for how many queries have failed during the life of the $wpdb object 232 | */ 233 | public $num_failed_queries = 0; 234 | 235 | /** 236 | * Gets ready to make database connections 237 | * @param array db class vars 238 | */ 239 | public function __construct( $args = null ) { 240 | if ( is_array( $args ) ) { 241 | foreach ( get_class_vars( __CLASS__ ) as $var => $value ) { 242 | if ( isset( $args[ $var ] ) ) { 243 | $this->$var = $args[ $var ]; 244 | } 245 | } 246 | } 247 | 248 | $this->use_mysqli = $this->should_use_mysqli(); 249 | if ( $this->use_mysqli ) { 250 | mysqli_report( MYSQLI_REPORT_OFF ); 251 | } 252 | 253 | $this->init_charset(); 254 | } 255 | 256 | /** 257 | * Add the connection parameters for a database 258 | */ 259 | public function add_database( $db ) { 260 | extract( $db, EXTR_SKIP ); 261 | if ( ! isset( $dataset ) ) { 262 | $dataset = 'global'; 263 | } 264 | 265 | if ( ! isset( $read ) ) { 266 | $read = 1; 267 | } 268 | 269 | if ( ! isset( $write ) ) { 270 | $write = 1; 271 | } 272 | 273 | unset( $db['dataset'] ); 274 | 275 | if ( $read ) { 276 | $this->hyper_servers[ $dataset ]['read'][ $read ][] = $db; 277 | } 278 | if ( $write ) { 279 | $this->hyper_servers[ $dataset ]['write'][ $write ][] = $db; 280 | } 281 | } 282 | 283 | /** 284 | * Specify the dateset where a table is found 285 | */ 286 | public function add_table( $dataset, $table ) { 287 | $this->hyper_tables[ $table ] = $dataset; 288 | } 289 | 290 | /** 291 | * Add a callback to a group of callbacks. 292 | * The default group is 'dataset', used to examine 293 | * queries and determine dataset. 294 | */ 295 | public function add_callback( $callback, $group = 'dataset' ) { 296 | $this->hyper_callbacks[ $group ][] = $callback; 297 | } 298 | 299 | /** 300 | * Find the table to be used for query routing. Falls back on 301 | * core get_table_from_query after checking for special cases. 302 | * @param string query 303 | * @return string table 304 | */ 305 | public function get_table_from_query( $q ) { 306 | // Remove characters that can legally trail the table name 307 | $q = rtrim( $q, ';/-#' ); 308 | // allow (select...) union [...] style queries. Use the first queries table name. 309 | $q = ltrim( $q, "\t (" ); 310 | // Strip everything between parentheses except nested 311 | // selects and use only 1500 chars of the query 312 | $q = preg_replace( '/\((?!\s*select)[^(]*?\)/is', '()', substr( $q, 0, 1500 ) ); 313 | 314 | // SELECT FOUND_ROWS() refers to the previous SELECT query 315 | if ( preg_match( '/^\s*SELECT.*?\s+FOUND_ROWS\(\)/is', $q ) ) { 316 | return $this->last_table; 317 | } 318 | 319 | // SELECT FROM information_schema.* WHERE TABLE_NAME = 'wp_12345_foo' 320 | if ( preg_match('/^\s*' 321 | . 'SELECT.*?\s+FROM\s+`?information_schema`?\.' 322 | . '.*\s+TABLE_NAME\s*=\s*["\']([\w-]+)["\']/is', $q, $maybe) ) { 323 | return $maybe[1]; 324 | } 325 | 326 | // Transaction support, requires a table hint via comment: IN_TABLE=table_name 327 | if ( preg_match('/^\s*' 328 | . '(?:START\s+TRANSACTION|COMMIT|ROLLBACK)\s*\/[*]\s*IN_TABLE\s*=\s*' 329 | . "'?([\w-]+)'?/is", $q, $maybe) ) { 330 | return $maybe[1]; 331 | } 332 | 333 | $this->last_table = parent::get_table_from_query( $q ); 334 | return $this->last_table; 335 | } 336 | 337 | /** 338 | * Determine the likelihood that this query could alter anything 339 | * @param string query 340 | * @return bool 341 | */ 342 | public function is_write_query( $q ) { 343 | // Quick and dirty: only SELECT statements are considered read-only. 344 | $q = ltrim( $q, "\r\n\t (" ); 345 | return ! preg_match( '/^(?:SELECT|SHOW|DESCRIBE|DESC|EXPLAIN)\s/i', $q ); 346 | } 347 | 348 | /** 349 | * Set a flag to prevent reading from slaves which might be lagging after a write 350 | */ 351 | public function send_reads_to_masters() { 352 | $this->srtm = true; 353 | } 354 | 355 | /** 356 | * Callbacks are executed in the order in which they are registered until one 357 | * of them returns something other than null. 358 | */ 359 | public function run_callbacks( $group, $args = null ) { 360 | if ( ! isset( $this->hyper_callbacks[ $group ] ) || ! is_array( $this->hyper_callbacks[ $group ] ) ) { 361 | return null; 362 | } 363 | 364 | if ( ! isset( $args ) ) { 365 | $args = array( $this ); 366 | } elseif ( is_array( $args ) ) { 367 | // 8.0+ changed the behavior of call_user_func_array(), associative arrays would turn into named attributes 368 | // Here we discard the keys and hope for the best 369 | $args = array_values( $args ); 370 | $args[] = $this; 371 | } else { 372 | $args = array( $args, $this ); 373 | } 374 | 375 | foreach ( $this->hyper_callbacks[ $group ] as $func ) { 376 | $result = call_user_func_array( $func, $args ); 377 | if ( isset( $result ) ) { 378 | return $result; 379 | } 380 | } 381 | } 382 | 383 | /** 384 | * Figure out which database server should handle the query, and connect to it. 385 | * @param string query 386 | * @return resource mysql database connection 387 | */ 388 | public function db_connect( $query = '' ) { 389 | if ( empty( $query ) ) { 390 | return false; 391 | } 392 | 393 | $this->table = $this->get_table_from_query( $query ); 394 | 395 | if ( isset( $this->hyper_tables[ $this->table ] ) ) { 396 | $dataset = $this->hyper_tables[ $this->table ]; 397 | $this->callback_result = null; 398 | } else { 399 | $this->callback_result = $this->run_callbacks( 'dataset', $query ); 400 | if ( null !== $this->callback_result ) { 401 | if ( is_array( $this->callback_result ) ) { 402 | extract( $this->callback_result, EXTR_OVERWRITE ); 403 | } else { 404 | $dataset = $this->callback_result; 405 | } 406 | } 407 | } 408 | 409 | if ( ! isset( $dataset ) ) { 410 | $dataset = 'global'; 411 | } 412 | 413 | if ( ! $dataset ) { 414 | return $this->log_and_bail( "Unable to determine dataset (for table: $this->table)" ); 415 | } else { 416 | $this->dataset = $dataset; 417 | } 418 | 419 | $this->run_callbacks( 'dataset_found', $dataset ); 420 | 421 | if ( empty( $this->hyper_servers ) ) { 422 | if ( $this->is_mysql_connection( $this->dbh ) ) { 423 | return $this->dbh; 424 | } 425 | if ( 426 | ! defined( 'DB_HOST' ) 427 | || ! defined( 'DB_USER' ) 428 | || ! defined( 'DB_PASSWORD' ) 429 | || ! defined( 'DB_NAME' ) ) { 430 | return $this->log_and_bail( 'We were unable to query because there was no database defined' ); 431 | } 432 | $this->dbh = $this->ex_mysql_connect( DB_HOST, DB_USER, DB_PASSWORD, $this->persistent ); 433 | if ( ! $this->is_mysql_connection( $this->dbh ) ) { 434 | return $this->log_and_bail( 'We were unable to connect to the database. (DB_HOST)' ); 435 | } 436 | if ( ! $this->ex_mysql_select_db( DB_NAME, $this->dbh ) ) { 437 | return $this->log_and_bail( 'We were unable to select the database' ); 438 | } 439 | if ( ! empty( $this->charset ) ) { 440 | $collation_query = "SET NAMES '$this->charset'"; 441 | if ( ! empty( $this->collate ) ) { 442 | $collation_query .= " COLLATE '$this->collate'"; 443 | } 444 | $this->ex_mysql_query( $collation_query, $this->dbh ); 445 | } 446 | return $this->dbh; 447 | } 448 | 449 | // Determine whether the query must be sent to the master (a writable server) 450 | if ( ! empty( $use_master ) || true === $this->srtm || isset( $this->srtm[ $this->table ] ) ) { 451 | $use_master = true; 452 | } elseif ( $this->is_write_query( $query ) ) { 453 | $use_master = true; 454 | if ( is_array( $this->srtm ) ) { 455 | $this->srtm[ $this->table ] = true; 456 | } 457 | } elseif ( ! isset( $use_master ) && is_array( $this->srtm ) && ! empty( $this->srtm ) ) { 458 | // Detect queries that have a join in the srtm array. 459 | $use_master = false; 460 | $query_match = substr( $query, 0, 1000 ); 461 | foreach ( $this->srtm as $key => $value ) { 462 | if ( false !== stripos( $query_match, $key ) ) { 463 | $use_master = true; 464 | break; 465 | } 466 | } 467 | } else { 468 | $use_master = false; 469 | } 470 | 471 | if ( $use_master ) { 472 | $dbhname = $dataset . '__w'; 473 | $this->dbhname = $dbhname; 474 | $operation = 'write'; 475 | } else { 476 | $dbhname = $dataset . '__r'; 477 | $this->dbhname = $dbhname; 478 | $operation = 'read'; 479 | } 480 | 481 | // Try to reuse an existing connection 482 | while ( isset( $this->dbhs[ $dbhname ] ) && $this->is_mysql_connection( $this->dbhs[ $dbhname ] ) ) { 483 | // Find the connection for incrementing counters 484 | foreach ( array_keys( $this->db_connections ) as $i ) { 485 | if ( $this->db_connections[ $i ]['dbhname'] == $dbhname ) { 486 | $conn =& $this->db_connections[ $i ]; 487 | } 488 | } 489 | 490 | if ( isset( $server['name'] ) ) { 491 | $name = $server['name']; 492 | // A callback has specified a database name so it's possible the existing connection selected a different one. 493 | if ( $name != $this->used_servers[ $dbhname ]['name'] ) { 494 | if ( ! $this->ex_mysql_select_db( $name, $this->dbhs[ $dbhname ] ) ) { 495 | // this can happen when the user varies and lacks permission on the $name database 496 | if ( isset( $conn['disconnect (select failed)'] ) ) { 497 | ++$conn['disconnect (select failed)']; 498 | } else { 499 | $conn['disconnect (select failed)'] = 1; 500 | } 501 | 502 | $this->disconnect( $dbhname ); 503 | break; 504 | } 505 | $this->used_servers[ $dbhname ]['name'] = $name; 506 | } 507 | } else { 508 | $name = $this->used_servers[ $dbhname ]['name']; 509 | } 510 | 511 | $this->current_host = $this->dbh2host[ $dbhname ]; 512 | 513 | // Keep this connection at the top of the stack to prevent disconnecting frequently-used connections 514 | $k = array_search( $dbhname, $this->open_connections ); 515 | if ( $k ) { 516 | unset( $this->open_connections[ $k ] ); 517 | $this->open_connections[] = $dbhname; 518 | } 519 | 520 | $this->last_used_server = $this->used_servers[ $dbhname ]; 521 | $this->last_connection = compact( 'dbhname', 'name' ); 522 | 523 | if ( $this->should_mysql_ping() && ! $this->ex_mysql_ping( $this->dbhs[ $dbhname ] ) ) { 524 | if ( isset( $conn['disconnect (ping failed)'] ) ) { 525 | ++$conn['disconnect (ping failed)']; 526 | } else { 527 | $conn['disconnect (ping failed)'] = 1; 528 | } 529 | 530 | $this->disconnect( $dbhname ); 531 | break; 532 | } 533 | 534 | if ( isset( $conn['queries'] ) ) { 535 | ++$conn['queries']; 536 | } else { 537 | $conn['queries'] = 1; 538 | } 539 | 540 | return $this->dbhs[ $dbhname ]; 541 | } 542 | 543 | if ( $use_master && defined( 'MASTER_DB_DEAD' ) ) { 544 | 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." ); 545 | } 546 | 547 | if ( empty( $this->hyper_servers[ $dataset ][ $operation ] ) ) { 548 | return $this->log_and_bail( "No databases available with $this->table ($dataset)" ); 549 | } 550 | 551 | // Put the groups in order by priority 552 | ksort( $this->hyper_servers[ $dataset ][ $operation ] ); 553 | 554 | // Make a list of at least $this->min_tries connections to try, repeating as necessary. 555 | $servers = array(); 556 | $this->unique_servers[ $dbhname ] = array(); 557 | do { 558 | foreach ( $this->hyper_servers[ $dataset ][ $operation ] as $group => $items ) { 559 | $keys = array_keys( $items ); 560 | shuffle( $keys ); 561 | foreach ( $keys as $key ) { 562 | $servers[] = compact( 'group', 'key' ); 563 | 564 | if ( ! isset( $this->unique_servers[ $dbhname ][ $items[ $key ]['host'] ] ) ) { 565 | $this->unique_servers[ $dbhname ][ $items[ $key ]['host'] ] = true; 566 | } 567 | } 568 | } 569 | 570 | if ( 0 === count( $this->unique_servers[ $dbhname ] ) ) { 571 | return $this->log_and_bail( "No database servers were found to match the query ($this->table, $dataset)" ); 572 | } 573 | } while ( count( $servers ) < $this->min_tries ); // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found 574 | 575 | // Connect to a database server 576 | do { 577 | $unique_lagged_slaves = array(); 578 | $unique_readonly_masters = array(); 579 | $success = false; 580 | $tries_remaining = count( $servers ); 581 | 582 | foreach ( $servers as $group_key ) { 583 | --$tries_remaining; 584 | 585 | // Variables: $group, $key 586 | extract( $group_key, EXTR_OVERWRITE ); 587 | 588 | // Variables: $host, $user, $password, $name, $read, $write [, $lag_threshold, $timeout ] 589 | extract( $this->hyper_servers[ $dataset ][ $operation ][ $group ][ $key ], EXTR_OVERWRITE ); 590 | $port = null; 591 | 592 | // Split host:port into $host and $port 593 | if ( strpos( $host, ':' ) ) { 594 | list($host, $port) = explode( ':', $host ); 595 | } 596 | 597 | // Overlay $server if it was extracted from a callback 598 | if ( isset( $server ) && is_array( $server ) ) { 599 | extract( $server, EXTR_OVERWRITE ); 600 | } 601 | 602 | // Split again in case $server had host:port 603 | if ( strpos( $host, ':' ) ) { 604 | list($host, $port) = explode( ':', $host ); 605 | } 606 | 607 | // Make sure there's always a port number 608 | if ( empty( $port ) ) { 609 | $port = 3306; 610 | } 611 | 612 | // Use a default timeout of 200ms 613 | if ( ! isset( $timeout ) ) { 614 | $timeout = 0.2; 615 | } 616 | 617 | // Get the minimum group here, in case $server rewrites it 618 | if ( ! isset( $min_group ) || $min_group > $group ) { 619 | $min_group = $group; 620 | } 621 | 622 | // If a master is read-only and we have others which might not be RO, go to the next one 623 | if ( $use_master && $this->unique_servers[ $dbhname ] > 1 && empty( $ignore_server_read_only ) ) { 624 | if ( count( $unique_readonly_masters ) == count( $this->unique_servers[ $dbhname ] ) ) { 625 | $ignore_server_read_only = true; // All are read-only, ignore RO and retry 626 | continue 2; 627 | } 628 | 629 | if ( $this->is_server_marked_read_only( $host, $port ) ) { 630 | $unique_readonly_masters[ "$host:$port" ] = true; 631 | continue; 632 | } 633 | } 634 | 635 | // Can be used by the lag callbacks 636 | $this->lag_cache_key = "$host:$port"; 637 | $this->lag_threshold = isset( $lag_threshold ) ? $lag_threshold : $this->default_lag_threshold; 638 | 639 | // Check for a lagged slave, if applicable 640 | if ( ! $use_master && ! $write && ! isset( $ignore_slave_lag ) 641 | && isset( $this->lag_threshold ) && ! isset( $server['host'] ) 642 | // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure 643 | && ( $lagged_status = $this->get_lag_cache() ) === HYPERDB_LAG_BEHIND 644 | ) { 645 | // If it is the last lagged slave and it is with the best preference we will ignore its lag 646 | if ( ! isset( $unique_lagged_slaves[ "$host:$port" ] ) 647 | && count( $unique_lagged_slaves ) + 1 == count( $this->unique_servers[ $dbhname ] ) 648 | && $group == $min_group ) { 649 | $this->lag_threshold = null; 650 | } else { 651 | $unique_lagged_slaves[ "$host:$port" ] = $this->lag; 652 | continue; 653 | } 654 | } 655 | 656 | $this->timer_start(); 657 | 658 | // Connect if necessary or possible 659 | $server_state = null; 660 | if ( $use_master || ! $tries_remaining || 661 | // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure 662 | 'up' === ( $server_state = $this->get_server_state( $host, $port, $dbhname, $timeout ) ) ) { 663 | $this->set_connect_timeout( 'pre_connect', $use_master, $tries_remaining ); 664 | $this->dbhs[ $dbhname ] = $this->ex_mysql_connect( "$host:$port", $user, $password, $this->persistent ); 665 | $this->set_connect_timeout( 'post_connect', $use_master, $tries_remaining ); 666 | } else { 667 | $this->dbhs[ $dbhname ] = false; 668 | } 669 | 670 | $elapsed = $this->timer_stop(); 671 | 672 | if ( $this->is_mysql_connection( $this->dbhs[ $dbhname ] ) ) { 673 | /** 674 | * If we care about lag, disconnect lagged slaves and try to find others. 675 | * We don't disconnect if it is the last lagged slave and it is with the best preference. 676 | */ 677 | if ( ! $use_master && ! $write && ! isset( $ignore_slave_lag ) 678 | && isset( $this->lag_threshold ) && ! isset( $server['host'] ) 679 | && HYPERDB_LAG_OK !== $lagged_status 680 | && ( $lagged_status = $this->get_lag() ) === HYPERDB_LAG_BEHIND // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure 681 | && ! ( 682 | ! isset( $unique_lagged_slaves[ "$host:$port" ] ) 683 | && count( $unique_lagged_slaves ) + 1 === count( $this->unique_servers[ $dbhname ] ) 684 | && $group == $min_group 685 | ) 686 | ) { 687 | $success = false; 688 | $unique_lagged_slaves[ "$host:$port" ] = $this->lag; 689 | $this->disconnect( $dbhname ); 690 | $this->dbhs[ $dbhname ] = false; 691 | $msg = "Replication lag of {$this->lag}s on $host:$port ($dbhname)"; 692 | $this->print_error( $msg ); 693 | continue; 694 | } elseif ( $this->ex_mysql_select_db( $name, $this->dbhs[ $dbhname ] ) ) { 695 | $success = true; 696 | $this->current_host = "$host:$port"; 697 | $this->dbh2host[ $dbhname ] = "$host:$port"; 698 | $queries = 1; 699 | $lag = isset( $this->lag ) ? $this->lag : 0; 700 | $this->last_connection = compact( 'dbhname', 'host', 'port', 'user', 'name', 'server_state', 'elapsed', 'success', 'queries', 'lag' ); 701 | $this->db_connections[] = $this->last_connection; 702 | $this->open_connections[] = $dbhname; 703 | break; 704 | } 705 | } 706 | 707 | if ( 'down' == $server_state ) { 708 | continue; // don't flood the logs if already down 709 | } 710 | 711 | $errno = $this->ex_mysql_errno(); 712 | if ( ( HYPERDB_CONN_HOST_ERROR == $errno || HYPERDB_CONNNECTION_ERROR == $errno ) && 713 | ( 'up' == $server_state || ! $tries_remaining ) ) { 714 | $this->mark_server_as_down( $host, $port ); 715 | $server_state = 'down'; 716 | } 717 | 718 | $referrer = ''; 719 | if ( ! empty( $_SERVER['HTTP_HOST'] ) ) { 720 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- print_error() will sanitize the entire message 721 | $referrer .= (string) $_SERVER['HTTP_HOST']; 722 | } 723 | 724 | if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { 725 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- print_error() will sanitize the entire message 726 | $referrer .= (string) $_SERVER['REQUEST_URI']; 727 | } 728 | 729 | if ( ! isset( $tcp ) ) { 730 | $tcp = ''; 731 | } 732 | 733 | if ( ! isset( $server ) ) { 734 | $server = ''; 735 | } 736 | 737 | $success = false; 738 | $this->last_connection = compact( 'dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success' ); 739 | $this->db_connections[] = $this->last_connection; 740 | $msg = date( 'Y-m-d H:i:s' ) . " Can't select $dbhname - \n"; // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date 741 | $msg .= "'referrer' => '{$referrer}',\n"; 742 | $msg .= "'server' => {$server},\n"; 743 | $msg .= "'host' => {$host},\n"; 744 | $msg .= "'error' => " . $this->ex_mysql_error() . ",\n"; 745 | $msg .= "'errno' => " . $this->ex_mysql_errno() . ",\n"; 746 | $msg .= "'server_state' => $server_state\n"; 747 | $msg .= "'lagged_status' => " . ( isset( $lagged_status ) ? $lagged_status : HYPERDB_LAG_UNKNOWN ); 748 | 749 | $this->print_error( $msg ); 750 | } 751 | 752 | if ( ! $success || ! isset( $this->dbhs[ $dbhname ] ) || ! $this->is_mysql_connection( $this->dbhs[ $dbhname ] ) ) { 753 | if ( ! isset( $ignore_slave_lag ) && count( $unique_lagged_slaves ) ) { 754 | // Lagged slaves were not used. Ignore the lag for this connection attempt and retry. 755 | $ignore_slave_lag = true; 756 | continue; 757 | } 758 | 759 | $error_details = array( 760 | 'host' => $host, 761 | 'port' => $port, 762 | 'operation' => $operation, 763 | 'table' => $this->table, 764 | 'dataset' => $dataset, 765 | 'dbhname' => $dbhname, 766 | ); 767 | $this->run_callbacks( 'db_connection_error', $error_details ); 768 | 769 | return $this->bail( "Unable to connect to $host:$port to $operation table '$this->table' ($dataset)" ); 770 | } 771 | 772 | break; 773 | } while ( true ); 774 | 775 | if ( ! isset( $charset ) ) { 776 | $charset = null; 777 | } 778 | 779 | if ( ! isset( $collate ) ) { 780 | $collate = null; 781 | } 782 | 783 | $this->set_charset( $this->dbhs[ $dbhname ], $charset, $collate ); 784 | 785 | $this->dbh = $this->dbhs[ $dbhname ]; // needed by $wpdb->_real_escape() 786 | 787 | $this->last_used_server = compact( 'host', 'user', 'name', 'read', 'write' ); 788 | 789 | $this->used_servers[ $dbhname ] = $this->last_used_server; 790 | 791 | // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found 792 | while ( ! $this->persistent && count( $this->open_connections ) > $this->max_connections ) { 793 | $oldest_connection = array_shift( $this->open_connections ); 794 | if ( $this->dbhs[ $oldest_connection ] != $this->dbhs[ $dbhname ] ) { 795 | $this->disconnect( $oldest_connection ); 796 | } 797 | } 798 | 799 | return $this->dbhs[ $dbhname ]; 800 | } 801 | 802 | /** 803 | * Sets the connection's character set. 804 | * @param resource $dbh The resource given by ex_mysql_connect 805 | * @param string $charset The character set (optional) 806 | * @param string $collate The collation (optional) 807 | */ 808 | public function set_charset( $dbh, $charset = null, $collate = null ) { 809 | if ( ! isset( $charset ) ) { 810 | $charset = $this->charset; 811 | } 812 | if ( ! isset( $collate ) ) { 813 | $collate = $this->collate; 814 | } 815 | 816 | if ( ! $this->has_cap( 'collation', $dbh ) || empty( $charset ) ) { 817 | return; 818 | } 819 | 820 | if ( ! in_array( strtolower( $charset ), array( 'utf8', 'utf8mb4', 'latin1' ) ) ) { 821 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 822 | wp_die( sprintf( "%s charset isn't supported in HyperDB for security reasons", htmlspecialchars( $charset ) ) ); 823 | } 824 | 825 | if ( $this->is_mysql_set_charset_callable() && $this->has_cap( 'set_charset', $dbh ) ) { 826 | $this->ex_mysql_set_charset( $charset, $dbh ); 827 | $this->real_escape = true; 828 | } else { 829 | $query = $this->prepare( 'SET NAMES %s', $charset ); 830 | if ( ! empty( $collate ) ) { 831 | $query .= $this->prepare( ' COLLATE %s', $collate ); 832 | } 833 | $this->ex_mysql_query( $query, $dbh ); 834 | } 835 | } 836 | 837 | /** 838 | * Force addslashes() for the escapes. 839 | * 840 | * HyperDB makes connections when a query is made 841 | * which is why we can't use mysql_real_escape_string() for escapes. 842 | * This is also the reason why we don't allow certain charsets. See set_charset(). 843 | */ 844 | public function _real_escape( $string ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore 845 | $escaped = addslashes( (string) $string ); 846 | if ( method_exists( get_parent_class( $this ), 'add_placeholder_escape' ) ) { 847 | $escaped = $this->add_placeholder_escape( $escaped ); 848 | } 849 | return $escaped; 850 | } 851 | 852 | /** 853 | * Disconnect and remove connection from open connections list 854 | * @param string $tdbhname 855 | */ 856 | public function disconnect( $dbhname ) { 857 | $k = array_search( $dbhname, $this->open_connections ); 858 | if ( false !== $k ) { 859 | unset( $this->open_connections[ $k ] ); 860 | } 861 | 862 | foreach ( array_keys( $this->db_connections ) as $i ) { 863 | if ( $this->db_connections[ $i ]['dbhname'] == $dbhname ) { 864 | unset( $this->db_connections[ $i ] ); 865 | } 866 | } 867 | 868 | if ( $this->is_mysql_connection( $this->dbhs[ $dbhname ] ) ) { 869 | $this->ex_mysql_close( $this->dbhs[ $dbhname ] ); 870 | } 871 | 872 | unset( $this->dbhs[ $dbhname ] ); 873 | } 874 | 875 | /** 876 | * Kill cached query results 877 | */ 878 | public function flush() { 879 | $this->last_error = ''; 880 | $this->last_errno = 0; 881 | $this->num_rows = 0; 882 | parent::flush(); 883 | } 884 | 885 | /** 886 | * Basic query. See docs for more details. 887 | * @param string $query 888 | * @return int number of rows 889 | */ 890 | public function query( $query ) { 891 | // some queries are made before the plugins have been loaded, and thus cannot be filtered with this method 892 | if ( function_exists( 'apply_filters' ) ) { 893 | $query = apply_filters( 'query', $query ); 894 | } 895 | 896 | // initialise return 897 | $return_val = 0; 898 | $this->flush(); 899 | 900 | // Log how the function was called 901 | $this->func_call = "\$db->query(\"$query\")"; 902 | 903 | // Keep track of the last query for debug.. 904 | $this->last_query = $query; 905 | 906 | if ( preg_match( '/^\s*SELECT\s+FOUND_ROWS(\s*)/i', $query ) ) { 907 | if ( $this->is_mysql_result( $this->last_found_rows_result ) ) { 908 | $this->result = $this->last_found_rows_result; 909 | $this->last_found_rows_result = null; 910 | $elapsed = 0; 911 | } else { 912 | $this->print_error( 'Attempted SELECT FOUND_ROWS() without prior SQL_CALC_FOUND_ROWS.' ); 913 | return false; 914 | } 915 | } else { 916 | $this->dbh = $this->db_connect( $query ); 917 | 918 | if ( ! $this->is_mysql_connection( $this->dbh ) ) { 919 | $this->check_current_query = true; 920 | $this->last_error = 'Database connection failed'; 921 | $this->num_failed_queries++; 922 | 923 | if ( function_exists( 'do_action' ) ) { 924 | do_action( 'sql_query_log', $query, false, $this->last_error ); 925 | } 926 | 927 | return false; 928 | } 929 | 930 | $query_comment = $this->run_callbacks( 'get_query_comment', $query ); 931 | if ( ! empty( $query_comment ) ) { 932 | $query = rtrim( $query, ";\t \n\r" ) . ' /* ' . $query_comment . ' */'; 933 | } 934 | 935 | // If we're writing to the database, make sure the query will write safely. 936 | if ( $this->check_current_query && method_exists( $this, 'check_ascii' ) && ! $this->check_ascii( $query ) ) { 937 | $stripped_query = $this->strip_invalid_text_from_query( $query ); 938 | if ( $stripped_query !== $query ) { 939 | $this->insert_id = 0; 940 | $this->last_error = 'Invalid query'; 941 | $this->num_failed_queries++; 942 | 943 | if ( function_exists( 'do_action' ) ) { 944 | do_action( 'sql_query_log', $query, false, $this->last_error ); 945 | } 946 | 947 | return false; 948 | } 949 | } 950 | 951 | $this->check_current_query = true; 952 | 953 | // Inject setup and teardown statements 954 | $statement_before_query = $this->run_callbacks( 'statement_before_query' ); 955 | $statement_after_query = $this->run_callbacks( 'statement_after_query' ); 956 | $query_for_log = $query; 957 | 958 | $this->timer_start(); 959 | if ( $statement_before_query ) { 960 | $query_for_log = "$statement_before_query; $query_for_log"; 961 | $this->ex_mysql_query( $statement_before_query, $this->dbh ); 962 | } 963 | 964 | $this->result = $this->ex_mysql_query( $query, $this->dbh ); 965 | $this->last_error = $this->ex_mysql_error( $this->dbh ); 966 | 967 | if ( $statement_after_query ) { 968 | $query_for_log = "$query_for_log; $statement_after_query"; 969 | $this->ex_mysql_query( $statement_after_query, $this->dbh ); 970 | } 971 | $elapsed = $this->timer_stop(); 972 | ++$this->num_queries; 973 | 974 | if ( preg_match( '/^\s*SELECT\s+SQL_CALC_FOUND_ROWS\s/i', $query ) && false !== $this->result ) { 975 | if ( false === strpos( $query, 'NO_SELECT_FOUND_ROWS' ) ) { 976 | $this->timer_start(); 977 | $this->last_found_rows_result = $this->ex_mysql_query( 'SELECT FOUND_ROWS()', $this->dbh ); 978 | $elapsed += $this->timer_stop(); 979 | $this->last_error = $this->ex_mysql_error( $this->dbh ); 980 | ++$this->num_queries; 981 | $query .= '; SELECT FOUND_ROWS()'; 982 | } 983 | } else { 984 | $this->last_found_rows_result = null; 985 | } 986 | 987 | $this->dbhname_heartbeats[ $this->dbhname ]['last_used'] = microtime( true ); 988 | 989 | if ( $this->save_queries ) { 990 | if ( is_callable( $this->save_query_callback ) ) { 991 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace 992 | $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 ) ); 993 | if ( null !== $saved_query ) { 994 | $this->queries[] = $saved_query; 995 | } 996 | } else { 997 | $this->queries[] = array( $query_for_log, $elapsed, $this->get_caller() ); 998 | } 999 | } 1000 | } 1001 | 1002 | if ( $this->last_error ) { 1003 | $this->last_errno = $this->ex_mysql_errno( $this->dbh ); 1004 | $this->dbhname_heartbeats[ $this->dbhname ]['last_errno'] = $this->last_errno; 1005 | 1006 | // Write failed, use fallback master connection 1007 | if ( HYPERDB_ER_OPTION_PREVENTS_STATEMENT == $this->last_errno && 1008 | false !== strpos( $this->last_error, 'read-only' ) && 1009 | count( $this->unique_servers[ $this->dbhname ] ) > 1 && 1010 | ! $this->is_server_marked_read_only() ) { 1011 | // Stay away from this server 1012 | $this->disconnect( $this->dbhname ); 1013 | $this->mark_server_read_only(); 1014 | 1015 | $msg = "Can't write to $this->dbhname - server switched to read-only mode. Retrying on configured fallback connection."; 1016 | $this->print_error( $msg ); 1017 | 1018 | return $this->query( $query ); 1019 | } 1020 | 1021 | $this->print_error( $this->last_error ); 1022 | $this->num_failed_queries++; 1023 | 1024 | if ( function_exists( 'do_action' ) ) { 1025 | do_action( 'sql_query_log', $query, false, $this->last_error ); 1026 | } 1027 | 1028 | return false; 1029 | } 1030 | 1031 | if ( preg_match( '/^\s*(insert|delete|update|replace|alter)\s/i', $query ) ) { 1032 | $this->rows_affected = $this->ex_mysql_affected_rows( $this->dbh ); 1033 | 1034 | if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { 1035 | $this->insert_id = $this->ex_mysql_insert_id( $this->dbh ); 1036 | } 1037 | // Return number of rows affected 1038 | $return_val = $this->rows_affected; 1039 | } elseif ( is_bool( $this->result ) ) { 1040 | $return_val = $this->result; 1041 | $this->result = null; 1042 | } else { 1043 | $i = 0; 1044 | $this->col_info = array(); 1045 | while ( $i < $this->ex_mysql_num_fields( $this->result ) ) { 1046 | $this->col_info[ $i ] = $this->ex_mysql_fetch_field( $this->result ); 1047 | $i++; 1048 | } 1049 | $num_rows = 0; 1050 | $this->last_result = array(); 1051 | // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition 1052 | while ( ( $row = $this->ex_mysql_fetch_object( $this->result ) ) ) { 1053 | $this->last_result[ $num_rows ] = $row; 1054 | $num_rows++; 1055 | } 1056 | 1057 | $this->ex_mysql_free_result( $this->result ); 1058 | $this->result = null; 1059 | 1060 | // Log number of rows the query returned 1061 | $this->num_rows = $num_rows; 1062 | 1063 | // Return number of rows selected 1064 | $return_val = $this->num_rows; 1065 | } 1066 | 1067 | if ( function_exists( 'do_action' ) ) { 1068 | do_action( 'sql_query_log', $query, $return_val, $this->last_error ); 1069 | } 1070 | 1071 | return $return_val; 1072 | } 1073 | 1074 | /** 1075 | * Whether or not MySQL database is at least the required minimum version. 1076 | * The additional argument allows the caller to check a specific database. 1077 | * 1078 | * @since 2.5.0 1079 | * @uses $wp_version 1080 | * 1081 | * @return WP_Error 1082 | */ 1083 | public function check_database_version( $dbh_or_table = false ) { 1084 | global $wp_version; 1085 | // Make sure the server has MySQL 4.1.2 1086 | $mysql_version = preg_replace( '|[^0-9\.]|', '', $this->db_version( $dbh_or_table ) ); 1087 | if ( version_compare( $mysql_version, '4.1.2', '<' ) ) { 1088 | // translators: 1 - WordPress version 1089 | return new WP_Error( 'database_version', sprintf( __( 'ERROR: WordPress %s requires MySQL 4.1.2 or higher' ), $wp_version ) ); 1090 | } 1091 | } 1092 | 1093 | /** 1094 | * This function is called when WordPress is generating the table schema to determine wether or not the current database 1095 | * supports or needs the collation statements. 1096 | * The additional argument allows the caller to check a specific database. 1097 | * @return bool 1098 | */ 1099 | public function supports_collation( $dbh_or_table = false ) { 1100 | return $this->has_cap( 'collation', $dbh_or_table ); 1101 | } 1102 | 1103 | /** 1104 | * Generic function to determine if a database supports a particular feature 1105 | * The additional argument allows the caller to check a specific database. 1106 | * @param string $db_cap the feature 1107 | * @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) 1108 | * @return bool 1109 | */ 1110 | public function has_cap( $db_cap, $dbh_or_table = false ) { 1111 | $version = $this->db_version( $dbh_or_table ); 1112 | 1113 | switch ( strtolower( $db_cap ) ) : 1114 | case 'collation': 1115 | case 'group_concat': 1116 | case 'subqueries': 1117 | return version_compare( $version, '4.1', '>=' ); 1118 | case 'set_charset': 1119 | return version_compare( $version, '5.0.7', '>=' ); 1120 | case 'utf8mb4': // @since WP 4.1.0 1121 | if ( version_compare( $version, '5.5.3', '<' ) ) { 1122 | return false; 1123 | } 1124 | if ( $this->use_mysqli ) { 1125 | $client_version = mysqli_get_client_info(); 1126 | } else { 1127 | $client_version = mysql_get_client_info(); 1128 | } 1129 | 1130 | /* 1131 | * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server. 1132 | * mysqlnd has supported utf8mb4 since 5.0.9. 1133 | */ 1134 | if ( false !== strpos( $client_version, 'mysqlnd' ) ) { 1135 | $client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version ); 1136 | return version_compare( $client_version, '5.0.9', '>=' ); 1137 | } else { 1138 | return version_compare( $client_version, '5.5.3', '>=' ); 1139 | } 1140 | case 'utf8mb4_520': // since WP 4.6 1141 | return $this->has_cap( 'utf8mb4', $dbh_or_table ) && version_compare( $version, '5.6', '>=' ); 1142 | endswitch; 1143 | 1144 | return false; 1145 | } 1146 | 1147 | /** 1148 | * The database version number 1149 | * @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) 1150 | * @return false|string false on failure, version number on success 1151 | */ 1152 | public function db_version( $dbh_or_table = false ) { 1153 | $server_info = $this->db_server_info( $dbh_or_table ); 1154 | return $server_info ? preg_replace( '/[^0-9.].*/', '', $server_info ) : false; 1155 | } 1156 | 1157 | /** 1158 | * Retrieves full database server information 1159 | * @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) 1160 | * @return string|false Server info on success, false on failure 1161 | */ 1162 | public function db_server_info( $dbh_or_table = false ) { 1163 | if ( ! $dbh_or_table && $this->dbh ) { 1164 | $dbh =& $this->dbh; 1165 | } elseif ( $this->is_mysql_connection( $dbh_or_table ) ) { 1166 | $dbh =& $dbh_or_table; 1167 | } else { 1168 | $dbh = $this->db_connect( "SELECT FROM $dbh_or_table $this->users" ); 1169 | } 1170 | 1171 | return $dbh ? $this->ex_mysql_get_server_info( $dbh ) : false; 1172 | } 1173 | 1174 | /** 1175 | * Get the name of the function that called wpdb. 1176 | * @return string the name of the calling function 1177 | */ 1178 | public function get_caller() { 1179 | // requires PHP 4.3+ 1180 | if ( ! is_callable( 'debug_backtrace' ) ) { 1181 | return ''; 1182 | } 1183 | 1184 | $hyper_callbacks = array(); 1185 | foreach ( $this->hyper_callbacks as $group_callbacks ) { 1186 | $hyper_callbacks = array_merge( $hyper_callbacks, $group_callbacks ); 1187 | } 1188 | 1189 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace 1190 | $bt = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); 1191 | $caller = ''; 1192 | 1193 | foreach ( (array) $bt as $trace ) { 1194 | if ( isset( $trace['class'] ) && is_a( $this, $trace['class'] ) ) { 1195 | continue; 1196 | } elseif ( ! isset( $trace['function'] ) ) { 1197 | continue; 1198 | } elseif ( strtolower( $trace['function'] ) == 'call_user_func_array' ) { 1199 | continue; 1200 | } elseif ( strtolower( $trace['function'] ) == 'apply_filters' ) { 1201 | continue; 1202 | } elseif ( strtolower( $trace['function'] ) == 'do_action' ) { 1203 | continue; 1204 | } 1205 | 1206 | if ( in_array( strtolower( $trace['function'] ), $hyper_callbacks ) ) { 1207 | continue; 1208 | } 1209 | 1210 | if ( isset( $trace['class'] ) ) { 1211 | $caller = $trace['class'] . '::' . $trace['function']; 1212 | } else { 1213 | $caller = $trace['function']; 1214 | } 1215 | break; 1216 | } 1217 | return $caller; 1218 | } 1219 | 1220 | public function log_and_bail( $msg ) { 1221 | $logged = $this->run_callbacks( 'log_and_bail', $msg ); 1222 | 1223 | if ( ! $logged ) { 1224 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 1225 | error_log( "WordPress database error $msg for query {$this->last_query} made by " . $this->get_caller() ); 1226 | } 1227 | 1228 | return $this->bail( $msg ); 1229 | } 1230 | 1231 | /** 1232 | * Check the responsiveness of a tcp/ip daemon 1233 | * @return (string) 'up' when $host:$post responds within $float_timeout seconds, 1234 | * otherwise a string with details about the failure. 1235 | */ 1236 | public function check_tcp_responsiveness( $host, $port, $float_timeout ) { 1237 | if ( function_exists( 'apcu_store' ) ) { 1238 | $use_apc = true; 1239 | $apcu_key = "tcp_responsive_{$host}{$port}"; 1240 | $apcu_ttl = 10; 1241 | } else { 1242 | $use_apc = false; 1243 | } 1244 | 1245 | if ( $use_apc ) { 1246 | $server_state = apcu_fetch( $apcu_key ); 1247 | if ( $server_state ) { 1248 | return $server_state; 1249 | } 1250 | } 1251 | 1252 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fsockopen 1253 | $socket = @ fsockopen( $host, $port, $errno, $errstr, $float_timeout ); 1254 | if ( false === $socket ) { 1255 | $server_state = "down [ > $float_timeout ] ($errno) '$errstr'"; 1256 | if ( $use_apc ) { 1257 | apcu_store( $apcu_key, $server_state, $apcu_ttl ); 1258 | } 1259 | 1260 | return $server_state; 1261 | } 1262 | 1263 | fclose( $socket ); 1264 | 1265 | if ( $use_apc ) { 1266 | apcu_store( $apcu_key, 'up', $apcu_ttl ); 1267 | } 1268 | 1269 | return 'up'; 1270 | } 1271 | 1272 | public function get_server_state( $host, $port, $dbhname, $timeout ) { 1273 | // We still do the check_tcp_responsiveness() until we have 1274 | // mysql connect function with less than 1 second timeout 1275 | if ( $this->check_tcp_responsiveness && $timeout > 0 ) { 1276 | $server_state = $this->check_tcp_responsiveness( $host, $port, $timeout ); 1277 | if ( 'up' !== $server_state ) { 1278 | return $server_state; 1279 | } 1280 | } 1281 | 1282 | if ( ! empty( $this->servers_state[ "$host:$port" ] ) ) { 1283 | return $this->servers_state[ "$host:$port" ]; 1284 | } 1285 | 1286 | if ( ! function_exists( 'apcu_store' ) ) { 1287 | return 'up'; 1288 | } 1289 | 1290 | $server_state = apcu_fetch( "server_state_$host$port" ); 1291 | if ( ! $server_state ) { 1292 | return 'up'; 1293 | } 1294 | 1295 | return $this->servers_state[ "$host:$port" ] = $server_state; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found 1296 | } 1297 | 1298 | public function mark_server_as_down( $host, $port, $apcu_ttl = 10 ) { 1299 | $this->servers_state[ "$host:$port" ] = 'down'; 1300 | 1301 | if ( ! function_exists( 'apcu_store' ) ) { 1302 | return; 1303 | } 1304 | 1305 | apcu_add( "server_state_$host$port", 'down', $apcu_ttl ); 1306 | } 1307 | 1308 | // Signal to other PHP workers that a server can not take writes 1309 | public function mark_server_read_only( $host = null, $port = null ) { 1310 | $host_key = $host ? "$host:$port" : $this->current_host; 1311 | 1312 | $this->read_only_servers[ $host_key ] = true; 1313 | 1314 | if ( ! function_exists( 'apcu_store' ) ) { 1315 | return; 1316 | } 1317 | 1318 | apcu_add( "server_readonly_$host_key", 'read_only', 120 ); 1319 | } 1320 | 1321 | public function is_server_marked_read_only( $host = null, $port = null ) { 1322 | $host_key = $host ? "$host:$port" : $this->current_host; 1323 | 1324 | if ( ! empty( $this->read_only_servers[ $host_key ] ) ) { 1325 | return true; 1326 | } 1327 | 1328 | if ( ! function_exists( 'apcu_store' ) ) { 1329 | return false; 1330 | } 1331 | 1332 | if ( 'read_only' !== apcu_fetch( "server_readonly_$host_key" ) ) //phpcs:ignore Generic.ControlStructures.InlineControlStructure.NotAllowed 1333 | return false; 1334 | 1335 | return $this->read_only_servers[ $host_key ] = true; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found 1336 | } 1337 | 1338 | public function set_connect_timeout( $tag, $use_master, $tries_remaining ) { 1339 | static $default_connect_timeout; 1340 | 1341 | if ( ! isset( $default_connect_timeout ) ) { 1342 | $default_connect_timeout = $this->ex_mysql_connect_timeout(); 1343 | } 1344 | 1345 | switch ( $tag ) { 1346 | case 'pre_connect': 1347 | if ( ! $use_master && $tries_remaining ) { 1348 | $this->ex_mysql_connect_timeout( 1 ); 1349 | } 1350 | break; 1351 | case 'post_connect': 1352 | default: 1353 | if ( ! $use_master && $tries_remaining ) { 1354 | $this->ex_mysql_connect_timeout( $default_connect_timeout ); 1355 | } 1356 | break; 1357 | } 1358 | } 1359 | 1360 | public function get_lag_cache() { 1361 | $this->lag = $this->run_callbacks( 'get_lag_cache' ); 1362 | 1363 | return $this->check_lag(); 1364 | } 1365 | 1366 | public function get_lag() { 1367 | $this->lag = $this->run_callbacks( 'get_lag' ); 1368 | 1369 | return $this->check_lag(); 1370 | } 1371 | 1372 | public function check_lag() { 1373 | if ( false === $this->lag ) { 1374 | return HYPERDB_LAG_UNKNOWN; 1375 | } 1376 | 1377 | if ( $this->lag > $this->lag_threshold ) { 1378 | return HYPERDB_LAG_BEHIND; 1379 | } 1380 | 1381 | return HYPERDB_LAG_OK; 1382 | } 1383 | 1384 | public function should_use_mysqli() { 1385 | if ( ! function_exists( 'mysqli_connect' ) ) { 1386 | return false; 1387 | } 1388 | 1389 | if ( defined( 'WP_USE_EXT_MYSQL' ) && WP_USE_EXT_MYSQL ) { 1390 | return false; 1391 | } 1392 | 1393 | return true; 1394 | } 1395 | 1396 | public function should_mysql_ping() { 1397 | // Shouldn't happen 1398 | if ( ! isset( $this->dbhname_heartbeats[ $this->dbhname ] ) ) { 1399 | return true; 1400 | } 1401 | 1402 | // MySQL server has gone away 1403 | if ( isset( $this->dbhname_heartbeats[ $this->dbhname ]['last_errno'] ) && 1404 | HYPERDB_SERVER_GONE_ERROR == $this->dbhname_heartbeats[ $this->dbhname ]['last_errno'] ) { 1405 | unset( $this->dbhname_heartbeats[ $this->dbhname ]['last_errno'] ); 1406 | return true; 1407 | } 1408 | 1409 | // More than 0.1 seconds of inactivity on that dbhname 1410 | if ( microtime( true ) - $this->dbhname_heartbeats[ $this->dbhname ]['last_used'] > 0.1 ) { 1411 | return true; 1412 | } 1413 | 1414 | return false; 1415 | } 1416 | 1417 | public function is_mysql_connection( $dbh ) { 1418 | if ( ! $this->use_mysqli ) { 1419 | return is_resource( $dbh ); 1420 | } 1421 | 1422 | return $dbh instanceof mysqli; 1423 | } 1424 | 1425 | public function is_mysql_result( $result ) { 1426 | if ( ! $this->use_mysqli ) { 1427 | return is_resource( $result ); 1428 | } 1429 | 1430 | return $result instanceof mysqli_result; 1431 | } 1432 | 1433 | public function is_mysql_set_charset_callable() { 1434 | if ( ! $this->use_mysqli ) { 1435 | return function_exists( 'mysql_set_charset' ); 1436 | } 1437 | 1438 | return function_exists( 'mysqli_set_charset' ); 1439 | } 1440 | 1441 | // MySQL execution functions. 1442 | // They perform the appropriate calls based on whether we use MySQLi. 1443 | 1444 | public function ex_mysql_query( $query, $dbh ) { 1445 | if ( ! $this->use_mysqli ) { 1446 | return mysql_query( $query, $dbh ); 1447 | } 1448 | 1449 | return mysqli_query( $dbh, $query ); 1450 | } 1451 | 1452 | public function ex_mysql_unbuffered_query( $query, $dbh ) { 1453 | if ( ! $this->use_mysqli ) { 1454 | return mysql_unbuffered_query( $query, $dbh ); 1455 | } 1456 | 1457 | return mysqli_query( $dbh, $query, MYSQLI_USE_RESULT ); 1458 | } 1459 | 1460 | public function ex_mysql_connect( $db_host, $db_user, $db_password, $persistent ) { 1461 | $client_flags = defined( 'MYSQL_CLIENT_FLAGS' ) ? MYSQL_CLIENT_FLAGS : 0; 1462 | 1463 | if ( ! $this->use_mysqli ) { 1464 | $connect_function = $persistent ? 'mysql_pconnect' : 'mysql_connect'; 1465 | return @$connect_function( $db_host, $db_user, $db_password, true ); 1466 | } 1467 | 1468 | $dbh = mysqli_init(); 1469 | 1470 | // mysqli_real_connect doesn't support the host param including a port or socket 1471 | // like mysql_connect does. This duplicates how mysql_connect detects a port and/or socket file. 1472 | $port = null; 1473 | $socket = null; 1474 | $port_or_socket = strstr( $db_host, ':' ); 1475 | if ( ! empty( $port_or_socket ) ) { 1476 | $db_host = substr( $db_host, 0, strpos( $db_host, ':' ) ); 1477 | $port_or_socket = substr( $port_or_socket, 1 ); 1478 | if ( 0 !== strpos( $port_or_socket, '/' ) ) { 1479 | $port = intval( $port_or_socket ); 1480 | $maybe_socket = strstr( $port_or_socket, ':' ); 1481 | if ( ! empty( $maybe_socket ) ) { 1482 | $socket = substr( $maybe_socket, 1 ); 1483 | } 1484 | } else { 1485 | $socket = $port_or_socket; 1486 | } 1487 | } 1488 | 1489 | if ( $persistent ) { 1490 | $db_host = "p:{$db_host}"; 1491 | } 1492 | 1493 | $retval = mysqli_real_connect( $dbh, $db_host, $db_user, $db_password, null, $port, $socket, $client_flags ); 1494 | 1495 | if ( ! $retval || $dbh->connect_errno ) { 1496 | return false; 1497 | } 1498 | 1499 | return $dbh; 1500 | } 1501 | 1502 | public function ex_mysql_select_db( $db_name, $dbh ) { 1503 | if ( ! $this->use_mysqli ) { 1504 | return @mysql_select_db( $db_name, $dbh ); 1505 | } 1506 | 1507 | return @mysqli_select_db( $dbh, $db_name ); 1508 | } 1509 | 1510 | public function ex_mysql_close( $dbh ) { 1511 | if ( ! $this->use_mysqli ) { 1512 | return mysql_close( $dbh ); 1513 | } 1514 | 1515 | return mysqli_close( $dbh ); 1516 | } 1517 | 1518 | public function ex_mysql_set_charset( $charset, $dbh ) { 1519 | if ( ! $this->use_mysqli ) { 1520 | return mysql_set_charset( $charset, $dbh ); 1521 | } 1522 | 1523 | return mysqli_set_charset( $dbh, $charset ); 1524 | } 1525 | 1526 | public function ex_mysql_errno( $dbh = null ) { 1527 | if ( ! $this->use_mysqli ) { 1528 | return is_resource( $dbh ) ? mysql_errno( $dbh ) : mysql_errno(); 1529 | } 1530 | 1531 | if ( is_null( $dbh ) ) { 1532 | return mysqli_connect_errno(); 1533 | } 1534 | 1535 | return mysqli_errno( $dbh ); 1536 | } 1537 | 1538 | public function ex_mysql_error( $dbh = null ) { 1539 | if ( ! $this->use_mysqli ) { 1540 | return is_resource( $dbh ) ? mysql_error( $dbh ) : mysql_error(); 1541 | } 1542 | 1543 | if ( is_null( $dbh ) ) { 1544 | return mysqli_connect_error(); 1545 | } 1546 | 1547 | if ( ! $this->is_mysql_connection( $dbh ) ) { 1548 | return false; 1549 | } 1550 | 1551 | return mysqli_error( $dbh ); 1552 | } 1553 | 1554 | public function ex_mysql_ping( $dbh ) { 1555 | if ( ! $this->use_mysqli ) { 1556 | return @mysql_ping( $dbh ); 1557 | } 1558 | 1559 | return @mysqli_ping( $dbh ); 1560 | } 1561 | 1562 | public function ex_mysql_affected_rows( $dbh ) { 1563 | if ( ! $this->use_mysqli ) { 1564 | return mysql_affected_rows( $dbh ); 1565 | } 1566 | 1567 | return mysqli_affected_rows( $dbh ); 1568 | } 1569 | 1570 | public function ex_mysql_insert_id( $dbh ) { 1571 | if ( ! $this->use_mysqli ) { 1572 | return mysql_insert_id( $dbh ); 1573 | } 1574 | 1575 | return mysqli_insert_id( $dbh ); 1576 | } 1577 | 1578 | public function ex_mysql_num_fields( $result ) { 1579 | if ( ! $this->use_mysqli ) { 1580 | return @mysql_num_fields( $result ); 1581 | } 1582 | 1583 | return @mysqli_num_fields( $result ); 1584 | } 1585 | 1586 | public function ex_mysql_fetch_field( $result ) { 1587 | if ( ! $this->use_mysqli ) { 1588 | return @mysql_fetch_field( $result ); 1589 | } 1590 | 1591 | return @mysqli_fetch_field( $result ); 1592 | } 1593 | 1594 | public function ex_mysql_fetch_assoc( $result ) { 1595 | if ( ! $this->use_mysqli ) { 1596 | return mysql_fetch_assoc( $result ); 1597 | } 1598 | 1599 | if ( ! $this->is_mysql_result( $result ) ) { 1600 | return false; 1601 | } 1602 | 1603 | $object = mysqli_fetch_assoc( $result ); 1604 | 1605 | return ! is_null( $object ) ? $object : false; 1606 | } 1607 | 1608 | public function ex_mysql_fetch_object( $result ) { 1609 | if ( ! $this->use_mysqli ) { 1610 | return @mysql_fetch_object( $result ); 1611 | } 1612 | 1613 | if ( ! $this->is_mysql_result( $result ) ) { 1614 | return false; 1615 | } 1616 | 1617 | $object = @mysqli_fetch_object( $result ); 1618 | 1619 | return ! is_null( $object ) ? $object : false; 1620 | } 1621 | 1622 | public function ex_mysql_fetch_row( $result ) { 1623 | if ( ! $this->use_mysqli ) { 1624 | return mysql_fetch_row( $result ); 1625 | } 1626 | 1627 | if ( ! $this->is_mysql_result( $result ) ) { 1628 | return false; 1629 | } 1630 | 1631 | $row = mysqli_fetch_row( $result ); 1632 | 1633 | return ! is_null( $row ) ? $row : false; 1634 | 1635 | } 1636 | 1637 | public function ex_mysql_num_rows( $result ) { 1638 | if ( ! $this->use_mysqli ) { 1639 | return mysql_num_rows( $result ); 1640 | } 1641 | 1642 | return mysqli_num_rows( $result ); 1643 | } 1644 | 1645 | public function ex_mysql_free_result( $result ) { 1646 | if ( ! $this->use_mysqli ) { 1647 | @mysql_free_result( $result ); 1648 | } 1649 | 1650 | @mysqli_free_result( $result ); 1651 | } 1652 | 1653 | public function ex_mysql_get_server_info( $dbh ) { 1654 | if ( ! $this->use_mysqli ) { 1655 | return mysql_get_server_info( $dbh ); 1656 | } 1657 | 1658 | return mysqli_get_server_info( $dbh ); 1659 | } 1660 | 1661 | public function ex_mysql_connect_timeout( $timeout = null ) { 1662 | if ( is_null( $timeout ) ) { 1663 | if ( ! $this->use_mysqli ) { 1664 | return ini_get( 'mysql.connect_timeout' ); 1665 | } 1666 | 1667 | return ini_get( 'default_socket_timeout' ); 1668 | } 1669 | 1670 | if ( ! $this->use_mysqli ) { 1671 | // phpcs:ignore WordPress.PHP.IniSet.Risky 1672 | return ini_set( 'mysql.connect_timeout', $timeout ); 1673 | } 1674 | 1675 | // phpcs:ignore WordPress.PHP.IniSet.Risky 1676 | return ini_set( 'default_socket_timeout', $timeout ); 1677 | } 1678 | // Helper functions for configuration 1679 | 1680 | } // class hyperdb 1681 | 1682 | // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 1683 | $wpdb = new hyperdb(); 1684 | 1685 | /** @psalm-suppress UnresolvableInclude */ 1686 | require DB_CONFIG_FILE; 1687 | --------------------------------------------------------------------------------