PluginProbe
WebberZone Top 10 — Popular Posts / trunk
WebberZone Top 10 — Popular Posts vtrunk
4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 1.6.2 All 116 releases
top-10 / includes / admin / class-tools-page.php

class-tools-page.php in WebberZone Top 10 — Popular Posts trunk, at includes/admin/class-tools-page.php

1,016 lines 36.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Tools Page class.
4 *
5 * @package WebberZone\Top_Ten\Admin
6 */
7
8 namespace WebberZone\Top_Ten\Admin;
9
10 use WebberZone\Top_Ten\Database;
11 use WebberZone\Top_Ten\Counter;
12 use WebberZone\Top_Ten\Admin\Activator;
13 use WebberZone\Top_Ten\Util\Hook_Registry;
14
15 if ( ! defined( 'WPINC' ) ) {
16 die;
17 }
18
19 /**
20 * Generates the Tools page.
21 *
22 * @since 3.3.0
23 */
24 class Tools_Page {
25
26 /**
27 * Parent Menu ID.
28 *
29 * @since 3.3.0
30 *
31 * @var string Parent Menu ID.
32 */
33 public $parent_id;
34
35 /**
36 * Constructor class.
37 *
38 * @since 3.3.0
39 */
40 public function __construct() {
41 Hook_Registry::add_action( 'admin_menu', array( $this, 'admin_menu' ) );
42 Hook_Registry::add_action( 'network_admin_menu', array( $this, 'network_admin_menu' ), 11 );
43 Hook_Registry::add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
44 Hook_Registry::add_action( 'admin_init', array( $this, 'handle_recreate_tables_action' ) );
45 Hook_Registry::add_action( 'admin_init', array( $this, 'handle_create_missing_tables_action' ) );
46
47 // Clear table statistics cache when counts are updated.
48 Hook_Registry::add_action( 'tptn_count_updated', array( 'WebberZone\Top_Ten\Database', 'clear_table_statistics_cache' ) );
49 Hook_Registry::add_action( 'tptn_delete_counts', array( 'WebberZone\Top_Ten\Database', 'clear_table_statistics_cache' ) );
50 Hook_Registry::add_action( 'tptn_set_count', array( 'WebberZone\Top_Ten\Database', 'clear_table_statistics_cache' ) );
51 }
52
53 /**
54 * Admin Menu.
55 *
56 * @since 3.3.0
57 */
58 public function admin_menu() {
59
60 $this->parent_id = add_submenu_page(
61 'tptn_dashboard',
62 esc_html__( 'Top 10 Tools', 'top-10' ),
63 esc_html__( 'Tools', 'top-10' ),
64 'manage_options',
65 'tptn_tools_page',
66 array( $this, 'render_page' )
67 );
68
69 add_action( 'load-' . $this->parent_id, array( $this, 'help_tabs' ) );
70 }
71
72 /**
73 * Admin Menu.
74 *
75 * @since 3.3.0
76 */
77 public function network_admin_menu() {
78
79 $this->parent_id = add_submenu_page(
80 'tptn_dashboard',
81 esc_html__( 'Top 10 Tools', 'top-10' ),
82 esc_html__( 'Tools', 'top-10' ),
83 'manage_network_options',
84 'tptn_network_tools_page',
85 array( $this, 'render_page' )
86 );
87
88 add_action( 'load-' . $this->parent_id, array( $this, 'help_tabs' ) );
89 }
90
91 /**
92 * Enqueue scripts in admin area.
93 *
94 * @since 3.3.0
95 *
96 * @param string $hook The current admin page.
97 */
98 public function admin_enqueue_scripts( $hook ) {
99 if ( $hook === $this->parent_id ) {
100 wp_enqueue_script( 'top-ten-admin-js' );
101 wp_enqueue_style( 'top-ten-admin-css' );
102 wp_enqueue_style( 'wp-spinner' );
103 wp_localize_script(
104 'top-ten-admin-js',
105 'top_ten_admin_data',
106 array(
107 'ajax_url' => admin_url( 'admin-ajax.php' ),
108 'security' => wp_create_nonce( 'tptn-admin' ),
109 'strings' => array(
110 'confirm_message' => esc_html__( 'Are you sure you want to clear the cache?', 'top-10' ),
111 'network_confirm_message' => esc_html__( 'Are you sure you want to clear the Top 10 cache across all active sites?', 'top-10' ),
112 'clearing_text' => esc_html__( 'Clearing...', 'top-10' ),
113 'fail_message' => esc_html__( 'Failed to clear cache. Please try again.', 'top-10' ),
114 'request_fail_message' => esc_html__( 'Request failed: ', 'top-10' ),
115 ),
116 )
117 );
118 }
119 }
120
121 /**
122 * Render the tools settings page.
123 *
124 * @since 2.5.0
125 *
126 * @return void
127 */
128 public function render_page() {
129 $screen = get_current_screen();
130 $network_wide = is_multisite() && is_network_admin();
131
132 // Keep the screen check as a fallback for admin screen implementations that
133 // do not expose is_network_admin() reliably.
134 if ( $screen && $screen->id === $this->parent_id . '-network' ) {
135 $network_wide = true;
136 }
137
138 /* Recreate the shared table indexes. */
139 if ( ( ! is_multisite() || $network_wide ) && isset( $_POST['tptn_recreate_primary_key'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
140 self::recreate_primary_key();
141 add_settings_error( 'tptn-notices', '', esc_html__( 'Primary Key has been recreated', 'top-10' ), 'updated' );
142 }
143
144 /* Truncate overall posts table */
145 if ( isset( $_POST['tptn_reset_overall'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
146 if ( ! is_multisite() || $network_wide ) {
147 Database::truncate_table( Database::get_table( false ) );
148 } else {
149 Counter::delete_counts( array( 'daily' => false ) );
150 }
151 Dashboard_Widgets::clear_network_dashboard_cache();
152 add_settings_error( 'tptn-notices', '', esc_html__( 'Top 10 popular posts reset', 'top-10' ), 'updated' );
153 }
154
155 /* Truncate daily posts table */
156 if ( isset( $_POST['tptn_reset_daily'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
157 if ( ! is_multisite() || $network_wide ) {
158 Database::truncate_table( Database::get_table( true ) );
159 } else {
160 Counter::delete_counts( array( 'daily' => true ) );
161 }
162 Dashboard_Widgets::clear_network_dashboard_cache();
163 add_settings_error( 'tptn-notices', '', esc_html__( 'Top 10 daily popular posts reset', 'top-10' ), 'updated' );
164 }
165
166 /* Recreate the shared tables. */
167 if ( ( ! is_multisite() || $network_wide ) && isset( $_POST['tptn_recreate_tables'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
168 $result = self::recreate_tables();
169 if ( is_wp_error( $result ) ) {
170 add_settings_error( 'tptn-notices', '', $result->get_error_message(), 'error' );
171 } else {
172 add_settings_error( 'tptn-notices', '', esc_html__( 'Top 10 tables have been recreated', 'top-10' ), 'updated' );
173 }
174 }
175
176 /* Sync funnel (run aggregation now) */
177 if ( isset( $_POST['tptn_sync_funnel'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
178 $blog_id = ( is_multisite() && ! $network_wide ) ? get_current_blog_id() : null;
179 $result = Database::aggregate_visit_log( 10000, $blog_id );
180 if ( true === $result ) {
181 $message = $network_wide
182 ? __( 'The network funnel has been synced. Buffered visits from all sites have been aggregated.', 'top-10' )
183 : __( 'The site funnel has been synced. Buffered visits for this site have been aggregated.', 'top-10' );
184 add_settings_error( 'tptn-notices', '', esc_html( $message ), 'updated' );
185 } elseif ( 0 === $result ) {
186 $message = $network_wide
187 ? __( 'Nothing to sync. The network funnel is empty.', 'top-10' )
188 : __( 'Nothing to sync. This site funnel is empty.', 'top-10' );
189 add_settings_error( 'tptn-notices', '', esc_html( $message ), 'updated' );
190 } elseif ( false === $result ) {
191 add_settings_error( 'tptn-notices', '', esc_html__( 'Sync skipped: another aggregation is already in progress. Please try again in a moment.', 'top-10' ), 'updated' );
192 } elseif ( is_wp_error( $result ) ) {
193 add_settings_error(
194 'tptn-notices',
195 '',
196 sprintf(
197 /* translators: %s: error message from the database */
198 esc_html__( 'Sync failed: %s', 'top-10' ),
199 esc_html( $result->get_error_message() )
200 ),
201 'error'
202 );
203 }
204 }
205
206 /* Fix cron schedules */
207 if ( isset( $_POST['tptn_fix_crons'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
208 $result = self::fix_crons( $network_wide );
209 if ( is_wp_error( $result ) ) {
210 add_settings_error( 'tptn-notices', '', implode( ' ', array_map( 'esc_html', $result->get_error_messages() ) ), 'error' );
211 } else {
212 add_settings_error( 'tptn-notices', '', esc_html( $result ), 'updated' );
213 }
214 }
215
216 ob_start();
217 ?>
218 <div class="wrap">
219 <h1><?php esc_html_e( 'Top 10 Tools', 'top-10' ); ?></h1>
220 <?php
221 /** This action is documented in includes/admin/class-dashboard.php */
222 do_action( 'tptn_settings_page_header' );
223 ?>
224
225 <?php settings_errors(); ?>
226
227 <div id="poststuff">
228 <div id="post-body" class="metabox-holder columns-2">
229 <div id="post-body-content">
230
231 <form method="post" >
232
233 <div class="postbox">
234 <h2><span><?php esc_html_e( 'Status', 'top-10' ); ?></span></h2>
235 <div class="inside">
236 <div class="tptn-db-status">
237 <?php echo self::get_db_status_report(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
238 </div>
239 </div>
240 </div>
241
242 <div class="postbox">
243 <h2><span><?php esc_html_e( 'Clear cache', 'top-10' ); ?></span></h2>
244 <div class="inside">
245 <p>
246 <?php
247 printf(
248 '<button type="button" name="tptn_cache_clear" class="button button-secondary tptn_cache_clear" data-network-wide="%1$s" aria-label="%2$s">%2$s</button>',
249 $network_wide ? '1' : '0',
250 esc_html( $network_wide ? __( 'Clear Cache Across Network', 'top-10' ) : __( 'Clear cache', 'top-10' ) )
251 );
252 ?>
253 </p>
254 <p class="description">
255 <?php
256 if ( $network_wide ) {
257 esc_html_e( 'Clear the Top 10 cache for all active sites in the network.', 'top-10' );
258 } else {
259 esc_html_e( 'Clear the Top 10 cache. This will also be cleared automatically when you save the settings page.', 'top-10' );
260 }
261 ?>
262 </p>
263 </div>
264 </div>
265
266 <div class="postbox">
267 <h2><span><?php echo esc_html( $network_wide ? __( 'Sync Network Funnel', 'top-10' ) : __( 'Sync Funnel', 'top-10' ) ); ?></span></h2>
268 <div class="inside">
269 <p>
270 <button name="tptn_sync_funnel" type="submit" id="tptn_sync_funnel" class="button button-secondary"><?php echo esc_html( $network_wide ? __( 'Sync Network Funnel Now', 'top-10' ) : __( 'Sync Funnel Now', 'top-10' ) ); ?></button>
271 </p>
272 <p class="description">
273 <?php
274 if ( $network_wide ) {
275 esc_html_e( 'Drain buffered visits from all sites into the count tables immediately. This is equivalent to running the aggregation cron job.', 'top-10' );
276 } else {
277 esc_html_e( 'Drain this site\'s buffered visits into the count tables immediately. Other sites\' buffered visits are not affected.', 'top-10' );
278 }
279 ?>
280 </p>
281 </div>
282 </div>
283
284 <?php
285 /**
286 * Fires between the action boxes on the Tools page.
287 *
288 * @since 4.5.0
289 *
290 * @param bool $network_wide Whether the Tools page is being rendered in network admin.
291 */
292 do_action( 'tptn_tools_page_actions', $network_wide );
293 ?>
294
295 <div class="postbox">
296 <h2><span><?php echo esc_html( $network_wide ? __( 'Fix Network Cron Schedules', 'top-10' ) : __( 'Fix Cron Schedules', 'top-10' ) ); ?></span></h2>
297 <div class="inside">
298 <p>
299 <button name="tptn_fix_crons" type="submit" id="tptn_fix_crons" class="button button-secondary"><?php echo esc_html( $network_wide ? __( 'Fix Network Cron Schedules', 'top-10' ) : __( 'Fix Cron Schedules', 'top-10' ) ); ?></button>
300 </p>
301 <p class="description">
302 <?php
303 if ( $network_wide ) {
304 esc_html_e( 'Clears and reschedules the Top 10 maintenance and aggregation jobs for all active sites. Use this if a site\'s job is not scheduled or if your error log reports cron reschedule errors.', 'top-10' );
305 } else {
306 esc_html_e( 'Clears and reschedules the Top 10 cron jobs for this site. Use this if the status above shows a job as not scheduled or if your error log reports cron reschedule errors for the Top 10 hooks.', 'top-10' );
307 }
308 ?>
309 </p>
310 </div>
311 </div>
312
313 <?php if ( ! is_multisite() || $network_wide ) : ?>
314 <div class="postbox">
315 <h2><span><?php esc_html_e( 'Recreate Primary Key', 'top-10' ); ?></span></h2>
316 <div class="inside">
317 <p>
318 <button name="tptn_recreate_primary_key" type="submit" id="tptn_recreate_primary_key" class="button button-secondary"><?php esc_attr_e( 'Recreate Primary Key', 'top-10' ); ?></button>
319 </p>
320 <p class="description">
321 <?php esc_html_e( 'Deletes and reinitializes the primary key in the shared database tables. Run this from Network Admin on multisite installations. Remember to back up your database first!', 'top-10' ); ?>
322 </p>
323 <p>
324 <code style="display:block;"><?php echo self::recreate_primary_key_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></code>
325 </p>
326 </div>
327 </div>
328 <?php endif; ?>
329
330 <div class="postbox">
331 <h2><span><?php esc_html_e( 'Reset database', 'top-10' ); ?></span></h2>
332 <div class="inside">
333 <p class="description">
334 <?php
335 if ( $network_wide ) {
336 esc_html_e( 'This will reset the Top 10 popular posts across all sites in the network. This cannot be reversed. Make sure that your database has been backed up before proceeding.', 'top-10' );
337 } elseif ( is_multisite() ) {
338 esc_html_e( 'This will reset the Top 10 popular posts for the current site only. Other sites in the network will not be affected. This cannot be reversed. Make sure that your database has been backed up before proceeding.', 'top-10' );
339 } else {
340 esc_html_e( 'This will reset the Top 10 popular posts for this site. This cannot be reversed. Make sure that your database has been backed up before proceeding.', 'top-10' );
341 }
342 ?>
343 </p>
344 <p>
345 <?php
346 printf(
347 '<button name="tptn_reset_overall" type="submit" id="tptn_reset_overall" class="button button-secondary" style="color:#fff;background-color: #a00;border-color: #900;" onclick="if (!confirm(\'%s\')) return false;">%s</button>',
348 esc_attr__( $network_wide ? 'Are you sure you want to reset popular posts across the network?' : 'Are you sure you want to reset the popular posts for this site?', 'top-10' ),
349 esc_attr__( $network_wide ? 'Reset Popular Posts Across Network' : 'Reset Popular Posts', 'top-10' )
350 );
351 ?>
352 </p>
353 <p>
354 <?php
355 printf(
356 '<button name="tptn_reset_daily" type="submit" id="tptn_reset_daily" class="button button-secondary" style="color:#fff;background-color: #a00;border-color: #900;" onclick="if (!confirm(\'%s\')) return false;">%s</button>',
357 esc_attr__( $network_wide ? 'Are you sure you want to reset daily popular posts across the network?' : 'Are you sure you want to reset the daily popular posts for this site?', 'top-10' ),
358 esc_attr__( $network_wide ? 'Reset Daily Popular Posts Across Network' : 'Reset Daily Popular Posts', 'top-10' )
359 );
360 ?>
361 </p>
362 </div>
363 </div>
364
365 <?php if ( ! is_multisite() || $network_wide ) : ?>
366 <div class="postbox">
367 <h2><span><?php esc_html_e( 'Recreate Database Tables', 'top-10' ); ?></span></h2>
368 <div class="inside">
369 <p class="description">
370 <?php esc_html_e( 'Only click the button below after performing a full backup of the database. You can use any of the popular backup plugins or phpMyAdmin to achieve this. The authors of this plugin do not guarantee that everything will go smoothly as it depends on your site environment and volume of data. If you are not comfortable, please do not proceed.', 'top-10' ); ?>
371 </p>
372 <p>
373 <button name="tptn_recreate_tables" type="submit" id="tptn_recreate_tables" style="color:#fff;background-color: #a00;border-color: #900;" onclick="if (!confirm('<?php esc_attr_e( 'Hit Cancel if you have not backed up your database', 'top-10' ); ?>')) return false;" class="button button-secondary"><?php esc_attr_e( 'Recreate Database Tables', 'top-10' ); ?></button>
374 </p>
375 </div>
376 </div>
377 <?php endif; ?>
378
379 <?php wp_nonce_field( 'tptn-tools-settings' ); ?>
380 </form>
381
382 </div><!-- /#post-body-content -->
383
384 <div id="postbox-container-1" class="postbox-container">
385
386 <div id="side-sortables" class="meta-box-sortables ui-sortable">
387 <?php include_once 'sidebar.php'; ?>
388 </div><!-- /#side-sortables -->
389
390 </div><!-- /#postbox-container-1 -->
391 </div><!-- /#post-body -->
392 <br class="clear" />
393 </div><!-- /#poststuff -->
394
395 </div><!-- /.wrap -->
396
397 <?php
398 echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
399 }
400
401 /**
402 * Function to delete and create the primary keys in the database table.
403 *
404 * @since 2.5.6
405 */
406 public static function recreate_primary_key() {
407 global $wpdb;
408
409 $table_name = Database::get_table( false );
410 $table_name_daily = Database::get_table( true );
411
412 $show_errors = $wpdb->hide_errors();
413
414 if ( $wpdb->query( $wpdb->prepare( "SHOW INDEXES FROM {$table_name} WHERE Key_name = %s", 'PRIMARY' ) ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
415 $wpdb->query( 'ALTER TABLE ' . $table_name . ' DROP PRIMARY KEY ' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange
416 }
417 if ( $wpdb->query( $wpdb->prepare( "SHOW INDEXES FROM {$table_name_daily} WHERE Key_name = %s", 'PRIMARY' ) ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
418 $wpdb->query( 'ALTER TABLE ' . $table_name_daily . ' DROP PRIMARY KEY ' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange
419 }
420 if ( $wpdb->query( $wpdb->prepare( "SHOW INDEXES FROM {$table_name_daily} WHERE Key_name = %s", 'blog_date' ) ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
421 $wpdb->query( 'ALTER TABLE ' . $table_name_daily . ' DROP INDEX blog_date' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange
422 }
423
424 $wpdb->query( 'ALTER TABLE ' . $table_name . ' ADD PRIMARY KEY(postnumber, blog_id) ' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange
425 $wpdb->query( 'ALTER TABLE ' . $table_name_daily . ' ADD PRIMARY KEY(postnumber, dp_date, blog_id) ' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange
426 $wpdb->query( 'ALTER TABLE ' . $table_name_daily . ' ADD INDEX blog_date(blog_id, dp_date, postnumber)' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange
427
428 $wpdb->show_errors( $show_errors );
429 }
430
431 /**
432 * Retrieves the SQL code to recreate the PRIMARY KEY.
433 *
434 * @since 2.5.7
435 */
436 public static function recreate_primary_key_html() {
437
438 $table_name = Database::get_table( false );
439 $table_name_daily = Database::get_table( true );
440
441 $sql = 'ALTER TABLE ' . $table_name . ' DROP PRIMARY KEY; ';
442 $sql .= '<br />';
443 $sql .= 'ALTER TABLE ' . $table_name_daily . ' DROP PRIMARY KEY; ';
444 $sql .= '<br />';
445 $sql .= 'ALTER TABLE ' . $table_name_daily . ' DROP INDEX IF EXISTS blog_date; ';
446 $sql .= '<br />';
447 $sql .= 'ALTER TABLE ' . $table_name . ' ADD PRIMARY KEY(postnumber, blog_id); ';
448 $sql .= '<br />';
449 $sql .= 'ALTER TABLE ' . $table_name_daily . ' ADD PRIMARY KEY(postnumber, dp_date, blog_id); ';
450 $sql .= '<br />';
451 $sql .= 'ALTER TABLE ' . $table_name_daily . ' ADD INDEX blog_date(blog_id, dp_date, postnumber); ';
452
453 /**
454 * Filters the SQL code to recreate the PRIMARY KEY.
455 *
456 * @since 2.5.7
457 * @param string $sql SQL code to recreate PRIMARY KEY.
458 */
459 return apply_filters( 'tptn_recreate_primary_key_html', $sql );
460 }
461
462 /**
463 * Retrieves the SQL code to recreate the PRIMARY KEY.
464 *
465 * @since 2.7.0
466 */
467 public static function recreate_tables() {
468 $errors = new \WP_Error();
469
470 $result = Database::recreate_overall_table( false );
471 if ( is_wp_error( $result ) ) {
472 $errors->merge_from( $result );
473 }
474
475 $result = Database::recreate_daily_table( false );
476 if ( is_wp_error( $result ) ) {
477 $errors->merge_from( $result );
478 }
479
480 $result = Database::recreate_funnel_table( false );
481 if ( is_wp_error( $result ) ) {
482 $errors->merge_from( $result );
483 }
484
485 $result = Database::recreate_log_table( false );
486 if ( is_wp_error( $result ) ) {
487 $errors->merge_from( $result );
488 }
489
490 Database::clear_table_installation_cache();
491 Dashboard_Widgets::clear_network_dashboard_cache();
492
493 return $errors->has_errors() ? $errors : true;
494 }
495
496 /**
497 * Clear and reschedule the Top 10 cron jobs.
498 *
499 * @since 4.4.0
500 *
501 * @param bool $network_wide Whether to repair cron schedules for all active sites.
502 * @return string|\WP_Error Success message or WP_Error if a job could not be rescheduled.
503 */
504 public static function fix_crons( $network_wide = false ) {
505 if ( $network_wide && is_multisite() ) {
506 $site_ids = wp_list_pluck(
507 get_sites(
508 array(
509 'archived' => 0,
510 'spam' => 0,
511 'deleted' => 0,
512 'number' => 0,
513 )
514 ),
515 'blog_id'
516 );
517 $errors = new \WP_Error();
518 $sites_processed = 0;
519 $current_blog_id = get_current_blog_id();
520
521 foreach ( $site_ids as $site_id ) {
522 $site_id = absint( $site_id );
523 $switched = $site_id !== $current_blog_id;
524 if ( $switched ) {
525 switch_to_blog( $site_id );
526 }
527
528 try {
529 $result = self::fix_crons_for_site();
530 if ( is_wp_error( $result ) ) {
531 foreach ( $result->get_error_messages() as $message ) {
532 $errors->add(
533 'tptn-cron-site-' . $site_id,
534 sprintf(
535 /* translators: 1: Site ID, 2: Error message. */
536 __( 'Site %1$d: %2$s', 'top-10' ),
537 $site_id,
538 $message
539 )
540 );
541 }
542 } else {
543 ++$sites_processed;
544 }
545 } finally {
546 if ( $switched ) {
547 restore_current_blog();
548 }
549 }
550 }
551
552 if ( $errors->has_errors() ) {
553 return $errors;
554 }
555
556 return sprintf(
557 /* translators: %s: Number of sites. */
558 __( 'Cron schedules have been repaired for %s active site(s).', 'top-10' ),
559 number_format_i18n( $sites_processed )
560 );
561 }
562
563 return self::fix_crons_for_site();
564 }
565
566 /**
567 * Clear and reschedule the Top 10 cron jobs for the current site.
568 *
569 * @since 4.5.0
570 *
571 * @return string|\WP_Error Success message or WP_Error if a job could not be rescheduled.
572 */
573 private static function fix_crons_for_site() {
574 $errors = new \WP_Error();
575 $messages = array();
576 $success = true;
577
578 if ( self::clear_scheduled_hook_or_error( 'tptn_cron_hook', $errors ) ) {
579 if ( tptn_get_option( 'cron_on' ) ) {
580 Cron::enable_run(
581 (int) tptn_get_option( 'cron_hour' ),
582 (int) tptn_get_option( 'cron_min' ),
583 tptn_get_option( 'cron_recurrence' )
584 );
585 if ( wp_next_scheduled( 'tptn_cron_hook' ) ) {
586 $messages[] = __( 'Maintenance cron has been rescheduled.', 'top-10' );
587 } else {
588 $errors->add( 'tptn_cron_hook', __( 'The maintenance cron could not be rescheduled as the cron event list could not be saved.', 'top-10' ) );
589 $success = false;
590 }
591 } else {
592 $messages[] = __( 'Maintenance cron is disabled in the settings and was left unscheduled.', 'top-10' );
593 }
594 } else {
595 $success = false;
596 }
597
598 if ( self::clear_scheduled_hook_or_error( 'tptn_aggregation_cron_hook', $errors ) ) {
599 Cron::enable_aggregation_run();
600 if ( wp_next_scheduled( 'tptn_aggregation_cron_hook' ) ) {
601 $messages[] = __( 'Aggregation cron has been rescheduled.', 'top-10' );
602 } else {
603 $errors->add( 'tptn_aggregation_cron_hook', __( 'The aggregation cron could not be rescheduled as the cron event list could not be saved. Check for object cache or database issues.', 'top-10' ) );
604 $success = false;
605 }
606 } else {
607 $success = false;
608 }
609
610 // Only drop the previously recorded WP-Cron errors once both jobs are confirmed rebuilt (or intentionally left off).
611 if ( $success ) {
612 Cron::clear_reschedule_errors();
613 }
614
615 return $errors->has_errors() ? $errors : implode( ' ', $messages );
616 }
617
618 /**
619 * Clear a scheduled hook, surfacing any WP_Error from WordPress core instead of silently ignoring it.
620 *
621 * If the existing event cannot be cleared, the caller must not attempt to reschedule it: the stale
622 * event would still satisfy wp_next_scheduled(), making a broken cron write look like a successful repair.
623 *
624 * @since 4.4.0
625 *
626 * @param string $hook Hook name to clear.
627 * @param \WP_Error $errors Error collector to append to on failure.
628 * @return bool True if the hook was cleared (or had nothing scheduled), false on failure.
629 */
630 private static function clear_scheduled_hook_or_error( $hook, \WP_Error $errors ) {
631 $result = wp_clear_scheduled_hook( $hook, array(), true );
632
633 if ( is_wp_error( $result ) ) {
634 $errors->add(
635 $hook,
636 sprintf(
637 /* translators: 1: Hook name, 2: Error message from WordPress core. */
638 __( 'Could not clear the existing schedule for %1$s: %2$s', 'top-10' ),
639 $hook,
640 $result->get_error_message()
641 )
642 );
643 return false;
644 }
645
646 return true;
647 }
648
649 /**
650 * Generates the Tools help page.
651 *
652 * @since 3.3.0
653 */
654 public static function help_tabs() {
655 $screen = get_current_screen();
656
657 $screen->set_help_sidebar(
658 /* translators: 1: Support link. */
659 '<p>' . sprintf( __( 'For more information or how to get support visit the <a href="%1$s">WebberZone support site</a>.', 'top-10' ), esc_url( 'https://webberzone.com/support/' ) ) . '</p>' .
660 /* translators: 1: Forum link. */
661 '<p>' . sprintf( __( 'Support queries should be posted in the <a href="%1$s">WordPress.org support forums</a>.', 'top-10' ), esc_url( 'https://wordpress.org/support/plugin/top-10' ) ) . '</p>' .
662 '<p>' . sprintf(
663 /* translators: 1: Github Issues link, 2: Github page. */
664 __( '<a href="%1$s">Post an issue</a> on <a href="%2$s">GitHub</a> (bug reports only).', 'top-10' ),
665 esc_url( 'https://github.com/WebberZone/top-10/issues' ),
666 esc_url( 'https://github.com/WebberZone/top-10' )
667 ) . '</p>'
668 );
669
670 $screen->add_help_tab(
671 array(
672 'id' => 'tptn-settings-general',
673 'title' => __( 'General', 'top-10' ),
674 'content' =>
675 '<p>' . __( 'This screen provides some tools that help maintain certain features of Top 10.', 'top-10' ) . '</p>' .
676 '<p>' . __( 'Clear the cache, reset the popular posts tables plus some miscellaneous fixes for older versions of Top 10.', 'top-10' ) . '</p>',
677 )
678 );
679
680 /**
681 * Fires after the Tools page help tabs have been registered.
682 *
683 * @since 2.5.0
684 *
685 * @param \WP_Screen $screen The current screen object.
686 */
687 do_action( 'tptn_settings_tools_help', $screen );
688 }
689
690 /**
691 * Check if tables exist and create them if they don't.
692 *
693 * @since 4.2.0
694 *
695 * @return array Array of table statuses indicating whether they are installed.
696 */
697 public static function check_table_status() {
698 $tables = array(
699 'top_ten' => Database::get_table( false ),
700 'top_ten_daily' => Database::get_table( true ),
701 'top_ten_visits_funnel' => Database::get_funnel_table(),
702 'top_ten_visits_log' => Database::get_log_table(),
703 );
704
705 $statuses = array();
706
707 $installed_label = '<span style="color: #006400;">' . __( 'Installed', 'top-10' ) . '</span>';
708 $not_installed_label = '<span style="color: #8B0000;">' . __( 'Not Installed', 'top-10' ) . '</span>';
709 $table_statuses = Database::get_table_installation_status();
710
711 foreach ( $tables as $key => $table_name ) {
712 $statuses[ $key ] = ! empty( $table_statuses[ $table_name ] ) ? $installed_label : $not_installed_label;
713 }
714
715 // Create tables if they don't exist.
716 if ( empty( $table_statuses[ Database::get_table( false ) ] ) || empty( $table_statuses[ Database::get_table( true ) ] ) ) {
717 // Use Activator to create tables.
718 Activator::create_tables();
719 Database::clear_table_statistics_cache();
720 $table_statuses = Database::get_table_installation_status( true );
721
722 // Refresh statuses after creating tables.
723 foreach ( $tables as $key => $table_name ) {
724 $statuses[ $key ] = ! empty( $table_statuses[ $table_name ] ) ? $installed_label : $not_installed_label;
725 }
726 }
727
728 /**
729 * Filter the table statuses report.
730 *
731 * @since 4.1.0
732 *
733 * @param array $statuses Array of table statuses.
734 */
735 return apply_filters( 'tptn_table_statuses', $statuses );
736 }
737
738 /**
739 * Get database status report.
740 *
741 * @since 4.2.0
742 *
743 * @return string HTML output for the database status report.
744 */
745 public static function get_db_status_report() {
746 global $tptn_db_version;
747
748 // Get table statuses.
749 $statuses = self::check_table_status();
750
751 // Get table statistics from Database class.
752 $table_stats = Database::get_table_statistics();
753
754 ob_start();
755 ?>
756 <table class="form-table">
757 <tr>
758 <th scope="row"><?php esc_html_e( 'Database version', 'top-10' ); ?></th>
759 <td>
760 <?php esc_html_e( 'Installed version', 'top-10' ); ?> <?php echo esc_html( get_site_option( 'tptn_db_version', '0' ) ); ?> /
761 <?php esc_html_e( 'Current version', 'top-10' ); ?> <?php echo esc_html( $tptn_db_version ); ?>
762 </td>
763 </tr>
764
765 <?php
766 $table_keys = array(
767 'top_ten',
768 'top_ten_daily',
769 'top_ten_visits_funnel',
770 'top_ten_visits_log',
771 );
772 foreach ( $table_keys as $table_key ) :
773 if ( ! isset( $statuses[ $table_key ] ) ) {
774 continue;
775 }
776 ?>
777 <tr>
778 <th scope="row"><?php printf( /* translators: %s: Table name */ esc_html__( '%s table', 'top-10' ), esc_html( $table_key ) ); ?></th>
779 <td>
780 <?php
781 echo $statuses[ $table_key ]; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
782
783 if ( isset( $table_stats[ $table_key ] ) ) {
784 $format = is_multisite()
785 ? __( 'Network-wide estimated entries: %1$s | Network-wide estimated size: %2$s', 'top-10' )
786 : __( 'Estimated entries: %1$s | Estimated size: %2$s', 'top-10' );
787
788 echo '<br><span class="description">';
789 printf(
790 esc_html( $format ),
791 '<strong>' . esc_html( number_format_i18n( $table_stats[ $table_key ]['entries'] ) ) . '</strong>',
792 '<strong>' . esc_html( size_format( $table_stats[ $table_key ]['size'] ) ) . '</strong>'
793 );
794 echo '</span>';
795 }
796 ?>
797 </td>
798 </tr>
799 <?php endforeach; ?>
800
801 <?php
802 $cron_jobs = array(
803 'tptn_cron_hook' => __( 'Maintenance cron', 'top-10' ),
804 'tptn_aggregation_cron_hook' => __( 'Aggregation cron', 'top-10' ),
805 );
806 foreach ( $cron_jobs as $hook => $label ) :
807 $next_run = wp_next_scheduled( $hook );
808 $recurrence = $next_run ? wp_get_schedule( $hook ) : false;
809 $schedules = wp_get_schedules();
810 $interval_display = '';
811 if ( $recurrence && isset( $schedules[ $recurrence ]['display'] ) ) {
812 $interval_display = $schedules[ $recurrence ]['display'];
813 }
814 ?>
815 <tr>
816 <th scope="row"><?php echo esc_html( $label ); ?></th>
817 <td>
818 <?php if ( $next_run ) : ?>
819 <span style="color: #006400;"><?php esc_html_e( 'Scheduled', 'top-10' ); ?></span>
820 <br><span class="description">
821 <?php
822 if ( $interval_display ) {
823 printf(
824 /* translators: %s: schedule display name e.g. "Every two minutes" */
825 esc_html__( 'Runs: %s', 'top-10' ),
826 esc_html( $interval_display )
827 );
828 echo '<br>';
829 }
830 printf(
831 /* translators: %s: human-readable time difference */
832 esc_html__( 'Next run: %s', 'top-10' ),
833 /* translators: %s: human-readable time difference */
834 esc_html( sprintf( __( 'in %s', 'top-10' ), human_time_diff( time(), $next_run ) ) )
835 );
836 ?>
837 </span>
838 <?php else : ?>
839 <span style="color: #8B0000;"><?php esc_html_e( 'Not scheduled', 'top-10' ); ?></span>
840 <?php endif; ?>
841 <?php
842 $reschedule_error = Cron::get_reschedule_error( $hook );
843 if ( $reschedule_error ) :
844 ?>
845 <br><span class="description" style="color: #8B0000;">
846 <?php
847 printf(
848 /* translators: 1: Error message from WP-Cron, 2: Human-readable time difference. */
849 esc_html__( 'WP-Cron reported an error rescheduling this job %2$s ago: %1$s', 'top-10' ),
850 esc_html( $reschedule_error['message'] ),
851 esc_html( human_time_diff( $reschedule_error['time'] ) )
852 );
853 ?>
854 </span>
855 <?php endif; ?>
856 </td>
857 </tr>
858 <?php endforeach; ?>
859
860 <?php if ( ( ! is_multisite() || is_network_admin() ) && ! Database::are_tables_installed() ) : ?>
861 <tr>
862 <th scope="row"><?php esc_html_e( 'Repair database', 'top-10' ); ?></th>
863 <td>
864 <a href="<?php echo esc_url( wp_nonce_url( ( is_multisite() && is_network_admin() ? network_admin_url( 'admin.php?page=tptn_network_tools_page&action=recreate_tables' ) : admin_url( 'admin.php?page=tptn_tools_page&action=recreate_tables' ) ), 'tptn-recreate-tables' ) ); ?>" class="button">
865 <?php esc_html_e( 'Recreate tables', 'top-10' ); ?>
866 </a>
867 </td>
868 </tr>
869 <?php endif; ?>
870 </table>
871 <?php
872
873 return ob_get_clean();
874 }
875
876 /**
877 * Handle recreate tables action from admin area.
878 *
879 * @since 4.2.0
880 */
881 /**
882 * Handle the create-missing-tables GET action triggered from the admin notice.
883 *
884 * @since 4.3.0
885 */
886 public static function handle_create_missing_tables_action() {
887 if ( ! isset( $_GET['action'] ) || 'tptn_create_missing_tables' !== $_GET['action'] || ! isset( $_GET['_wpnonce'] ) ) {
888 return;
889 }
890
891 if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'tptn-create-missing-tables' ) ) {
892 wp_die( esc_html__( 'Security check failed', 'top-10' ) );
893 }
894
895 if ( is_multisite() && ! is_network_admin() ) {
896 wp_die( esc_html__( 'Database table repair must be run from Network Admin.', 'top-10' ) );
897 }
898
899 $capability = is_network_admin() ? 'manage_network_options' : 'manage_options';
900 if ( ! current_user_can( $capability ) ) {
901 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'top-10' ) );
902 }
903
904 Activator::create_tables();
905 Database::clear_table_statistics_cache();
906 Dashboard_Widgets::clear_network_dashboard_cache();
907 $table_statuses = Database::get_table_installation_status( true );
908
909 $still_missing = false;
910 foreach ( $table_statuses as $installed ) {
911 if ( ! $installed ) {
912 $still_missing = true;
913 break;
914 }
915 }
916
917 $status = $still_missing ? 'failed' : 'created';
918 $referer = wp_get_referer();
919 $fallback = is_network_admin() ? network_admin_url( 'admin.php?page=tptn_network_tools_page' ) : admin_url( 'admin.php?page=tptn_tools_page' );
920 $redirect = add_query_arg( 'tptn_tables_created', $status, $referer ? $referer : $fallback );
921
922 wp_safe_redirect( $redirect );
923 exit;
924 }
925
926 /**
927 * Handle the recreate-tables GET action from the tools page.
928 *
929 * @since 4.1.0
930 */
931 public static function handle_recreate_tables_action() {
932 if ( ! isset( $_GET['action'] ) || 'recreate_tables' !== $_GET['action'] || ! isset( $_GET['_wpnonce'] ) ) {
933 return;
934 }
935
936 if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'tptn-recreate-tables' ) ) {
937 wp_die( esc_html__( 'Security check failed', 'top-10' ) );
938 }
939
940 if ( is_multisite() && ! is_network_admin() ) {
941 wp_die( esc_html__( 'Database table repair must be run from Network Admin.', 'top-10' ) );
942 }
943
944 $capability = is_network_admin() ? 'manage_network_options' : 'manage_options';
945 if ( ! current_user_can( $capability ) ) {
946 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'top-10' ) );
947 }
948
949 // Recreate tables.
950 $result_overall = Database::recreate_overall_table( false );
951 $result_daily = Database::recreate_daily_table( false );
952 $result_funnel = Database::recreate_funnel_table( false );
953 $result_log = Database::recreate_log_table( false );
954 Database::clear_table_installation_cache();
955 Dashboard_Widgets::clear_network_dashboard_cache();
956
957 // Check for errors.
958 if ( is_wp_error( $result_overall ) ) {
959 add_settings_error(
960 'tptn-notices',
961 'tptn-recreate-overall-error',
962 $result_overall->get_error_message(),
963 'error'
964 );
965 }
966
967 if ( is_wp_error( $result_daily ) ) {
968 add_settings_error(
969 'tptn-notices',
970 'tptn-recreate-daily-error',
971 $result_daily->get_error_message(),
972 'error'
973 );
974 }
975
976 if ( is_wp_error( $result_funnel ) ) {
977 add_settings_error(
978 'tptn-notices',
979 'tptn-recreate-funnel-error',
980 $result_funnel->get_error_message(),
981 'error'
982 );
983 }
984
985 if ( is_wp_error( $result_log ) ) {
986 add_settings_error(
987 'tptn-notices',
988 'tptn-recreate-log-error',
989 $result_log->get_error_message(),
990 'error'
991 );
992 }
993
994 // If no errors, add success message.
995 if ( ! is_wp_error( $result_overall ) && ! is_wp_error( $result_daily ) && ! is_wp_error( $result_funnel ) && ! is_wp_error( $result_log ) ) {
996 add_settings_error(
997 'tptn-notices',
998 'tptn-recreate-success',
999 __( 'Tables have been recreated successfully.', 'top-10' ),
1000 'success'
1001 );
1002
1003 // Clear table statistics cache since tables were recreated.
1004 Database::clear_table_statistics_cache();
1005 }
1006
1007 // Redirect back to the tools page.
1008 if ( is_network_admin() ) {
1009 wp_safe_redirect( network_admin_url( 'admin.php?page=tptn_network_tools_page&settings-updated=true' ) );
1010 } else {
1011 wp_safe_redirect( admin_url( 'admin.php?page=tptn_tools_page&settings-updated=true' ) );
1012 }
1013 exit;
1014 }
1015 }
1016