├── VERSION ├── WP_VERSION ├── .gitignore ├── config ├── bash │ └── .bashrc ├── php-fpm │ └── uploads.ini └── hyperdb │ ├── db-config.php │ └── db.php ├── docker-compose.yml ├── Dockerfile ├── Makefile ├── docker-entrypoint2.sh └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.6 2 | -------------------------------------------------------------------------------- /WP_VERSION: -------------------------------------------------------------------------------- 1 | php8.1-fpm-alpine 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docker-compose-*.yml 2 | /volume 3 | /backup -------------------------------------------------------------------------------- /config/bash/.bashrc: -------------------------------------------------------------------------------- 1 | alias ll='ls -al' 2 | alias wp='wp --allow-root' 3 | -------------------------------------------------------------------------------- /config/php-fpm/uploads.ini: -------------------------------------------------------------------------------- 1 | file_uploads = On 2 | memory_limit = 256M 3 | upload_max_filesize = 256M 4 | post_max_size = 300M 5 | max_execution_time = 600 -------------------------------------------------------------------------------- /config/hyperdb/db-config.php: -------------------------------------------------------------------------------- 1 | add_database( array( 7 | 'host' => DB_HOST, // If port is other than 3306, use host:port 8 | 'user' => DB_USER, 9 | 'password' => DB_PASSWORD, 10 | 'name' => DB_NAME, 11 | 'write' => 1, // master server takes write queries 12 | 'read' => 0, // ... and read queries 13 | ) ); 14 | 15 | /** 16 | * Register replica database server if it's available in this environment 17 | */ 18 | if ( ! empty( $_ENV['REPLICA_DB_HOST'] ) ) { 19 | $wpdb->add_database(array( 20 | 'host' => $_ENV['REPLICA_DB_HOST'] . ':' . $_ENV['REPLICA_DB_PORT'], 21 | 'user' => $_ENV['REPLICA_DB_USER'], 22 | 'password' => $_ENV['REPLICA_DB_PASSWORD'], 23 | 'name' => $_ENV['REPLICA_DB_NAME'], 24 | 'write' => 0, // replica doesn't take write queries 25 | 'read' => 1, // ... but it does take read queries 26 | )); 27 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | nginx: 6 | image: emcniece/nginx-cache-purge-wp:0.0.2 7 | ports: 8 | - 8080:80 9 | links: 10 | - wp-fpm 11 | volumes: 12 | - wproot:/var/www/html 13 | 14 | wp-fpm: 15 | tty: true 16 | stdin_open: true 17 | image: emcniece/wordpress:0.0.6 18 | volumes: 19 | - wproot:/var/www/html 20 | environment: 21 | WORDPRESS_DB_HOST: mysql 22 | WORDPRESS_DB_USER: wordpress 23 | WORDPRESS_DB_PASSWORD: wordpress 24 | WPFPM_WP_REDIS_HOST: redis 25 | WPFPM_RT_WP_NGINX_HELPER_CACHE_PATH: "/tmp/cache" 26 | #WP_PLUGINS: "nginx-helper redis-cache" 27 | #ENABLE_HYPERDB: "true" 28 | #ENABLE_CRON: "true" 29 | #CRON_CMD: "*/1 * * * * /usr/bin/curl -silent -A 'Mozilla' 'https://google.com' > /dev/null 2>&1" 30 | 31 | mysql: 32 | image: mariadb 33 | command: mysqld --innodb-buffer-pool-size=20M 34 | environment: 35 | MYSQL_RANDOM_ROOT_PASSWORD: 'yes' 36 | MYSQL_USER: wordpress 37 | MYSQL_DATABASE: wordpress 38 | MYSQL_PASSWORD: wordpress 39 | 40 | redis: 41 | image: redis:3-alpine 42 | 43 | volumes: 44 | wproot: -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM wordpress:php8.1-fpm-alpine 2 | 3 | RUN apk --no-cache add openssl imagemagick perl 4 | 5 | ENV PHPREDIS_VERSION=5.3.7 \ 6 | CONFIG_VAR_FLAG=WPFPM_ \ 7 | PAGER=more \ 8 | WP_PLUGINS="nginx-helper redis-cache" \ 9 | ENABLE_HYPERDB=false \ 10 | ENABLE_CRON=false 11 | 12 | RUN docker-php-source extract \ 13 | && curl -L -o /tmp/redis.tar.gz https://github.com/phpredis/phpredis/archive/$PHPREDIS_VERSION.tar.gz \ 14 | && tar xfz /tmp/redis.tar.gz \ 15 | && mv phpredis-$PHPREDIS_VERSION /usr/src/php/ext/redis \ 16 | && docker-php-ext-install redis \ 17 | && docker-php-source delete \ 18 | && curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \ 19 | && chmod +x wp-cli.phar \ 20 | && mv wp-cli.phar /usr/local/bin/wp \ 21 | && rm -r /tmp/* 22 | 23 | # PHP-FPM Upload limit increase 24 | ADD config/php-fpm/uploads.ini /usr/local/etc/php/conf.d/ 25 | 26 | # HyperDB drop-in for master-slave rw config 27 | ADD config/hyperdb/ /var/www/config/hyperdb/ 28 | 29 | # Shell nice-to-haves 30 | ADD config/bash/.bashrc /root 31 | 32 | # Inherit & override default entrypoint 33 | COPY docker-entrypoint2.sh /usr/local/bin/ 34 | ENTRYPOINT ["docker-entrypoint2.sh"] 35 | CMD ["php-fpm"] 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Creates a base image for emcniece/wordpress 2 | 3 | NAMESPACE := emcniece 4 | PROJECT := wordpress 5 | PLATFORM := linux 6 | ARCH := amd64 7 | DOCKER_IMAGE := $(NAMESPACE)/$(PROJECT) 8 | 9 | VERSION := $(shell cat VERSION) 10 | WP_VERSION := $(shell cat WP_VERSION) 11 | 12 | all: help 13 | 14 | help: 15 | @echo "---" 16 | @echo "IMAGE: $(DOCKER_IMAGE)" 17 | @echo "VERSION: $(VERSION)" 18 | @echo "---" 19 | @echo "make image - compile Docker image" 20 | @echo "make run-debug - run container with tail" 21 | @echo "make docker - push to Docker repository" 22 | @echo "make release - push to latest tag Docker repository" 23 | 24 | image: 25 | docker build -t $(DOCKER_IMAGE):$(VERSION) \ 26 | -f Dockerfile . 27 | 28 | run-debug: 29 | docker run -d $(DOCKER_IMAGE):$(VERSION) tail -f /dev/null 30 | 31 | run: 32 | docker run -d $(DOCKER_IMAGE):$(VERSION) 33 | 34 | docker: 35 | @echo "Pushing $(DOCKER_IMAGE):$(VERSION)" 36 | docker push $(DOCKER_IMAGE):$(VERSION) 37 | 38 | release: docker 39 | @echo "Pushing $(DOCKER_IMAGE):latest" 40 | docker tag $(DOCKER_IMAGE):$(VERSION) $(DOCKER_IMAGE):latest 41 | docker tag $(DOCKER_IMAGE):$(VERSION) $(DOCKER_IMAGE):$(WP_VERSION) 42 | docker push $(DOCKER_IMAGE):latest 43 | docker push $(DOCKER_IMAGE):$(VERSION) 44 | docker push $(DOCKER_IMAGE):$(WP_VERSION) 45 | 46 | clean: 47 | docker rmi $(DOCKER_IMAGE):$(WP_VERSION) 48 | docker rmi $(DOCKER_IMAGE):$(VERSION) 49 | docker rmi $(DOCKER_IMAGE):latest 50 | -------------------------------------------------------------------------------- /docker-entrypoint2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Remove exec from original entrypoint so we can continue here 5 | sed -i -e 's/exec/\# exec/g' /usr/local/bin/docker-entrypoint.sh 6 | 7 | # Normal setup 8 | /bin/bash /usr/local/bin/docker-entrypoint.sh "$1" 9 | 10 | # Generate vars for wp-config.php injection 11 | echo "Generating PHP Defines from ENV..." 12 | DEFINES=$(awk -v pat="$CONFIG_VAR_FLAG" 'END { 13 | print "// Generated by docker-entrypoint2.sh:"; 14 | 15 | for (name in ENVIRON) { 16 | if ( name ~ pat ) { 17 | print "define(\"" substr(name, length(pat)+1) "\", \"" ENVIRON[name] "\");" 18 | } 19 | } 20 | 21 | print " " 22 | }' < /dev/null) 23 | echo "$DEFINES" 24 | 25 | echo "Adding Defines to wp-config.php..." 26 | 27 | # Remove previously-injected vars 28 | sed '/\/\/ENTRYPOINT_START/,/\/\/ENTRYPOINT_END/d' wp-config.php > wp-config.tmp 29 | 30 | # Add current vars 31 | awk '/^\/\*.*stop editing.*\*\/$/ && c == 0 { c = 1; system("cat") } { print }' wp-config.tmp > wp-config.php < /tmp/mycron 80 | echo "$CRON_CMD" >> /tmp/mycron 81 | 82 | #install new cron file 83 | crontab /tmp/mycron 84 | echo "Cron CMD installed." 85 | 86 | rm /tmp/mycron 87 | fi 88 | 89 | # Print firstrun date/time to file 90 | date > /var/www/firstrun 91 | else 92 | echo "First run already completed, skipping configuration." 93 | fi 94 | 95 | # Set up Nginx Helper log directory 96 | mkdir -p wp-content/uploads/nginx-helper 97 | 98 | # Set usergroup for all modified files 99 | chown -R www-data:www-data /var/www/html/ 100 | 101 | if [ "$ENABLE_CRON" == "true" ]; then 102 | echo "Starting Cron daemon..." 103 | 104 | if pgrep -x "crond" > /dev/null; then 105 | echo "Running" 106 | else 107 | /usr/sbin/crond 108 | fi 109 | fi 110 | 111 | exec "$@" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # emcniece/wordpress 2 | 3 | Github: https://github.com/emcniece/docker-wordpress 4 | Docker Hub: https://hub.docker.com/r/emcniece/wordpress/ 5 | 6 | From [WordPress](https://hub.docker.com/_/wordpress/), extended with [Nginx Helper](https://en-ca.wordpress.org/plugins/nginx-helper/) and [Redis Object Cache](https://en-ca.wordpress.org/plugins/redis-cache/) support. 7 | 8 | Inherits regular setup from the [WordPress Docker image](https://hub.docker.com/_/wordpress/). 9 | 10 | **PHP-FPM only** - requires a partner Nginx container to forward traffic to port 9000. 11 | 12 | ## Installing Plugins 13 | 14 | The `WP_PLUGINS` environment variable can be set to preload plugins into the `wp-content/plugins/` directory. By default it is set to `WP_PLUGINS="nginx-helper redis-cache"` as these plugins are core to the operation of this container. Plugins will only be installed the first time the container is run. 15 | 16 | To add more plugins, modify the variable at runtime: 17 | 18 | ```sh 19 | docker run -td \ 20 | -v ./html:/var/www/html \ 21 | -e "WP_PLUGINS=nginx-helper redis-cache mailgun my-other-plugin" 22 | emcniece/wordpress:4-php7.1-fpm-alpine 23 | ``` 24 | 25 | ... or in [docker-compose.yml](./docker-compose.yml): 26 | 27 | ```yml 28 | wp-fpm: 29 | tty: true 30 | stdin_open: true 31 | image: emcniece/wordpress:latest 32 | volumes: 33 | - wproot:/var/www/html 34 | environment: 35 | WORDPRESS_DB_HOST: mysql 36 | WORDPRESS_DB_USER: wordpress 37 | WORDPRESS_DB_PASSWORD: wordpress 38 | WPFPM_WP_REDIS_HOST: redis 39 | WPFPM_RT_WP_NGINX_HELPER_CACHE_PATH: "/tmp/cache" 40 | WP_PLUGINS: "nginx-helper redis-cache mailgun my-other-plugin" 41 | ``` 42 | 43 | ## Recommended Environment 44 | 45 | Recognized environment variables: 46 | 47 | - `ENABLE_HYPERDB`: Installs HyperDB config on first run, `[true|false]` 48 | - eg. `ENABLE_HYPERDB=true` 49 | - `WP_PLUGINS`: space-separated plugin directory names, to be installed from the WP plugin marketplace 50 | - eg. `WP_PLUGINS="nginx-helper redis-cache"` 51 | - `ENABLE_CRON`: Enables `crond` daemon in background (logs to container stdout) 52 | - eg. `ENABLE_CRON=true` 53 | - `CRON_CMD`: Cron command to be added to `crontab` on startup 54 | - eg. `CRON_CMD="*/10 * * * * /usr/bin/ls -al /root"` 55 | 56 | 57 | ### Runtime ENV wp-config.php Injection 58 | 59 | Any environment variables prefixed with `WPFPM_` will be injected into `wp-config.php` during each container startup. **Warning:** this means that `wp-config.php` is regenerated each restart using the provided environment variables. 60 | 61 | The provided `docker-compose.yml` file injects 2 variables into `wp-config.php`: 62 | 63 | ```yml 64 | WPFPM_WP_REDIS_HOST: redis # Name of the Redis container 65 | WPFPM_RT_WP_NGINX_HELPER_CACHE_PATH: "/tmp/cache" # Set in emcniece/nginx-cache-purge-wp default.conf 66 | ``` 67 | 68 | ### HyperDB Configuration 69 | 70 | The HyperDB drop-in allows a replica database to be configured alongside the primary database. 71 | 72 | When `ENABLE_HYPERDB=true` the HyperDB config files will be copied into place and will override the database config provided in `wp-config.php`. The HyperDB replica database can be configured with the following environment variables: 73 | 74 | - `REPLICA_DB_HOST` 75 | - `REPLICA_DB_USER` 76 | - `REPLICA_DB_PASSWORD` 77 | - `REPLICA_DB_NAME` 78 | 79 | 80 | ### SSL Forwarding 81 | 82 | The [docker-entrypoint2.sh](./docker-entrypoint2.sh) script also injects the PHP `HTTP_X_FORWARDED_PROTO` variable to allow for an SSL certificate to be terminated upstream while maintaining data communication into the container. This is useful in infrastructure with load balancers or reverse proxies, eg: 83 | 84 | ``` 85 | Internet -> Nginx proxy w/ SSL termination -> emcniece/nginx-cache-purge-wp -> emcniece/wordpress 86 | ``` 87 | 88 | ## Quick Run 89 | 90 | If you want to use the standalone FPM (port 9000) image: 91 | 92 | ``` 93 | docker run -td \ 94 | -v ./html:/var/www/html \ 95 | emcniece/wordpress:4-php7.1-fpm-alpine 96 | ``` 97 | 98 | If you want to run a full web-accessible stack (Nginx, WP-FPM, MySQL, Redis): 99 | 100 | ```sh 101 | docker-compose up -d 102 | ``` 103 | 104 | ## To Do 105 | 106 | - [x] Imagick / LibZip PHP Extensions (courtesy of `wordpress:5-php7.2-fpm-alpine`) 107 | - [ ] HyperDB config: allow read/write to be set for both primary and replica databases 108 | - [ ] HyperDB config: replace current ENV vars with `WPFPM_` injection pattern 109 | -------------------------------------------------------------------------------- /config/hyperdb/db.php: -------------------------------------------------------------------------------- 1 | dbh) for established mysql connections 84 | * @var array 85 | */ 86 | var $dbhs; 87 | 88 | /** 89 | * The multi-dimensional array of datasets and servers 90 | * @var array 91 | */ 92 | var $hyper_servers = array(); 93 | 94 | /** 95 | * Optional directory of tables and their datasets 96 | * @var array 97 | */ 98 | var $hyper_tables = array(); 99 | 100 | /** 101 | * Optional directory of callbacks to determine datasets from queries 102 | * @var array 103 | */ 104 | var $hyper_callbacks = array(); 105 | 106 | /** 107 | * Custom callback to save debug info in $this->queries 108 | * @var callable 109 | */ 110 | var $save_query_callback = null; 111 | 112 | /** 113 | * Whether to use persistent connections 114 | * @var bool 115 | */ 116 | var $persistent = false; 117 | 118 | /** 119 | * The maximum number of db links to keep open. The least-recently used 120 | * link will be closed when the number of links exceeds this. 121 | * @var int 122 | */ 123 | var $max_connections = 10; 124 | 125 | /** 126 | * Whether to check with fsockopen prior to connecting to mysql. 127 | * @var bool 128 | */ 129 | var $check_tcp_responsiveness = true; 130 | 131 | /** 132 | * Minimum number of connections to try before bailing 133 | * @var int 134 | */ 135 | var $min_tries = 3; 136 | 137 | /** 138 | * Send Reads To Masters. This disables slave connections while true. 139 | * Otherwise it is an array of written tables. 140 | * @var array 141 | */ 142 | var $srtm = array(); 143 | 144 | /** 145 | * The log of db connections made and the time each one took 146 | * @var array 147 | */ 148 | var $db_connections; 149 | 150 | /** 151 | * The list of unclosed connections sorted by LRU 152 | */ 153 | var $open_connections = array(); 154 | 155 | /** 156 | * The last server used and the database name selected 157 | * @var array 158 | */ 159 | var $last_used_server; 160 | 161 | /** 162 | * Lookup array (dbhname => (server, db name) ) for re-selecting the db 163 | * when a link is re-used. 164 | * @var array 165 | */ 166 | var $used_servers = array(); 167 | 168 | /** 169 | * Whether to save debug_backtrace in save_query_callback. You may wish 170 | * to disable this, e.g. when tracing out-of-memory problems. 171 | */ 172 | var $save_backtrace = true; 173 | 174 | /** 175 | * Maximum lag in seconds. Set null to disable. Requires callbacks. 176 | * @var integer 177 | */ 178 | var $default_lag_threshold = null; 179 | 180 | /** 181 | * Lookup array (dbhname => host:port) 182 | * @var array 183 | */ 184 | var $dbh2host = array(); 185 | 186 | /** 187 | * Keeps track of the dbhname usage and errors. 188 | */ 189 | var $dbhname_heartbeats = array(); 190 | 191 | /** 192 | * Counter for how many queries have failed during the life of the $wpdb object 193 | */ 194 | var $num_failed_queries = 0; 195 | 196 | /** 197 | * Gets ready to make database connections 198 | * @param array db class vars 199 | */ 200 | function __construct( $args = null ) { 201 | if ( is_array($args) ) 202 | foreach ( get_class_vars(__CLASS__) as $var => $value ) 203 | if ( isset($args[$var]) ) 204 | $this->$var = $args[$var]; 205 | 206 | $this->use_mysqli = $this->should_use_mysqli(); 207 | 208 | $this->init_charset(); 209 | } 210 | 211 | /** 212 | * Triggers __construct() for backwards compatibility with PHP4 213 | */ 214 | function hyperdb( $args = null ) { 215 | return $this->__construct($args); 216 | } 217 | 218 | /** 219 | * Sets $this->charset and $this->collate 220 | */ 221 | function init_charset() { 222 | if ( function_exists('is_multisite') && is_multisite() ) { 223 | $this->charset = 'utf8'; 224 | if ( defined( 'DB_COLLATE' ) && DB_COLLATE ) 225 | $this->collate = DB_COLLATE; 226 | else 227 | $this->collate = 'utf8_general_ci'; 228 | } elseif ( defined( 'DB_COLLATE' ) ) { 229 | $this->collate = DB_COLLATE; 230 | } 231 | 232 | if ( defined( 'DB_CHARSET' ) ) 233 | $this->charset = DB_CHARSET; 234 | } 235 | 236 | /** 237 | * Add the connection parameters for a database 238 | */ 239 | function add_database( $db ) { 240 | extract($db, EXTR_SKIP); 241 | isset($dataset) or $dataset = 'global'; 242 | isset($read) or $read = 1; 243 | isset($write) or $write = 1; 244 | unset($db['dataset']); 245 | 246 | if ( $read ) 247 | $this->hyper_servers[ $dataset ][ 'read' ][ $read ][] = $db; 248 | if ( $write ) 249 | $this->hyper_servers[ $dataset ][ 'write' ][ $write ][] = $db; 250 | } 251 | 252 | /** 253 | * Specify the dateset where a table is found 254 | */ 255 | function add_table( $dataset, $table ) { 256 | $this->hyper_tables[ $table ] = $dataset; 257 | } 258 | 259 | /** 260 | * Add a callback to a group of callbacks. 261 | * The default group is 'dataset', used to examine 262 | * queries and determine dataset. 263 | */ 264 | function add_callback( $callback, $group = 'dataset' ) { 265 | $this->hyper_callbacks[ $group ][] = $callback; 266 | } 267 | 268 | /** 269 | * Find the first table name referenced in a query 270 | * @param string query 271 | * @return string table 272 | */ 273 | function get_table_from_query( $q ) { 274 | // Remove characters that can legally trail the table name 275 | $q = rtrim($q, ';/-#'); 276 | // allow (select...) union [...] style queries. Use the first queries table name. 277 | $q = ltrim($q, "\t ("); 278 | // Strip everything between parentheses except nested 279 | // selects and use only 1500 chars of the query 280 | $q = preg_replace( '/\((?!\s*select)[^(]*?\)/is', '()', substr( $q, 0, 1500 ) ); 281 | 282 | // Refer to the previous query 283 | // wpdb doesn't implement last_table, so we run it first. 284 | if ( preg_match('/^\s*SELECT.*?\s+FOUND_ROWS\(\)/is', $q) ) 285 | return $this->last_table; 286 | 287 | if( method_exists( get_parent_class( $this ), 'get_table_from_query' ) ) { 288 | // WPDB has added support for get_table_from_query, which should take precedence 289 | return parent::get_table_from_query( $q ); 290 | } 291 | 292 | // Quickly match most common queries 293 | if ( preg_match('/^\s*(?:' 294 | . 'SELECT.*?\s+FROM' 295 | . '|INSERT(?:\s+IGNORE)?(?:\s+INTO)?' 296 | . '|REPLACE(?:\s+INTO)?' 297 | . '|UPDATE(?:\s+IGNORE)?' 298 | . '|DELETE(?:\s+IGNORE)?(?:\s+FROM)?' 299 | . ')\s+`?([\w-]+)`?/is', $q, $maybe) ) 300 | return $maybe[1]; 301 | 302 | // SHOW TABLE STATUS and SHOW TABLES 303 | if ( preg_match('/^\s*(?:' 304 | . 'SHOW\s+TABLE\s+STATUS.+(?:LIKE\s+|WHERE\s+Name\s*=\s*)' 305 | . '|SHOW\s+(?:FULL\s+)?TABLES.+(?:LIKE\s+|WHERE\s+Name\s*=\s*)' 306 | . ')\W([\w-]+)\W/is', $q, $maybe) ) 307 | return $maybe[1]; 308 | 309 | // Big pattern for the rest of the table-related queries in MySQL 5.0 310 | if ( preg_match('/^\s*(?:' 311 | . '(?:EXPLAIN\s+(?:EXTENDED\s+)?)?SELECT.*?\s+FROM' 312 | . '|INSERT(?:\s+LOW_PRIORITY|\s+DELAYED|\s+HIGH_PRIORITY)?(?:\s+IGNORE)?(?:\s+INTO)?' 313 | . '|REPLACE(?:\s+LOW_PRIORITY|\s+DELAYED)?(?:\s+INTO)?' 314 | . '|UPDATE(?:\s+LOW_PRIORITY)?(?:\s+IGNORE)?' 315 | . '|DELETE(?:\s+LOW_PRIORITY|\s+QUICK|\s+IGNORE)*(?:\s+FROM)?' 316 | . '|DESCRIBE|DESC|EXPLAIN|HANDLER' 317 | . '|(?:LOCK|UNLOCK)\s+TABLE(?:S)?' 318 | . '|(?:RENAME|OPTIMIZE|BACKUP|RESTORE|CHECK|CHECKSUM|ANALYZE|OPTIMIZE|REPAIR).*\s+TABLE' 319 | . '|TRUNCATE(?:\s+TABLE)?' 320 | . '|CREATE(?:\s+TEMPORARY)?\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?' 321 | . '|ALTER(?:\s+IGNORE)?\s+TABLE' 322 | . '|DROP\s+TABLE(?:\s+IF\s+EXISTS)?' 323 | . '|CREATE(?:\s+\w+)?\s+INDEX.*\s+ON' 324 | . '|DROP\s+INDEX.*\s+ON' 325 | . '|LOAD\s+DATA.*INFILE.*INTO\s+TABLE' 326 | . '|(?:GRANT|REVOKE).*ON\s+TABLE' 327 | . '|SHOW\s+(?:.*FROM|.*TABLE)' 328 | . ')\s+`?([\w-]+)`?/is', $q, $maybe) ) 329 | return $maybe[1]; 330 | } 331 | 332 | /** 333 | * Determine the likelihood that this query could alter anything 334 | * @param string query 335 | * @return bool 336 | */ 337 | function is_write_query( $q ) { 338 | // Quick and dirty: only SELECT statements are considered read-only. 339 | $q = ltrim($q, "\r\n\t ("); 340 | return !preg_match('/^(?:SELECT|SHOW|DESCRIBE|DESC|EXPLAIN)\s/i', $q); 341 | } 342 | 343 | /** 344 | * Set a flag to prevent reading from slaves which might be lagging after a write 345 | */ 346 | function send_reads_to_masters() { 347 | $this->srtm = true; 348 | } 349 | 350 | /** 351 | * Callbacks are executed in the order in which they are registered until one 352 | * of them returns something other than null. 353 | */ 354 | function run_callbacks( $group, $args = null) { 355 | if ( ! isset( $this->hyper_callbacks[ $group ] ) || ! is_array( $this->hyper_callbacks[ $group ] ) ) 356 | return null; 357 | 358 | if ( ! isset( $args ) ) { 359 | $args = array( &$this ); 360 | } elseif ( is_array( $args ) ) { 361 | $args[] = &$this; 362 | } else { 363 | $args = array( $args, &$this ); 364 | } 365 | 366 | foreach ( $this->hyper_callbacks[ $group ] as $func ) { 367 | $result = call_user_func_array($func, $args); 368 | if ( isset($result) ) 369 | return $result; 370 | } 371 | } 372 | 373 | /** 374 | * Figure out which database server should handle the query, and connect to it. 375 | * @param string query 376 | * @return resource mysql database connection 377 | */ 378 | function db_connect( $query = '' ) { 379 | if ( empty( $query ) ) 380 | return false; 381 | 382 | $this->last_table = $this->table = $this->get_table_from_query($query); 383 | 384 | if ( isset($this->hyper_tables[$this->table]) ) { 385 | $dataset = $this->hyper_tables[$this->table]; 386 | $this->callback_result = null; 387 | } elseif ( null !== $this->callback_result = $this->run_callbacks( 'dataset', $query ) ) { 388 | if ( is_array($this->callback_result) ) 389 | extract( $this->callback_result, EXTR_OVERWRITE ); 390 | else 391 | $dataset = $this->callback_result; 392 | } 393 | 394 | if ( ! isset($dataset) ) 395 | $dataset = 'global'; 396 | 397 | if ( ! $dataset ) 398 | return $this->log_and_bail("Unable to determine dataset (for table: $this->table)"); 399 | else 400 | $this->dataset = $dataset; 401 | 402 | $this->run_callbacks( 'dataset_found', $dataset ); 403 | 404 | if ( empty( $this->hyper_servers ) ) { 405 | if ( $this->is_mysql_connection( $this->dbh ) ) 406 | return $this->dbh; 407 | if ( 408 | !defined('DB_HOST') 409 | || !defined('DB_USER') 410 | || !defined('DB_PASSWORD') 411 | || !defined('DB_NAME') ) 412 | return $this->log_and_bail("We were unable to query because there was no database defined"); 413 | $this->dbh = $this->ex_mysql_connect( DB_HOST, DB_USER, DB_PASSWORD, $this->persistent ); 414 | if ( ! $this->is_mysql_connection( $this->dbh ) ) 415 | return $this->log_and_bail("We were unable to connect to the database. (DB_HOST)"); 416 | if ( ! $this->ex_mysql_select_db( DB_NAME, $this->dbh ) ) 417 | return $this->log_and_bail("We were unable to select the database"); 418 | if ( ! empty( $this->charset ) ) { 419 | $collation_query = "SET NAMES '$this->charset'"; 420 | if ( !empty( $this->collate ) ) 421 | $collation_query .= " COLLATE '$this->collate'"; 422 | $this->ex_mysql_query( $collation_query, $this->dbh ); 423 | } 424 | return $this->dbh; 425 | } 426 | 427 | // Determine whether the query must be sent to the master (a writable server) 428 | if ( !empty( $use_master ) || $this->srtm === true || isset($this->srtm[$this->table]) ) { 429 | $use_master = true; 430 | } elseif ( $is_write = $this->is_write_query($query) ) { 431 | $use_master = true; 432 | if ( is_array($this->srtm) ) 433 | $this->srtm[$this->table] = true; 434 | } elseif ( !isset($use_master) && is_array($this->srtm) && !empty($this->srtm) ) { 435 | // Detect queries that have a join in the srtm array. 436 | $use_master = false; 437 | $query_match = substr( $query, 0, 1000 ); 438 | foreach ( $this->srtm as $key => $value ) { 439 | if ( false !== stripos( $query_match, $key ) ) { 440 | $use_master = true; 441 | break; 442 | } 443 | } 444 | } else { 445 | $use_master = false; 446 | } 447 | 448 | if ( $use_master ) { 449 | $this->dbhname = $dbhname = $dataset . '__w'; 450 | $operation = 'write'; 451 | } else { 452 | $this->dbhname = $dbhname = $dataset . '__r'; 453 | $operation = 'read'; 454 | } 455 | 456 | // Try to reuse an existing connection 457 | while ( isset( $this->dbhs[$dbhname] ) && $this->is_mysql_connection( $this->dbhs[$dbhname] ) ) { 458 | // Find the connection for incrementing counters 459 | foreach ( array_keys($this->db_connections) as $i ) 460 | if ( $this->db_connections[$i]['dbhname'] == $dbhname ) 461 | $conn =& $this->db_connections[$i]; 462 | 463 | if ( isset($server['name']) ) { 464 | $name = $server['name']; 465 | // A callback has specified a database name so it's possible the existing connection selected a different one. 466 | if ( $name != $this->used_servers[$dbhname]['name'] ) { 467 | if ( ! $this->ex_mysql_select_db( $name, $this->dbhs[$dbhname] ) ) { 468 | // this can happen when the user varies and lacks permission on the $name database 469 | if ( isset( $conn['disconnect (select failed)'] ) ) 470 | ++$conn['disconnect (select failed)']; 471 | else 472 | $conn['disconnect (select failed)'] = 1; 473 | 474 | $this->disconnect($dbhname); 475 | break; 476 | } 477 | $this->used_servers[$dbhname]['name'] = $name; 478 | } 479 | } else { 480 | $name = $this->used_servers[$dbhname]['name']; 481 | } 482 | 483 | $this->current_host = $this->dbh2host[$dbhname]; 484 | 485 | // Keep this connection at the top of the stack to prevent disconnecting frequently-used connections 486 | if ( $k = array_search($dbhname, $this->open_connections) ) { 487 | unset($this->open_connections[$k]); 488 | $this->open_connections[] = $dbhname; 489 | } 490 | 491 | $this->last_used_server = $this->used_servers[$dbhname]; 492 | $this->last_connection = compact('dbhname', 'name'); 493 | 494 | if ( $this->should_mysql_ping() && ! $this->ex_mysql_ping( $this->dbhs[$dbhname] ) ) { 495 | if ( isset( $conn['disconnect (ping failed)'] ) ) 496 | ++$conn['disconnect (ping failed)']; 497 | else 498 | $conn['disconnect (ping failed)'] = 1; 499 | 500 | $this->disconnect($dbhname); 501 | break; 502 | } 503 | 504 | if ( isset( $conn['queries'] ) ) 505 | ++$conn['queries']; 506 | else 507 | $conn['queries'] = 1; 508 | 509 | return $this->dbhs[$dbhname]; 510 | } 511 | 512 | if ( $use_master && defined( "MASTER_DB_DEAD" ) ) { 513 | 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."); 514 | } 515 | 516 | if ( empty($this->hyper_servers[$dataset][$operation]) ) 517 | return $this->log_and_bail("No databases available with $this->table ($dataset)"); 518 | 519 | // Put the groups in order by priority 520 | ksort($this->hyper_servers[$dataset][$operation]); 521 | 522 | // Make a list of at least $this->min_tries connections to try, repeating as necessary. 523 | $servers = array(); 524 | do { 525 | foreach ( $this->hyper_servers[$dataset][$operation] as $group => $items ) { 526 | $keys = array_keys($items); 527 | shuffle($keys); 528 | foreach ( $keys as $key ) 529 | $servers[] = compact('group', 'key'); 530 | } 531 | 532 | if ( !$tries_remaining = count( $servers ) ) 533 | return $this->log_and_bail("No database servers were found to match the query ($this->table, $dataset)"); 534 | 535 | if ( !isset( $unique_servers ) ) 536 | $unique_servers = $tries_remaining; 537 | 538 | } while ( $tries_remaining < $this->min_tries ); 539 | 540 | // Connect to a database server 541 | do { 542 | $unique_lagged_slaves = array(); 543 | $success = false; 544 | 545 | foreach ( $servers as $group_key ) { 546 | --$tries_remaining; 547 | 548 | // If all servers are lagged, we need to start ignoring the lag and retry 549 | if ( count( $unique_lagged_slaves ) == $unique_servers ) 550 | break; 551 | 552 | // $group, $key 553 | extract($group_key, EXTR_OVERWRITE); 554 | 555 | // $host, $user, $password, $name, $read, $write [, $lag_threshold, $timeout ] 556 | extract($this->hyper_servers[$dataset][$operation][$group][$key], EXTR_OVERWRITE); 557 | $port = null; 558 | 559 | // Split host:port into $host and $port 560 | if ( strpos($host, ':') ) 561 | list($host, $port) = explode(':', $host); 562 | 563 | // Overlay $server if it was extracted from a callback 564 | if ( isset($server) && is_array($server) ) 565 | extract($server, EXTR_OVERWRITE); 566 | 567 | // Split again in case $server had host:port 568 | if ( strpos($host, ':') ) 569 | list($host, $port) = explode(':', $host); 570 | 571 | // Make sure there's always a port number 572 | if ( empty($port) ) 573 | $port = 3306; 574 | 575 | // Use a default timeout of 200ms 576 | if ( !isset($timeout) ) 577 | $timeout = 0.2; 578 | 579 | // Get the minimum group here, in case $server rewrites it 580 | if ( !isset( $min_group ) || $min_group > $group ) 581 | $min_group = $group; 582 | 583 | // Can be used by the lag callbacks 584 | $this->lag_cache_key = "$host:$port"; 585 | $this->lag_threshold = isset($lag_threshold) ? $lag_threshold : $this->default_lag_threshold; 586 | 587 | // Check for a lagged slave, if applicable 588 | if ( !$use_master && !$write && !isset( $ignore_slave_lag ) 589 | && isset($this->lag_threshold) && !isset( $server['host'] ) 590 | && ( $lagged_status = $this->get_lag_cache() ) === HYPERDB_LAG_BEHIND 591 | ) { 592 | // If it is the last lagged slave and it is with the best preference we will ignore its lag 593 | if ( !isset( $unique_lagged_slaves["$host:$port"] ) 594 | && $unique_servers == count( $unique_lagged_slaves ) + 1 595 | && $group == $min_group ) 596 | { 597 | $this->lag_threshold = null; 598 | } else { 599 | $unique_lagged_slaves["$host:$port"] = $this->lag; 600 | continue; 601 | } 602 | } 603 | 604 | $this->timer_start(); 605 | 606 | // Connect if necessary or possible 607 | $server_state = null; 608 | if ( $use_master || ! $tries_remaining || 609 | 'up' == $server_state = $this->get_server_state( $host, $port, $timeout ) ) 610 | { 611 | $this->set_connect_timeout( 'pre_connect', $use_master, $tries_remaining ); 612 | $this->dbhs[$dbhname] = $this->ex_mysql_connect( "$host:$port", $user, $password, $this->persistent ); 613 | $this->set_connect_timeout( 'post_connect', $use_master, $tries_remaining ); 614 | } else { 615 | $this->dbhs[$dbhname] = false; 616 | } 617 | 618 | $elapsed = $this->timer_stop(); 619 | 620 | if ( $this->is_mysql_connection( $this->dbhs[$dbhname] ) ) { 621 | /** 622 | * If we care about lag, disconnect lagged slaves and try to find others. 623 | * We don't disconnect if it is the last lagged slave and it is with the best preference. 624 | */ 625 | if ( !$use_master && !$write && !isset( $ignore_slave_lag ) 626 | && isset($this->lag_threshold) && !isset( $server['host'] ) 627 | && $lagged_status !== HYPERDB_LAG_OK 628 | && ( $lagged_status = $this->get_lag() ) === HYPERDB_LAG_BEHIND 629 | && !( 630 | !isset( $unique_lagged_slaves["$host:$port"] ) 631 | && $unique_servers == count( $unique_lagged_slaves ) + 1 632 | && $group == $min_group 633 | ) 634 | ) { 635 | $success = false; 636 | $unique_lagged_slaves["$host:$port"] = $this->lag; 637 | $this->disconnect( $dbhname ); 638 | $this->dbhs[$dbhname] = false; 639 | $msg = "Replication lag of {$this->lag}s on $host:$port ($dbhname)"; 640 | $this->print_error( $msg ); 641 | continue; 642 | } elseif ( $this->ex_mysql_select_db( $name, $this->dbhs[$dbhname] ) ) { 643 | $success = true; 644 | $this->current_host = "$host:$port"; 645 | $this->dbh2host[$dbhname] = "$host:$port"; 646 | $queries = 1; 647 | $lag = isset( $this->lag ) ? $this->lag : 0; 648 | $this->last_connection = compact('dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success', 'queries', 'lag'); 649 | $this->db_connections[] = $this->last_connection; 650 | $this->open_connections[] = $dbhname; 651 | break; 652 | } 653 | } 654 | 655 | if ( 'down' == $server_state ) 656 | continue; // don't flood the logs if already down 657 | 658 | if ( HYPERDB_CONN_HOST_ERROR == $this->ex_mysql_errno() && 659 | ( 'up' == $server_state || ! $tries_remaining ) ) 660 | { 661 | $this->mark_server_as_down( $host, $port ); 662 | $server_state = 'down'; 663 | } 664 | 665 | $success = false; 666 | $this->last_connection = compact('dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success'); 667 | $this->db_connections[] = $this->last_connection; 668 | $msg = date( "Y-m-d H:i:s" ) . " Can't select $dbhname - \n"; 669 | $msg .= "'referrer' => '{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}',\n"; 670 | $msg .= "'server' => {$server},\n"; 671 | $msg .= "'host' => {$host},\n"; 672 | $msg .= "'error' => " . $this->ex_mysql_error() . ",\n"; 673 | $msg .= "'errno' => " . $this->ex_mysql_errno() . ",\n"; 674 | $msg .= "'server_state' => $server_state\n"; 675 | $msg .= "'lagged_status' => " . ( isset( $lagged_status ) ? $lagged_status : HYPERDB_LAG_UNKNOWN ); 676 | 677 | $this->print_error( $msg ); 678 | } 679 | 680 | if ( ! $success || ! isset( $this->dbhs[$dbhname] ) || ! $this->is_mysql_connection( $this->dbhs[$dbhname] ) ) { 681 | if ( !isset( $ignore_slave_lag ) && count( $unique_lagged_slaves ) ) { 682 | // Lagged slaves were not used. Ignore the lag for this connection attempt and retry. 683 | $ignore_slave_lag = true; 684 | $tries_remaining = count( $servers ); 685 | continue; 686 | } 687 | 688 | $error_details = array( 689 | 'host' => $host, 690 | 'port' => $port, 691 | 'operation' => $operation, 692 | 'table' => $this->table, 693 | 'dataset' => $dataset, 694 | 'dbhname' => $dbhname 695 | ); 696 | $this->run_callbacks( 'db_connection_error', $error_details ); 697 | 698 | return $this->bail( "Unable to connect to $host:$port to $operation table '$this->table' ($dataset)" ); 699 | } 700 | 701 | break; 702 | } while ( true ); 703 | 704 | if ( !isset( $charset ) ) 705 | $charset = null; 706 | 707 | if ( !isset( $collate ) ) 708 | $collate = null; 709 | 710 | $this->set_charset($this->dbhs[$dbhname], $charset, $collate); 711 | 712 | $this->dbh = $this->dbhs[$dbhname]; // needed by $wpdb->_real_escape() 713 | 714 | $this->last_used_server = compact('host', 'user', 'name', 'read', 'write'); 715 | 716 | $this->used_servers[$dbhname] = $this->last_used_server; 717 | 718 | while ( !$this->persistent && count($this->open_connections) > $this->max_connections ) { 719 | $oldest_connection = array_shift($this->open_connections); 720 | if ( $this->dbhs[$oldest_connection] != $this->dbhs[$dbhname] ) 721 | $this->disconnect($oldest_connection); 722 | } 723 | 724 | return $this->dbhs[$dbhname]; 725 | } 726 | 727 | /** 728 | * Sets the connection's character set. 729 | * @param resource $dbh The resource given by ex_mysql_connect 730 | * @param string $charset The character set (optional) 731 | * @param string $collate The collation (optional) 732 | */ 733 | function set_charset($dbh, $charset = null, $collate = null) { 734 | if ( !isset($charset) ) 735 | $charset = $this->charset; 736 | if ( !isset($collate) ) 737 | $collate = $this->collate; 738 | 739 | if ( ! $this->has_cap( 'collation', $dbh ) || empty( $charset ) ) 740 | return; 741 | 742 | if ( ! in_array( strtolower( $charset ), array( 'utf8', 'utf8mb4', 'latin1' ) ) ) 743 | wp_die( "$charset charset isn't supported in HyperDB for security reasons" ); 744 | 745 | if ( $this->is_mysql_set_charset_callable() && $this->has_cap( 'set_charset', $dbh ) ) { 746 | $this->ex_mysql_set_charset( $charset, $dbh ); 747 | $this->real_escape = true; 748 | } else { 749 | $query = $this->prepare( 'SET NAMES %s', $charset ); 750 | if ( ! empty( $collate ) ) 751 | $query .= $this->prepare( ' COLLATE %s', $collate ); 752 | $this->ex_mysql_query( $query, $dbh ); 753 | } 754 | } 755 | 756 | /* 757 | * Force addslashes() for the escapes. 758 | * 759 | * HyperDB makes connections when a query is made 760 | * which is why we can't use mysql_real_escape_string() for escapes. 761 | * This is also the reason why we don't allow certain charsets. See set_charset(). 762 | */ 763 | function _real_escape( $string ) { 764 | $escaped = addslashes( $string ); 765 | if ( method_exists( get_parent_class( $this ), 'add_placeholder_escape' ) ) { 766 | $escaped = $this->add_placeholder_escape( $escaped ); 767 | } 768 | return $escaped; 769 | } 770 | 771 | /** 772 | * Disconnect and remove connection from open connections list 773 | * @param string $tdbhname 774 | */ 775 | function disconnect($dbhname) { 776 | if ( false !== $k = array_search($dbhname, $this->open_connections) ) 777 | unset($this->open_connections[$k]); 778 | 779 | foreach ( array_keys( $this->db_connections ) as $i ) 780 | if ( $this->db_connections[$i]['dbhname'] == $dbhname ) 781 | unset( $this->db_connections[$i] ); 782 | 783 | if ( $this->is_mysql_connection( $this->dbhs[$dbhname] ) ) 784 | $this->ex_mysql_close( $this->dbhs[$dbhname] ); 785 | 786 | unset($this->dbhs[$dbhname]); 787 | } 788 | 789 | /** 790 | * Kill cached query results 791 | */ 792 | function flush() { 793 | $this->last_error = ''; 794 | $this->last_errno = 0; 795 | $this->num_rows = 0; 796 | parent::flush(); 797 | } 798 | 799 | /** 800 | * Basic query. See docs for more details. 801 | * @param string $query 802 | * @return int number of rows 803 | */ 804 | function query( $query ) { 805 | // some queries are made before the plugins have been loaded, and thus cannot be filtered with this method 806 | if ( function_exists('apply_filters') ) 807 | $query = apply_filters('query', $query); 808 | 809 | // initialise return 810 | $return_val = 0; 811 | $this->flush(); 812 | 813 | // Log how the function was called 814 | $this->func_call = "\$db->query(\"$query\")"; 815 | 816 | // Keep track of the last query for debug.. 817 | $this->last_query = $query; 818 | 819 | if ( preg_match( '/^\s*SELECT\s+FOUND_ROWS(\s*)/i', $query ) ) { 820 | if ( $this->is_mysql_result( $this->last_found_rows_result ) ) { 821 | $this->result = $this->last_found_rows_result; 822 | $elapsed = 0; 823 | } else { 824 | $this->print_error( "Attempted SELECT FOUND_ROWS() without prior SQL_CALC_FOUND_ROWS." ); 825 | return false; 826 | } 827 | } else { 828 | $this->dbh = $this->db_connect( $query ); 829 | 830 | if ( ! $this->is_mysql_connection( $this->dbh ) ) { 831 | $this->check_current_query = true; 832 | $this->last_error = 'Database connection failed'; 833 | $this->num_failed_queries++; 834 | do_action( 'sql_query_log', $query, false, $this->last_error ); 835 | return false; 836 | } 837 | 838 | $query_comment = $this->run_callbacks( 'get_query_comment', $query ); 839 | if ( ! empty( $query_comment ) ) 840 | $query = rtrim( $query, ";\t \n\r" ) . ' /* ' . $query_comment . ' */'; 841 | 842 | // If we're writing to the database, make sure the query will write safely. 843 | if ( $this->check_current_query && method_exists( $this, 'check_ascii' ) && ! $this->check_ascii( $query ) ) { 844 | $stripped_query = $this->strip_invalid_text_from_query( $query ); 845 | if ( $stripped_query !== $query ) { 846 | $this->insert_id = 0; 847 | $this->last_error = 'Invalid query'; 848 | $this->num_failed_queries++; 849 | do_action( 'sql_query_log', $query, false, $this->last_error ); 850 | return false; 851 | } 852 | } 853 | 854 | $this->check_current_query = true; 855 | 856 | // Inject setup and teardown statements 857 | $statement_before_query = $this->run_callbacks( 'statement_before_query' ); 858 | $statement_after_query = $this->run_callbacks( 'statement_after_query' ); 859 | $query_for_log = $query; 860 | 861 | $this->timer_start(); 862 | if ( $statement_before_query ) { 863 | $query_for_log = "$statement_before_query; $query_for_log"; 864 | $this->ex_mysql_query( $statement_before_query, $this->dbh ); 865 | } 866 | 867 | $this->result = $this->ex_mysql_query( $query, $this->dbh ); 868 | 869 | if ( $statement_after_query ) { 870 | $query_for_log = "$query_for_log; $statement_after_query"; 871 | $this->ex_mysql_query( $statement_after_query, $this->dbh ); 872 | } 873 | $elapsed = $this->timer_stop(); 874 | ++$this->num_queries; 875 | 876 | if ( preg_match('/^\s*SELECT\s+SQL_CALC_FOUND_ROWS\s/i', $query) ) { 877 | if ( false === strpos($query, "NO_SELECT_FOUND_ROWS") ) { 878 | $this->timer_start(); 879 | $this->last_found_rows_result = $this->ex_mysql_query( "SELECT FOUND_ROWS()", $this->dbh ); 880 | $elapsed += $this->timer_stop(); 881 | ++$this->num_queries; 882 | $query .= "; SELECT FOUND_ROWS()"; 883 | } 884 | } else { 885 | $this->last_found_rows_result = null; 886 | } 887 | 888 | $this->dbhname_heartbeats[$this->dbhname]['last_used'] = microtime( true ); 889 | 890 | if ( $this->save_queries ) { 891 | if ( is_callable($this->save_query_callback) ) { 892 | $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 ) ); 893 | if ( $saved_query !== null ) { 894 | $this->queries[] = $saved_query; 895 | } 896 | } else { 897 | $this->queries[] = array( $query_for_log, $elapsed, $this->get_caller() ); 898 | } 899 | } 900 | } 901 | 902 | // If there is an error then take note of it 903 | if ( $this->last_error = $this->ex_mysql_error( $this->dbh ) ) { 904 | $this->last_errno = $this->ex_mysql_errno( $this->dbh ); 905 | $this->dbhname_heartbeats[$this->dbhname]['last_errno'] = $this->last_errno; 906 | $this->print_error($this->last_error); 907 | $this->num_failed_queries++; 908 | do_action( 'sql_query_log', $query, false, $this->last_error ); 909 | return false; 910 | } 911 | 912 | if ( preg_match('/^\s*(insert|delete|update|replace|alter)\s/i',$query) ) { 913 | $this->rows_affected = $this->ex_mysql_affected_rows( $this->dbh ); 914 | 915 | // Take note of the insert_id 916 | if ( preg_match('/^\s*(insert|replace)\s/i',$query) ) { 917 | $this->insert_id = $this->ex_mysql_insert_id( $this->dbh ); 918 | } 919 | // Return number of rows affected 920 | $return_val = $this->rows_affected; 921 | } else if ( is_bool( $this->result ) ) { 922 | $return_val = $this->result; 923 | $this->result = null; 924 | } else { 925 | $i = 0; 926 | $this->col_info = array(); 927 | while ( $i < $this->ex_mysql_num_fields( $this->result ) ) { 928 | $this->col_info[$i] = $this->ex_mysql_fetch_field( $this->result ); 929 | $i++; 930 | } 931 | $num_rows = 0; 932 | $this->last_result = array(); 933 | while ( $row = $this->ex_mysql_fetch_object( $this->result ) ) { 934 | $this->last_result[$num_rows] = $row; 935 | $num_rows++; 936 | } 937 | 938 | $this->ex_mysql_free_result( $this->result ); 939 | $this->result = null; 940 | 941 | // Log number of rows the query returned 942 | $this->num_rows = $num_rows; 943 | 944 | // Return number of rows selected 945 | $return_val = $this->num_rows; 946 | } 947 | 948 | do_action( 'sql_query_log', $query, $return_val, $this->last_error ); 949 | return $return_val; 950 | } 951 | 952 | /** 953 | * Whether or not MySQL database is at least the required minimum version. 954 | * The additional argument allows the caller to check a specific database. 955 | * 956 | * @since 2.5.0 957 | * @uses $wp_version 958 | * 959 | * @return WP_Error 960 | */ 961 | function check_database_version( $dbh_or_table = false ) { 962 | global $wp_version; 963 | // Make sure the server has MySQL 4.1.2 964 | $mysql_version = preg_replace( '|[^0-9\.]|', '', $this->db_version( $dbh_or_table ) ); 965 | if ( version_compare($mysql_version, '4.1.2', '<') ) 966 | return new WP_Error( 'database_version', sprintf(__('ERROR: WordPress %s requires MySQL 4.1.2 or higher'), $wp_version) ); 967 | } 968 | 969 | /** 970 | * This function is called when WordPress is generating the table schema to determine wether or not the current database 971 | * supports or needs the collation statements. 972 | * The additional argument allows the caller to check a specific database. 973 | * @return bool 974 | */ 975 | function supports_collation( $dbh_or_table = false ) { 976 | return $this->has_cap( 'collation', $dbh_or_table ); 977 | } 978 | 979 | /** 980 | * Generic function to determine if a database supports a particular feature 981 | * The additional argument allows the caller to check a specific database. 982 | * @param string $db_cap the feature 983 | * @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) 984 | * @return bool 985 | */ 986 | function has_cap( $db_cap, $dbh_or_table = false ) { 987 | $version = $this->db_version( $dbh_or_table ); 988 | 989 | switch ( strtolower( $db_cap ) ) : 990 | case 'collation' : 991 | case 'group_concat' : 992 | case 'subqueries' : 993 | return version_compare($version, '4.1', '>='); 994 | case 'set_charset' : 995 | return version_compare($version, '5.0.7', '>='); 996 | endswitch; 997 | 998 | return false; 999 | } 1000 | 1001 | /** 1002 | * The database version number 1003 | * @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) 1004 | * @return false|string false on failure, version number on success 1005 | */ 1006 | function db_version( $dbh_or_table = false ) { 1007 | if ( !$dbh_or_table && $this->dbh ) 1008 | $dbh =& $this->dbh; 1009 | elseif ( $this->is_mysql_connection( $dbh_or_table ) ) 1010 | $dbh =& $dbh_or_table; 1011 | else 1012 | $dbh = $this->db_connect( "SELECT FROM $dbh_or_table $this->users" ); 1013 | 1014 | if ( $dbh ) 1015 | return preg_replace( '/[^0-9.].*/', '', $this->ex_mysql_get_server_info( $dbh ) ); 1016 | return false; 1017 | } 1018 | 1019 | /** 1020 | * Get the name of the function that called wpdb. 1021 | * @return string the name of the calling function 1022 | */ 1023 | function get_caller() { 1024 | // requires PHP 4.3+ 1025 | if ( !is_callable('debug_backtrace') ) 1026 | return ''; 1027 | 1028 | $hyper_callbacks = array(); 1029 | foreach ( $this->hyper_callbacks as $group_name => $group_callbacks ) 1030 | $hyper_callbacks = array_merge( $hyper_callbacks, $group_callbacks ); 1031 | 1032 | $bt = debug_backtrace( false ); 1033 | $caller = ''; 1034 | 1035 | foreach ( (array) $bt as $trace ) { 1036 | if ( isset($trace['class']) && is_a( $this, $trace['class'] ) ) 1037 | continue; 1038 | elseif ( !isset($trace['function']) ) 1039 | continue; 1040 | elseif ( strtolower($trace['function']) == 'call_user_func_array' ) 1041 | continue; 1042 | elseif ( strtolower($trace['function']) == 'apply_filters' ) 1043 | continue; 1044 | elseif ( strtolower($trace['function']) == 'do_action' ) 1045 | continue; 1046 | 1047 | if ( in_array( strtolower($trace['function']), $hyper_callbacks ) ) 1048 | continue; 1049 | 1050 | if ( isset($trace['class']) ) 1051 | $caller = $trace['class'] . '::' . $trace['function']; 1052 | else 1053 | $caller = $trace['function']; 1054 | break; 1055 | } 1056 | return $caller; 1057 | } 1058 | 1059 | function log_and_bail( $msg ) { 1060 | $logged = $this->run_callbacks( 'log_and_bail', $msg ); 1061 | 1062 | if ( ! $logged ) 1063 | error_log( "WordPress database error $msg for query {$this->last_query} made by " . $this->get_caller() ); 1064 | 1065 | return $this->bail( $msg ); 1066 | } 1067 | 1068 | /** 1069 | * Check the responsiveness of a tcp/ip daemon 1070 | * @return (string) 'up' when $host:$post responds within $float_timeout seconds, 1071 | * otherwise a string with details about the failure. 1072 | */ 1073 | function check_tcp_responsiveness( $host, $port, $float_timeout ) { 1074 | if ( function_exists( 'apcu_store' ) ) { 1075 | $use_apc = true; 1076 | $apcu_key = "tcp_responsive_{$host}{$port}"; 1077 | $apcu_ttl = 10; 1078 | } else { 1079 | $use_apc = false; 1080 | } 1081 | 1082 | if ( $use_apc ) { 1083 | $server_state = apcu_fetch( $apcu_key ); 1084 | if ( $server_state ) 1085 | return $server_state; 1086 | } 1087 | 1088 | $socket = @ fsockopen( $host, $port, $errno, $errstr, $float_timeout ); 1089 | if ( $socket === false ) { 1090 | $server_state = "down [ > $float_timeout ] ($errno) '$errstr'"; 1091 | if ( $use_apc ) 1092 | apcu_store( $apcu_key, $server_state, $apcu_ttl ); 1093 | 1094 | return $server_state; 1095 | } 1096 | 1097 | fclose( $socket ); 1098 | 1099 | if ( $use_apc ) 1100 | apcu_store( $apcu_key, 'up', $apcu_ttl ); 1101 | 1102 | return 'up'; 1103 | } 1104 | 1105 | function get_server_state( $host, $port, $timeout ) { 1106 | // We still do the check_tcp_responsiveness() until we have 1107 | // mysql connect function with less than 1 second timeout 1108 | if ( $this->check_tcp_responsiveness ) { 1109 | $server_state = $this->check_tcp_responsiveness( $host, $port, $timeout ); 1110 | if ( 'up' !== $server_state ) 1111 | return $server_state; 1112 | } 1113 | 1114 | if ( ! function_exists( 'apcu_store' ) ) 1115 | return 'up'; 1116 | 1117 | $server_state = apcu_fetch( "server_state_$host$port" ); 1118 | if ( ! $server_state ) 1119 | return 'up'; 1120 | 1121 | return $server_state; 1122 | } 1123 | 1124 | function mark_server_as_down( $host, $port, $apcu_ttl = 10 ) { 1125 | if ( ! function_exists( 'apcu_store' ) ) 1126 | return; 1127 | 1128 | apcu_add( "server_state_$host$port", 'down', $apcu_ttl ); 1129 | } 1130 | 1131 | function set_connect_timeout( $tag, $use_master, $tries_remaining ) { 1132 | static $default_connect_timeout; 1133 | 1134 | if ( ! isset ( $default_connect_timeout ) ) 1135 | $default_connect_timeout = $this->ex_mysql_connect_timeout(); 1136 | 1137 | switch ( $tag ) { 1138 | case 'pre_connect': 1139 | if ( ! $use_master && $tries_remaining ) 1140 | $this->ex_mysql_connect_timeout( 1 ); 1141 | break; 1142 | case 'post_connect': 1143 | default: 1144 | if ( ! $use_master && $tries_remaining ) 1145 | $this->ex_mysql_connect_timeout( $default_connect_timeout ); 1146 | break; 1147 | } 1148 | } 1149 | 1150 | function get_lag_cache() { 1151 | $this->lag = $this->run_callbacks( 'get_lag_cache' ); 1152 | 1153 | return $this->check_lag(); 1154 | } 1155 | 1156 | function get_lag() { 1157 | $this->lag = $this->run_callbacks( 'get_lag' ); 1158 | 1159 | return $this->check_lag(); 1160 | } 1161 | 1162 | function check_lag() { 1163 | if ( $this->lag === false ) 1164 | return HYPERDB_LAG_UNKNOWN; 1165 | 1166 | if ( $this->lag > $this->lag_threshold ) 1167 | return HYPERDB_LAG_BEHIND; 1168 | 1169 | return HYPERDB_LAG_OK; 1170 | } 1171 | 1172 | function should_use_mysqli() { 1173 | if ( ! function_exists( 'mysqli_connect' ) ) 1174 | return false; 1175 | 1176 | if ( defined( 'WP_USE_EXT_MYSQL' ) && WP_USE_EXT_MYSQL ) 1177 | return false; 1178 | 1179 | return true; 1180 | } 1181 | 1182 | function should_mysql_ping() { 1183 | // Shouldn't happen 1184 | if ( ! isset( $this->dbhname_heartbeats[$this->dbhname] ) ) 1185 | return true; 1186 | 1187 | // MySQL server has gone away 1188 | if ( isset( $this->dbhname_heartbeats[$this->dbhname]['last_errno'] ) && 1189 | HYPERDB_SERVER_GONE_ERROR == $this->dbhname_heartbeats[$this->dbhname]['last_errno'] ) 1190 | { 1191 | unset( $this->dbhname_heartbeats[$this->dbhname]['last_errno'] ); 1192 | return true; 1193 | } 1194 | 1195 | // More than 0.1 seconds of inactivity on that dbhname 1196 | if ( microtime( true ) - $this->dbhname_heartbeats[$this->dbhname]['last_used'] > 0.1 ) 1197 | return true; 1198 | 1199 | return false; 1200 | } 1201 | 1202 | function is_mysql_connection( $dbh ) { 1203 | if ( ! $this->use_mysqli ) 1204 | return is_resource( $dbh ); 1205 | 1206 | return $dbh instanceof mysqli; 1207 | } 1208 | 1209 | function is_mysql_result( $result ) { 1210 | if ( ! $this->use_mysqli ) 1211 | return is_resource( $result ); 1212 | 1213 | return $result instanceof mysqli_result; 1214 | } 1215 | 1216 | function is_mysql_set_charset_callable() { 1217 | if ( ! $this->use_mysqli ) 1218 | return function_exists( 'mysql_set_charset' ); 1219 | 1220 | return function_exists( 'mysqli_set_charset' ); 1221 | } 1222 | 1223 | // MySQL execution functions. 1224 | // They perform the appropriate calls based on whether we use MySQLi. 1225 | 1226 | function ex_mysql_query( $query, $dbh ) { 1227 | if ( ! $this->use_mysqli ) 1228 | return mysql_query( $query, $dbh ); 1229 | 1230 | return mysqli_query( $dbh, $query ); 1231 | } 1232 | 1233 | function ex_mysql_unbuffered_query( $query, $dbh ) { 1234 | if ( ! $this->use_mysqli ) 1235 | return mysql_unbuffered_query( $query, $dbh ); 1236 | 1237 | return mysqli_query( $dbh, $query, MYSQLI_USE_RESULT ); 1238 | } 1239 | 1240 | function ex_mysql_connect( $db_host, $db_user, $db_password, $persistent ) { 1241 | $client_flags = defined( 'MYSQL_CLIENT_FLAGS' ) ? MYSQL_CLIENT_FLAGS : 0; 1242 | 1243 | if ( ! $this->use_mysqli ) { 1244 | $connect_function = $persistent ? 'mysql_pconnect' : 'mysql_connect'; 1245 | return @$connect_function( $db_host, $db_user, $db_password, true ); 1246 | } 1247 | 1248 | $dbh = mysqli_init(); 1249 | 1250 | // mysqli_real_connect doesn't support the host param including a port or socket 1251 | // like mysql_connect does. This duplicates how mysql_connect detects a port and/or socket file. 1252 | $port = null; 1253 | $socket = null; 1254 | $port_or_socket = strstr( $db_host, ':' ); 1255 | if ( ! empty( $port_or_socket ) ) { 1256 | $db_host = substr( $db_host, 0, strpos( $db_host, ':' ) ); 1257 | $port_or_socket = substr( $port_or_socket, 1 ); 1258 | if ( 0 !== strpos( $port_or_socket, '/' ) ) { 1259 | $port = intval( $port_or_socket ); 1260 | $maybe_socket = strstr( $port_or_socket, ':' ); 1261 | if ( ! empty( $maybe_socket ) ) { 1262 | $socket = substr( $maybe_socket, 1 ); 1263 | } 1264 | } else { 1265 | $socket = $port_or_socket; 1266 | } 1267 | } 1268 | 1269 | if ( $persistent ) 1270 | $db_host = "p:{$db_host}"; 1271 | 1272 | $retval = mysqli_real_connect( $dbh, $db_host, $db_user, $db_password, null, $port, $socket, $client_flags ); 1273 | 1274 | if ( ! $retval || $dbh->connect_errno ) 1275 | return false; 1276 | 1277 | return $dbh; 1278 | } 1279 | 1280 | function ex_mysql_select_db( $db_name, $dbh ) { 1281 | if ( ! $this->use_mysqli ) 1282 | return @mysql_select_db( $db_name, $dbh ); 1283 | 1284 | return @mysqli_select_db( $dbh, $db_name ); 1285 | } 1286 | 1287 | function ex_mysql_close( $dbh ) { 1288 | if ( ! $this->use_mysqli ) 1289 | return mysql_close( $dbh ); 1290 | 1291 | return mysqli_close( $dbh ); 1292 | } 1293 | 1294 | function ex_mysql_set_charset( $charset, $dbh ) { 1295 | if ( ! $this->use_mysqli ) 1296 | return mysql_set_charset( $charset, $dbh ); 1297 | 1298 | return mysqli_set_charset( $dbh, $charset ); 1299 | } 1300 | 1301 | function ex_mysql_errno( $dbh = null ) { 1302 | if ( ! $this->use_mysqli ) 1303 | return is_resource( $dbh ) ? mysql_errno( $dbh ) : mysql_errno(); 1304 | 1305 | if ( is_null( $dbh ) ) 1306 | return mysqli_connect_errno(); 1307 | 1308 | return mysqli_errno( $dbh ); 1309 | } 1310 | 1311 | function ex_mysql_error( $dbh = null ) { 1312 | if ( ! $this->use_mysqli ) 1313 | return is_resource( $dbh ) ? mysql_error( $dbh ) : mysql_error(); 1314 | 1315 | if ( is_null( $dbh ) ) 1316 | return mysqli_connect_error(); 1317 | 1318 | if ( ! $this->is_mysql_connection( $dbh ) ) 1319 | return false; 1320 | 1321 | return mysqli_error( $dbh ); 1322 | } 1323 | 1324 | function ex_mysql_ping( $dbh ) { 1325 | if ( ! $this->use_mysqli ) 1326 | return @mysql_ping( $dbh ); 1327 | 1328 | return @mysqli_ping( $dbh ); 1329 | } 1330 | 1331 | function ex_mysql_affected_rows( $dbh ) { 1332 | if ( ! $this->use_mysqli ) 1333 | return mysql_affected_rows( $dbh ); 1334 | 1335 | return mysqli_affected_rows( $dbh ); 1336 | } 1337 | 1338 | function ex_mysql_insert_id( $dbh ) { 1339 | if ( ! $this->use_mysqli ) 1340 | return mysql_insert_id( $dbh ); 1341 | 1342 | return mysqli_insert_id( $dbh ); 1343 | } 1344 | 1345 | function ex_mysql_num_fields( $result ) { 1346 | if ( ! $this->use_mysqli ) 1347 | return @mysql_num_fields( $result ); 1348 | 1349 | return @mysqli_num_fields( $result ); 1350 | } 1351 | 1352 | function ex_mysql_fetch_field( $result ) { 1353 | if ( ! $this->use_mysqli ) 1354 | return @mysql_fetch_field( $result ); 1355 | 1356 | return @mysqli_fetch_field( $result ); 1357 | } 1358 | 1359 | function ex_mysql_fetch_assoc( $result ) { 1360 | if ( ! $this->use_mysqli ) 1361 | return mysql_fetch_assoc( $result ); 1362 | 1363 | if ( ! $this->is_mysql_result( $result ) ) 1364 | return false; 1365 | 1366 | $object = mysqli_fetch_assoc( $result ); 1367 | 1368 | return ! is_null( $object ) ? $object : false; 1369 | } 1370 | 1371 | function ex_mysql_fetch_object( $result ) { 1372 | if ( ! $this->use_mysqli ) 1373 | return @mysql_fetch_object( $result ); 1374 | 1375 | if ( ! $this->is_mysql_result( $result ) ) 1376 | return false; 1377 | 1378 | $object = @mysqli_fetch_object( $result ); 1379 | 1380 | return ! is_null( $object ) ? $object : false; 1381 | } 1382 | 1383 | function ex_mysql_fetch_row( $result ) { 1384 | if ( ! $this->use_mysqli ) 1385 | return mysql_fetch_row( $result ); 1386 | 1387 | if ( ! $this->is_mysql_result( $result ) ) 1388 | return false; 1389 | 1390 | $row = mysqli_fetch_row( $result ); 1391 | 1392 | return ! is_null( $row ) ? $row : false; 1393 | 1394 | } 1395 | 1396 | function ex_mysql_num_rows( $result ) { 1397 | if ( ! $this->use_mysqli ) 1398 | return mysql_num_rows( $result ); 1399 | 1400 | return mysqli_num_rows( $result ); 1401 | } 1402 | 1403 | function ex_mysql_free_result( $result ) { 1404 | if ( ! $this->use_mysqli ) 1405 | return @mysql_free_result( $result ); 1406 | 1407 | return @mysqli_free_result( $result ); 1408 | } 1409 | 1410 | function ex_mysql_get_server_info( $dbh ) { 1411 | if ( ! $this->use_mysqli ) 1412 | return mysql_get_server_info( $dbh ); 1413 | 1414 | return mysqli_get_server_info( $dbh ); 1415 | } 1416 | 1417 | function ex_mysql_connect_timeout( $timeout = null ) { 1418 | if ( is_null( $timeout ) ) { 1419 | if ( ! $this->use_mysqli ) 1420 | return ini_get( 'mysql.connect_timeout' ); 1421 | 1422 | return ini_get( 'default_socket_timeout' ); 1423 | } 1424 | 1425 | if ( ! $this->use_mysqli ) 1426 | return ini_set( 'mysql.connect_timeout', $timeout ); 1427 | 1428 | return ini_set( 'default_socket_timeout', $timeout ); 1429 | } 1430 | // Helper functions for configuration 1431 | 1432 | } // class hyperdb 1433 | 1434 | $wpdb = new hyperdb(); 1435 | 1436 | require( DB_CONFIG_FILE ); 1437 | --------------------------------------------------------------------------------