PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.5.4
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.5.4
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / includes / class-wp-optimize-database-information.php

class-wp-optimize-database-information.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.5.4, at includes/class-wp-optimize-database-information.php

737 lines 19.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('No direct access allowed');
4
5 class WP_Optimize_Database_Information {
6
7 const UNKNOWN_DB = 'unknown';
8 const MARIA_DB = 'MariaDB';
9 const PERCONA_DB = 'Percona';
10 // for some reason coding standard parser give error here WordPress.DB.RestrictedFunctions.mysql_mysql_db
11 const MYSQL_DB = 'MysqlDB';
12
13 const MYISAM_ENGINE = 'MyISAM';
14 const MEMORY_ENGINE = 'Memory';
15 const INNODB_ENGINE = 'InnoDB';
16 const ARCHIVE_ENGINE = 'ARCHIVE';
17 const CSV_ENGINE = 'CSV';
18 const NDB_ENGINE = 'NDB';
19 const ARIA_ENGINE = 'Aria'; // MariaDB
20 const VIEW = 'VIEW';
21
22 /**
23 * Returns singleton instance object
24 *
25 * @return WP_Optimize_Database_Information Returns `WP_Optimize_Database_Information` object
26 */
27 public static function instance() {
28 static $_instance = null;
29 if (null === $_instance) {
30 $_instance = new self();
31 }
32 return $_instance;
33 }
34
35 /**
36 * Returns server type MySQL or MariaDB if mysql database or Unknown if not mysql.
37 *
38 * @return string
39 */
40 public function get_server_type() {
41 global $wpdb;
42 static $server_type = null;
43
44 if (!$wpdb->is_mysql) return self::UNKNOWN_DB;
45
46 if (null !== $server_type) return $server_type;
47
48 $server_type = self::MYSQL_DB;
49
50 $variables = $wpdb->get_results('SHOW SESSION VARIABLES LIKE "version%"');
51
52 if (!empty($variables)) {
53 foreach ($variables as $variable) {
54 if (preg_match('/mariadb/i', $variable->Value)) {
55 $server_type = self::MARIA_DB;
56 }
57 if (preg_match('/percona/i', $variable->Value)) {
58 $server_type = self::PERCONA_DB;
59 }
60 }
61 }
62
63 return $server_type;
64 }
65
66 /**
67 * Returns database server version
68 *
69 * @return string|bool
70 */
71 public function get_version() {
72 $version = $this->get_option_value('version');
73
74 if (!empty($version)) {
75 if (preg_match('/^(\d+)(\.\d+)+/', $version, $match)) {
76 return $match[0];
77 }
78 }
79
80 return false;
81 }
82
83 /**
84 * Return table type by $table_name.
85 *
86 * @param string $table_name Database table name.
87 * @return string|boolean - returns false upon failure
88 */
89 public function get_table_type($table_name) {
90 $table_info = $this->get_table_status($table_name);
91
92 if ($table_info) {
93 if (!$table_info->Engine && $this->is_view($table_name)) return self::VIEW;
94
95 return $table_info->Engine;
96 }
97
98 return false;
99 }
100
101 /**
102 * Returns information about database table.
103 *
104 * @param string $table_name
105 * @param bool $update if true, then force request to database and don't use cached values.
106 * @return mixed
107 */
108 public function get_table_status($table_name, $update = false) {
109 $tables_info = $this->get_show_table_status($update, $table_name);
110
111 foreach ($tables_info as $table_info) {
112 if ($table_name === $table_info->Name) return $table_info;
113 }
114
115 return false;
116 }
117
118 /**
119 * Returns result for query SHOW TABLE STATUS.
120 *
121 * @param bool $update refresh or no cached data
122 * @return array|object|stdClass[]|null
123 */
124 public function get_show_table_status($update = false, $table_name = '') {
125 global $wpdb;
126 static $tables_info = array();
127 static $fetched_all_tables = false;
128
129 if ($fetched_all_tables && !$update) {
130 return $tables_info;
131 }
132
133 // If a table name is provided, and the whole record hasn't been fetched yet, only fetch the information for the current table.
134 // This allows for a big performance gain when using WP-CLI or doing single optimizations.
135 if ($table_name && !$fetched_all_tables) {
136 $tables_info = $wpdb->get_results($wpdb->prepare("SHOW TABLE STATUS LIKE %s", $table_name));
137 } else {
138 if ($update || empty($tables_info) || !is_array($tables_info) || !$fetched_all_tables) {
139 $tables_info = $wpdb->get_results('SHOW TABLE STATUS');
140 $fetched_all_tables = true;
141 foreach ($tables_info as $i => $table) {
142 $rows_count = get_transient('wpo_' . $table->Name . '_count');
143 if (false === $rows_count) break;
144 $tables_info[$i]->Rows = $rows_count;
145 }
146 }
147 }
148
149 // If option innodb_file_per_table is disabled then Data_free column will have summary overhead value for all table.
150 if (!empty($tables_info)) {
151 foreach ($tables_info as $i => $table) {
152 if (self::INNODB_ENGINE == $table->Engine && !$this->is_option_enabled('innodb_file_per_table')) {
153 $tables_info[$i]->Data_free = 0;
154 }
155 }
156 }
157
158 return $tables_info;
159 }
160
161 /**
162 * Whether a table exists
163 *
164 * @param string $table_name Name of the table
165 * @param bool $use_default_prefix Whether to use default prefix
166 * @return boolean
167 */
168 public function table_exists($table_name, $use_default_prefix = true) {
169 global $wpdb;
170 return null !== $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($use_default_prefix ? $wpdb->prefix.$table_name : $table_name)));
171 }
172
173 /**
174 * Returns result for query SHOW FULL TABLES as associative array [table_name] => table_type.
175 *
176 * @return array
177 */
178 public function get_show_full_tables() {
179 global $wpdb;
180
181 static $tables_info = array();
182
183 if (empty($tables_info) || !is_array($tables_info)) {
184 $_tables_info = $wpdb->get_results('SHOW FULL TABLES', ARRAY_N);
185
186 if (!empty($_tables_info)) {
187 foreach ($_tables_info as $row) {
188 $tables_info[$row[0]] = $row[1];
189 }
190 }
191 }
192
193 return $tables_info;
194 }
195
196 /**
197 * Checks if table is a VIEW.
198 *
199 * @param string $table_name
200 * @return bool
201 */
202 public function is_view($table_name) {
203 $tables_info = $this->get_show_full_tables();
204
205 if (!array_key_exists($table_name, $tables_info)) return false;
206
207 return ('VIEW' === $tables_info[$table_name]);
208 }
209
210 /**
211 * Returns true if DDL supported.
212 *
213 * @return bool
214 */
215 public function has_online_ddl() {
216 if (self::MYSQL_DB === $this->get_server_type()) {
217 if (version_compare($this->get_version(), '5.7', '>=')) {
218 return true;
219 } else {
220 return false;
221 }
222 } elseif (self::MARIA_DB === $this->get_server_type()) {
223 if (version_compare($this->get_version(), '10.0.0', '>=')) {
224 return true;
225 } else {
226 return false;
227 }
228 }
229
230 return false;
231 }
232
233 /**
234 * Returns database option variable
235 *
236 * @param string $option_name Name of database option.
237 * @return mixed|null
238 */
239 public function get_option_value($option_name) {
240 global $wpdb;
241 static $options = array();
242
243 if (array_key_exists($option_name, $options)) return $options[$option_name];
244
245 $option = $wpdb->get_row(
246 $wpdb->prepare('SHOW SESSION VARIABLES LIKE %s', $option_name)
247 );
248
249 if (!empty($option)) {
250 $options[$option_name] = $option->Value;
251 return $option->Value;
252 }
253
254 return null;
255 }
256
257 /**
258 * Returns true if database option $option_name
259 *
260 * @param string $option_name Name of database option name.
261 * @return bool
262 */
263 public function is_option_enabled($option_name) {
264 $option_value = $this->get_option_value($option_name);
265
266 return 'ON' === strtoupper($option_value);
267 }
268
269 /**
270 * Returns true if table $table_name is optimizable
271 *
272 * @param string $table_name Name of database table
273 * @return bool
274 */
275 public function is_table_optimizable($table_name) {
276 $server_type = $this->get_server_type();
277 $server_version = $this->get_version();
278 $table_type = $this->get_table_type($table_name);
279
280 // return true if table is MyISAM.
281 if (self::MYISAM_ENGINE === $table_type) return true;
282
283 // return true if table is Archive or Aria.
284 if (self::ARCHIVE_ENGINE === $table_type || self::ARIA_ENGINE === $table_type) return true;
285
286 // if InnoDB then check if we can optimize.
287 if (self::INNODB_ENGINE === $table_type) {
288 // check for MysqlDB.
289 if (self::MYSQL_DB === $server_type && $this->has_online_ddl()) {
290 return true;
291 }
292
293 // check for MariaDB.
294 if (self::MARIA_DB === $server_type) {
295 // if innodb_file_per_table enabled or version not older than 10.1.1 and innodb_defragment enabled.
296 if ($this->is_option_enabled('innodb_file_per_table') || (version_compare($server_version, '10.1.1', '>=') && $this->is_option_enabled('innodb_defragment'))) {
297 return true;
298 }
299 }
300 }
301
302 // otherwise return false.
303 return false;
304 }
305
306 /**
307 * Returns true if table type is supported for optimization.
308 *
309 * @param string $table_name Name of database table
310 * @return bool
311 */
312 public function is_table_type_optimize_supported($table_name) {
313 $table_type = $this->get_table_type($table_name);
314
315 $supported_table_types = array(
316 self::MYISAM_ENGINE,
317 self::INNODB_ENGINE,
318 self::ARCHIVE_ENGINE,
319 self::ARIA_ENGINE,
320 );
321
322 return in_array($table_type, $supported_table_types);
323 }
324
325 /**
326 * Returns true if table type is supported for repair.
327 *
328 * @param string $table_name
329 * @return bool
330 */
331 public function is_table_type_repair_supported($table_name) {
332 $table_type = $this->get_table_type($table_name);
333
334 $supported_table_types = array(
335 self::MYISAM_ENGINE,
336 self::ARCHIVE_ENGINE,
337 self::CSV_ENGINE,
338 );
339
340 return in_array($table_type, $supported_table_types);
341 }
342
343 /**
344 * Run CHECK TABLE query and returns statuses for single or list of tables.
345 *
346 * @param array|string $table
347 * @return array
348 */
349 public function check_table($table) {
350 global $wpdb;
351
352 if (is_array($table)) {
353 $table = join('`,`', $table);
354 }
355
356 $result = array();
357
358 if (empty($table)) return $result;
359
360 $query_result = $wpdb->get_results('CHECK TABLE `'. esc_sql($table).'`;');
361
362 if (empty($query_result)) return $result;
363
364 foreach ($query_result as $row) {
365 $table_name_parts = explode('.', rtrim($row->Table, ' .'));
366 $table_name = array_pop($table_name_parts);
367
368 if (!array_key_exists($table_name, $result)) {
369 $result[$table_name] = array(
370 'status' => '',
371 'corrupted' => false,
372 );
373 }
374
375 if ('error' === $row->Msg_type) {
376 $result[$table_name]['status'] = $row->Msg_type;
377
378 if (preg_match('/corrupt/i', $row->Msg_text)) {
379 $result[$table_name]['corrupted'] = true;
380 } else {
381 $result[$table_name]['message'] = $row->Msg_text;
382 }
383 }
384
385 if ('status' === $row->Msg_type) {
386 $result[$table_name]['status'] = $row->Msg_text;
387 }
388 }
389
390 return $result;
391 }
392
393 /**
394 * Check all supported for repair tables and return statuses for them.
395 *
396 * @return array
397 */
398 public function check_all_tables() {
399 static $result = null;
400
401 if (null !== $result) return $result;
402
403 $tables = $this->get_show_table_status();
404 $supported_tables = array();
405
406 foreach ($tables as $table) {
407 if ('' === $table->Engine || $this->is_table_type_repair_supported($table->Name)) {
408 $supported_tables[] = $table->Name;
409 }
410 }
411
412 $result = $this->check_table($supported_tables);
413
414 return $result;
415 }
416
417 /**
418 * Returns true if table needing repair.
419 *
420 * @param string $table_name Database table name.
421 */
422 public function is_table_needing_repair($table_name) {
423 $table_statuses = $this->check_all_tables();
424
425 if (!$this->is_table_type_repair_supported($table_name)) return false;
426
427 return (array_key_exists($table_name, $table_statuses) && $table_statuses[$table_name]['corrupted']);
428 }
429
430 /**
431 * Check if $table using by any of installed plugins.
432 *
433 * @param string $table
434 * @return bool
435 */
436 public function is_table_using_by_plugin($table) {
437 $plugin_names = $this->get_table_plugin($table);
438
439 // if we can't determine which plugin use $table then return true.
440 if (!$plugin_names) {
441 return true;
442 }
443
444 // is WordPress core table or using by any of installed plugins then return true.
445 foreach ($plugin_names as $plugin_name) {
446 // $plugin_name is a translatable string, so we compare it with a translated string
447 if (__('WordPress core', 'wp-optimize') === $plugin_name || in_array($plugin_name, $this->get_all_installed_plugins())) {
448 return true;
449 }
450 }
451
452 return false;
453 }
454
455 /**
456 * Get blog_id
457 *
458 * @param string $table_name
459 * @return int
460 */
461 public function get_table_blog_id($table_name) {
462 global $wpdb;
463
464 if (is_multisite()) {
465 $blogs_ids = wp_list_pluck(WP_Optimize()->get_sites(), 'blog_id');
466
467 // if match with base_prefix_(number)_
468 if (preg_match('/^'.$wpdb->base_prefix.'(\d+)_/', $table_name, $match)) {
469 // check if matched number in available sites.
470 if (in_array($match[1], $blogs_ids)) return absint($match[1]);
471 }
472 }
473
474 return 1;
475 }
476
477 /**
478 * Get information about relations between tables and plugins. [ 'table' => ['plugin1', 'plugin2', ...], ... ].
479 *
480 * @return array
481 */
482 private function get_all_plugin_tables_relationship() {
483 static $plugin_tables = array();
484
485 if (!empty($plugin_tables)) return $plugin_tables;
486
487 $wp_core_tables = array(
488 'blogs',
489 'blog_versions',
490 'commentmeta',
491 'comments',
492 'links',
493 'options',
494 'postmeta',
495 'posts',
496 'registration_log',
497 'signups',
498 'term_relationships',
499 'term_taxonomy',
500 'termmeta',
501 'terms',
502 'usermeta',
503 'users',
504 'site',
505 'sitemeta',
506 );
507
508 $plugin_tables_json_file = $this->get_plugin_json_file_path();
509 $fallback_plugin_tables_json_file = WPO_PLUGIN_MAIN_PATH.'plugin.json';
510
511 if (is_file($plugin_tables_json_file) && is_readable($plugin_tables_json_file)) {
512 // get data from plugin.json file.
513 $plugin_tables = json_decode(file_get_contents($plugin_tables_json_file), true);
514 }
515
516 // Fallback to the bundled version if the list is empty
517 if (empty($plugin_tables)) {
518 if (is_file($fallback_plugin_tables_json_file) && is_readable($fallback_plugin_tables_json_file)) {
519 // get data from the bundled plugin.json file.
520 $plugin_tables = json_decode(file_get_contents($fallback_plugin_tables_json_file), true);
521 }
522 }
523
524 foreach ($wp_core_tables as $table) {
525 $plugin_tables[$table][] = __('WordPress core', 'wp-optimize');
526 }
527
528 // add WP-Optimize tables.
529 $wpo = 'wp-optimize';
530 $plugin_tables['tm_taskmeta'] = $plugin_tables['tm_taskmeta'] ?? array();
531 $plugin_tables['tm_tasks'] = $plugin_tables['tm_tasks'] ?? array();
532
533 if (!in_array($wpo, $plugin_tables['tm_taskmeta'], true)) {
534 $plugin_tables['tm_taskmeta'][] = $wpo;
535 }
536
537 if (!in_array($wpo, $plugin_tables['tm_tasks'], true)) {
538 $plugin_tables['tm_tasks'][] = $wpo;
539 }
540
541 $plugin_tables = array_merge($plugin_tables, $this->get_learndash_tables());
542
543 return $plugin_tables;
544 }
545
546 /**
547 * Try to get plugin name by table name and return it or return false if plugin is not defined.
548 *
549 * @param string $table
550 * @return array|bool - array with plugin slugs or false.
551 */
552 public function get_table_plugin($table) {
553 global $wpdb;
554
555 $plugins = array();
556 $original_table = $table;
557 // delete table prefix.
558 $table = preg_replace('/^'.$wpdb->prefix.'([0-9]+_)?/', '', $table);
559 $plugins_tables = $this->get_all_plugin_tables_relationship();
560
561 // If a direct match exists.
562 if (array_key_exists($table, $plugins_tables)) {
563 $plugins = $plugins_tables[$table];
564 }
565
566 // special handling for tables when plugins use custom prefix.
567 foreach ($plugins_tables as $plugin_table => $plugin_names) {
568 if (false === strpos($plugin_table, '*')) {
569 continue;
570 }
571
572 // support for wildcard * in table names.
573 $pattern = '/^'.str_replace('\*', '.*', preg_quote($plugin_table, '/')).'$/';
574
575 if (preg_match($pattern, $original_table)) {
576 $plugins = array_merge($plugins, $plugin_names);
577 }
578 }
579
580 if (!empty($plugins)) {
581 return array_unique($plugins);
582 }
583
584 return false;
585 }
586
587 /**
588 * Get the path where the updated plugin.json is stored
589 *
590 * @return string
591 */
592 private function get_plugin_json_file_path() {
593 $uploads_dir = wp_upload_dir(null, false);
594 $filtered_path = apply_filters('wpo_get_plugin_json_file_path', trailingslashit($uploads_dir['basedir']).'wpo/wpo-plugins-tables-list.json');
595 return is_string($filtered_path) ? $filtered_path : '';
596 }
597
598 /**
599 * Get all installed plugin slugs.
600 *
601 * @return array
602 */
603 public function get_all_installed_plugins() {
604 static $installed_plugins;
605
606 if (is_array($installed_plugins)) return $installed_plugins;
607
608 $installed_plugins = array();
609
610 if (!function_exists('get_plugins')) include_once(ABSPATH.'wp-admin/includes/plugin.php');
611
612 $plugins = get_plugins();
613
614 foreach ($plugins as $plugin_file => $plugin_data) {
615 if ('' !== $plugin_data['TextDomain']) {
616 $installed_plugins[] = $plugin_data['TextDomain'];
617 } else {
618 $plugin_file_parts = explode('/', $plugin_file);
619 $installed_plugins[]= $plugin_file_parts[0];
620 }
621 }
622
623 return $installed_plugins;
624 }
625
626 /**
627 * Check current plugin status installed/not installed and active/inactive.
628 *
629 * @param string $plugin
630 * @return array - ['installed' => true|false, 'active' => true|false]
631 */
632 public function get_plugin_status($plugin) {
633
634 if (!function_exists('get_plugins')) include_once(ABSPATH.'wp-admin/includes/plugin.php');
635 $plugins = get_plugins();
636
637 // return true for wp-optimize without checking.
638 if ('wp-optimize' === $plugin) {
639 return array(
640 'installed' => true,
641 'active' => true,
642 );
643 }
644
645 $installed = false;
646 $active = false;
647
648 foreach ($plugins as $plugin_file => $plugin_data) {
649 $plugin_file_parts = explode('/', $plugin_file);
650 $plugin_slug = $plugin_file_parts[0] ?? '';
651
652 if ($plugin === $plugin_slug) {
653 $installed = true;
654 $active = is_plugin_active($plugin_file);
655 }
656 }
657
658 return array(
659 'installed' => $installed,
660 'active' => $active,
661 );
662 }
663
664 /**
665 * Update list in plugin.json, if necessary
666 *
667 * @return void
668 */
669 public function update_plugin_json() {
670 // Add the possibility to turn this off.
671 if (!apply_filters('wpo_update_plugin_json', true)) return;
672
673 $update_request = wp_remote_get('https://plugins.svn.wordpress.org/wp-optimize/trunk/plugin.json', array('timeout' => 3000));
674 if (200 !== wp_remote_retrieve_response_code($update_request)) return;
675 $json_content = wp_remote_retrieve_body($update_request);
676 if (json_decode($json_content)) {
677 // phpcs:disable
678 // WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- WP_Filesystem not available this early
679 // Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
680 if (false === @file_put_contents($this->get_plugin_json_file_path(), $json_content)) {
681 error_log("WP-Optimize: wpo-plugins-tables-list.json couldn't be updated");
682 return;
683 }
684 // phpcs:enable
685 $this->change_plugin_json_permissions();
686 }
687 }
688
689 /**
690 * Set permissions to 640 for plugin.json
691 *
692 * @return void
693 */
694 public function change_plugin_json_permissions() {
695 $plugin_json_file_path = $this->get_plugin_json_file_path();
696 // phpcs:disable
697 // Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
698 if (is_file($plugin_json_file_path)) {
699 @chmod($plugin_json_file_path, 0640);
700 }
701 // phpcs:enable
702 }
703
704 /**
705 * Cache all table rows count
706 */
707 public function wpo_update_record_count() {
708 global $wpdb;
709 $tables_info = $wpdb->get_results('SHOW TABLE STATUS');
710 foreach ($tables_info as $table) {
711 $rows_count = $wpdb->get_var("SELECT COUNT(*) FROM `" . esc_sql($table->Name) . "`");
712 set_transient('wpo_' . $table->Name . '_count', $rows_count, 24*60*60);
713 }
714 }
715
716 /**
717 * Get a list of LearnDash tables with plugin slug for each of them.
718 *
719 * @return array
720 */
721 private function get_learndash_tables() {
722 $table_names = array(
723 '*pro_quiz_category',
724 '*pro_quiz_form',
725 '*pro_quiz_lock',
726 '*pro_quiz_prerequisite',
727 '*pro_quiz_question',
728 '*pro_quiz_statistic',
729 '*pro_quiz_statistic_ref',
730 '*pro_quiz_template',
731 '*pro_quiz_toplist',
732 );
733
734 return array_fill_keys($table_names, array('sfwd-lms'));
735 }
736 }
737