PluginProbe
WebberZone Top 10 — Popular Posts / 4.5.2
WebberZone Top 10 — Popular Posts v4.5.2
4.5.2 4.5.1 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 All 118 releases
← All changes | includes/admin/class-tools-page.php +400 -80 4.3.0 → 4.5.2 View file →
@@ -106,12 +106,13 @@
106 106 array(
107 107 'ajax_url' => admin_url( 'admin-ajax.php' ),
108 108 'security' => wp_create_nonce( 'tptn-admin' ),
109 109 'strings' => array(
110 - 'confirm_message' => esc_html__( 'Are you sure you want to clear the cache?', 'top-10' ),
111 - 'clearing_text' => esc_html__( 'Clearing...', 'top-10' ),
112 - 'fail_message' => esc_html__( 'Failed to clear cache. Please try again.', 'top-10' ),
113 - 'request_fail_message' => esc_html__( 'Request failed: ', 'top-10' ),
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' ),
114 115 ),
115 116 )
116 117 );
117 118 }
@@ -125,16 +126,18 @@
125 126 * @return void
126 127 */
127 128 public function render_page() {
128 129 $screen = get_current_screen();
129 - $network_wide = false;
130 + $network_wide = is_multisite() && is_network_admin();
130 131
131 - if ( $screen->id === $this->parent_id . '-network' ) {
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' ) {
132 135 $network_wide = true;
133 136 }
134 137
135 - /* Truncate overall posts table */
136 - if ( isset( $_POST['tptn_recreate_primary_key'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
138 + /* Recreate the shared table indexes. */
139 + if ( ( ! is_multisite() || $network_wide ) && isset( $_POST['tptn_recreate_primary_key'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
137 140 self::recreate_primary_key();
138 141 add_settings_error( 'tptn-notices', '', esc_html__( 'Primary Key has been recreated', 'top-10' ), 'updated' );
139 142 }
140 143
@@ -144,8 +147,9 @@
144 147 Database::truncate_table( Database::get_table( false ) );
145 148 } else {
146 149 Counter::delete_counts( array( 'daily' => false ) );
147 150 }
151 + Dashboard_Widgets::clear_network_dashboard_cache();
148 152 add_settings_error( 'tptn-notices', '', esc_html__( 'Top 10 popular posts reset', 'top-10' ), 'updated' );
149 153 }
150 154
151 155 /* Truncate daily posts table */
@@ -154,24 +158,59 @@
154 158 Database::truncate_table( Database::get_table( true ) );
155 159 } else {
156 160 Counter::delete_counts( array( 'daily' => true ) );
157 161 }
162 + Dashboard_Widgets::clear_network_dashboard_cache();
158 163 add_settings_error( 'tptn-notices', '', esc_html__( 'Top 10 daily popular posts reset', 'top-10' ), 'updated' );
159 164 }
160 165
161 - /* Recreate tables */
162 - if ( isset( $_POST['tptn_recreate_tables'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
163 - self::recreate_tables();
164 - add_settings_error( 'tptn-notices', '', esc_html__( 'Top 10 tables have been recreated', 'top-10' ), 'updated' );
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 + }
165 174 }
166 175
167 176 /* Sync funnel (run aggregation now) */
168 177 if ( isset( $_POST['tptn_sync_funnel'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
169 - $result = Database::aggregate_visit_log();
170 - if ( $result ) {
171 - add_settings_error( 'tptn-notices', '', esc_html__( 'Funnel has been synced. Buffered visits have been aggregated.', 'top-10' ), 'updated' );
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' );
172 211 } else {
173 - add_settings_error( 'tptn-notices', '', esc_html__( 'Nothing to sync. The funnel is empty or another sync is in progress.', 'top-10' ), 'updated' );
212 + add_settings_error( 'tptn-notices', '', esc_html( $result ), 'updated' );
174 213 }
175 214 }
176 215
177 216 ob_start();
@@ -177,9 +216,12 @@
177 216 ob_start();
178 217 ?>
179 218 <div class="wrap">
180 219 <h1><?php esc_html_e( 'Top 10 Tools', 'top-10' ); ?></h1>
181 - <?php do_action( 'tptn_settings_page_header' ); ?>
220 + <?php
221 + /** This action is documented in includes/admin/class-dashboard.php */
222 + do_action( 'tptn_settings_page_header' );
223 + ?>
182 224
183 225 <?php settings_errors(); ?>
184 226
185 227 <div id="poststuff">
@@ -202,58 +244,110 @@
202 244 <div class="inside">
203 245 <p>
204 246 <?php
205 247 printf(
206 - '<button type="button" name="tptn_cache_clear" class="button button-secondary tptn_cache_clear" aria-label="%1$s">%1$s</button>',
207 - esc_html__( 'Clear cache', 'top-10' )
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' ) )
208 251 );
209 252 ?>
210 253 </p>
211 254 <p class="description">
212 - <?php esc_html_e( 'Clear the Top 10 cache. This will also be cleared automatically when you save the settings page.', 'top-10' ); ?>
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 + ?>
213 262 </p>
214 263 </div>
215 264 </div>
216 265
217 266 <div class="postbox">
218 - <h2><span><?php esc_html_e( 'Sync Funnel', 'top-10' ); ?></span></h2>
267 + <h2><span><?php echo esc_html( $network_wide ? __( 'Sync Network Funnel', 'top-10' ) : __( 'Sync Funnel', 'top-10' ) ); ?></span></h2>
219 268 <div class="inside">
220 269 <p>
221 - <button name="tptn_sync_funnel" type="submit" id="tptn_sync_funnel" class="button button-secondary"><?php esc_attr_e( 'Sync Funnel Now', 'top-10' ); ?></button>
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>
222 271 </p>
223 272 <p class="description">
224 - <?php esc_html_e( 'Drain the visits funnel into the count tables immediately. This is equivalent to running the 5-minute aggregation cron job.', 'top-10' ); ?>
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 + ?>
225 280 </p>
226 281 </div>
227 282 </div>
228 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 +
229 295 <div class="postbox">
230 - <h2><span><?php esc_html_e( 'Recreate Primary Key', 'top-10' ); ?></span></h2>
296 + <h2><span><?php echo esc_html( $network_wide ? __( 'Fix Network Cron Schedules', 'top-10' ) : __( 'Fix Cron Schedules', 'top-10' ) ); ?></span></h2>
231 297 <div class="inside">
232 298 <p>
233 - <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>
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>
234 300 </p>
235 301 <p class="description">
236 - <?php esc_html_e( 'Deletes and reinitializes the primary key in the database tables. If the above function gives an error, then you can run the below code in phpMyAdmin or Adminer. Remember to backup your database first!', 'top-10' ); ?>
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 + ?>
237 309 </p>
238 - <p>
239 - <code style="display:block;"><?php echo self::recreate_primary_key_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></code>
240 - </p>
241 310 </div>
242 311 </div>
243 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 +
244 330 <div class="postbox">
245 331 <h2><span><?php esc_html_e( 'Reset database', 'top-10' ); ?></span></h2>
246 332 <div class="inside">
247 333 <p class="description">
248 - <?php esc_html_e( 'This will reset the Top 10 tables. If this is a multisite install, this will reset the popular posts for the current site. If this is the Network Admin screen, then it will reset the popular posts across all sites. This cannot be reversed. Make sure that your database has been backed up before proceeding', 'top-10' ); ?>
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 + ?>
249 343 </p>
250 344 <p>
251 345 <?php
252 346 printf(
253 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>',
254 - esc_attr__( 'Are you sure you want to reset the popular posts?', 'top-10' ),
255 - esc_attr__( 'Reset Popular Posts', 'top-10' )
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' )
256 350 );
257 351 ?>
258 352 </p>
259 353 <p>
@@ -259,10 +353,10 @@
259 353 <p>
260 354 <?php
261 355 printf(
262 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>',
263 - esc_attr__( 'Are you sure you want to reset the daily popular posts?', 'top-10' ),
264 - esc_attr__( 'Reset Daily Popular Posts', 'top-10' )
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' )
265 359 );
266 360 ?>
267 361 </p>
268 362 </div>
@@ -267,9 +361,9 @@
267 361 </p>
268 362 </div>
269 363 </div>
270 364
271 - <?php if ( ! is_multisite() || is_network_admin() ) : ?>
365 + <?php if ( ! is_multisite() || $network_wide ) : ?>
272 366 <div class="postbox">
273 367 <h2><span><?php esc_html_e( 'Recreate Database Tables', 'top-10' ); ?></span></h2>
274 368 <div class="inside">
275 369 <p class="description">
@@ -314,9 +408,9 @@
314 408
315 409 $table_name = Database::get_table( false );
316 410 $table_name_daily = Database::get_table( true );
317 411
318 - $wpdb->hide_errors();
412 + $show_errors = $wpdb->hide_errors();
319 413
320 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
321 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
322 416 }
@@ -330,9 +424,9 @@
330 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
331 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
332 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
333 427
334 - $wpdb->show_errors();
428 + $wpdb->show_errors( $show_errors );
335 429 }
336 430
337 431 /**
338 432 * Retrieves the SQL code to recreate the PRIMARY KEY.
@@ -370,43 +464,190 @@
370 464 *
371 465 * @since 2.7.0
372 466 */
373 467 public static function recreate_tables() {
374 - global $wpdb;
468 + $errors = new \WP_Error();
375 469
376 - $table_name = Database::get_table( false );
377 - $table_name_daily = Database::get_table( true );
378 - $table_name_temp = $table_name . '_temp';
379 - $table_name_daily_temp = $table_name_daily . '_temp';
470 + $result = Database::recreate_overall_table( false );
471 + if ( is_wp_error( $result ) ) {
472 + $errors->merge_from( $result );
473 + }
380 474
381 - $wpdb->hide_errors();
475 + $result = Database::recreate_daily_table( false );
476 + if ( is_wp_error( $result ) ) {
477 + $errors->merge_from( $result );
478 + }
382 479
383 - // Overall table.
384 - $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS {$table_name_temp}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
385 - $result_temp = $wpdb->query( "CREATE TEMPORARY TABLE {$table_name_temp} SELECT * FROM {$table_name};" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
480 + $result = Database::recreate_funnel_table( false );
481 + if ( is_wp_error( $result ) ) {
482 + $errors->merge_from( $result );
483 + }
386 484
387 - if ( false !== $result_temp ) {
388 - $wpdb->query( "DROP TABLE {$table_name}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
389 - Activator::single_activate();
390 - $wpdb->query( "INSERT INTO `{$table_name}` (postnumber, cntaccess, blog_id) SELECT postnumber, cntaccess, blog_id FROM `{$table_name_temp}`;" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
391 - $wpdb->query( "DROP TEMPORARY TABLE {$table_name_temp}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
485 + $result = Database::recreate_log_table( false );
486 + if ( is_wp_error( $result ) ) {
487 + $errors->merge_from( $result );
392 488 }
393 489
394 - // Daily table.
395 - $wpdb->query( "DROP TEMPORARY TABLE IF EXISTS {$table_name_daily_temp}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
396 - $result_daily_temp = $wpdb->query( "CREATE TEMPORARY TABLE {$table_name_daily_temp} SELECT * FROM {$table_name_daily};" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
490 + Database::clear_table_installation_cache();
491 + Dashboard_Widgets::clear_network_dashboard_cache();
397 492
398 - if ( false !== $result_daily_temp ) {
399 - $wpdb->query( "DROP TABLE {$table_name_daily}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
400 - Activator::single_activate();
401 - $wpdb->query( "INSERT INTO `{$table_name_daily}` (postnumber, cntaccess, dp_date, blog_id) SELECT postnumber, cntaccess, dp_date, blog_id FROM `{$table_name_daily_temp}`;" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
402 - $wpdb->query( "DROP TEMPORARY TABLE {$table_name_daily_temp}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
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 + );
403 561 }
404 562
405 - $wpdb->show_errors();
563 + return self::fix_crons_for_site();
406 564 }
407 565
408 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 + /**
409 650 * Generates the Tools help page.
410 651 *
411 652 * @since 3.3.0
412 653 */
@@ -435,8 +676,15 @@
435 676 '<p>' . __( 'Clear the cache, reset the popular posts tables plus some miscellaneous fixes for older versions of Top 10.', 'top-10' ) . '</p>',
436 677 )
437 678 );
438 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 + */
439 687 do_action( 'tptn_settings_tools_help', $screen );
440 688 }
441 689
442 690 /**
@@ -457,22 +705,24 @@
457 705 $statuses = array();
458 706
459 707 $installed_label = '<span style="color: #006400;">' . __( 'Installed', 'top-10' ) . '</span>';
460 708 $not_installed_label = '<span style="color: #8B0000;">' . __( 'Not Installed', 'top-10' ) . '</span>';
709 + $table_statuses = Database::get_table_installation_status();
461 710
462 711 foreach ( $tables as $key => $table_name ) {
463 - $statuses[ $key ] = Database::is_table_installed( $table_name ) ? $installed_label : $not_installed_label;
712 + $statuses[ $key ] = ! empty( $table_statuses[ $table_name ] ) ? $installed_label : $not_installed_label;
464 713 }
465 714
466 715 // Create tables if they don't exist.
467 - if ( ! Database::are_tables_installed() ) {
716 + if ( empty( $table_statuses[ Database::get_table( false ) ] ) || empty( $table_statuses[ Database::get_table( true ) ] ) ) {
468 717 // Use Activator to create tables.
469 718 Activator::create_tables();
470 719 Database::clear_table_statistics_cache();
720 + $table_statuses = Database::get_table_installation_status( true );
471 721
472 722 // Refresh statuses after creating tables.
473 723 foreach ( $tables as $key => $table_name ) {
474 - $statuses[ $key ] = Database::is_table_installed( $table_name ) ? $installed_label : $not_installed_label;
724 + $statuses[ $key ] = ! empty( $table_statuses[ $table_name ] ) ? $installed_label : $not_installed_label;
475 725 }
476 726 }
477 727
478 728 /**
@@ -512,9 +762,14 @@
512 762 </td>
513 763 </tr>
514 764
515 765 <?php
516 - $table_keys = array( 'top_ten', 'top_ten_daily', 'top_ten_visits_funnel', 'top_ten_visits_log' );
766 + $table_keys = array(
767 + 'top_ten',
768 + 'top_ten_daily',
769 + 'top_ten_visits_funnel',
770 + 'top_ten_visits_log',
771 + );
517 772 foreach ( $table_keys as $table_key ) :
518 773 if ( ! isset( $statuses[ $table_key ] ) ) {
519 774 continue;
520 775 }
@@ -525,13 +780,11 @@
525 780 <?php
526 781 echo $statuses[ $table_key ]; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
527 782
528 783 if ( isset( $table_stats[ $table_key ] ) ) {
529 - $format = ( is_multisite() && ! is_network_admin() )
530 - /* translators: 1: Number of entries, 2: Estimated table size */
531 - ? __( 'Entries: %1$s | Est. Size: %2$s', 'top-10' )
532 - /* translators: 1: Number of entries, 2: Table size */
533 - : __( 'Entries: %1$s | Size: %2$s', 'top-10' );
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' );
534 787
535 788 echo '<br><span class="description">';
536 789 printf(
537 790 esc_html( $format ),
@@ -550,9 +803,15 @@
550 803 'tptn_cron_hook' => __( 'Maintenance cron', 'top-10' ),
551 804 'tptn_aggregation_cron_hook' => __( 'Aggregation cron', 'top-10' ),
552 805 );
553 806 foreach ( $cron_jobs as $hook => $label ) :
554 - $next_run = wp_next_scheduled( $hook );
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 + }
555 814 ?>
556 815 <tr>
557 816 <th scope="row"><?php echo esc_html( $label ); ?></th>
558 817 <td>
@@ -559,11 +818,20 @@
559 818 <?php if ( $next_run ) : ?>
560 819 <span style="color: #006400;"><?php esc_html_e( 'Scheduled', 'top-10' ); ?></span>
561 820 <br><span class="description">
562 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 + }
563 830 printf(
564 831 /* translators: %s: human-readable time difference */
565 832 esc_html__( 'Next run: %s', 'top-10' ),
833 + /* translators: %s: human-readable time difference */
566 834 esc_html( sprintf( __( 'in %s', 'top-10' ), human_time_diff( time(), $next_run ) ) )
567 835 );
568 836 ?>
569 837 </span>
@@ -569,17 +837,32 @@
569 837 </span>
570 838 <?php else : ?>
571 839 <span style="color: #8B0000;"><?php esc_html_e( 'Not scheduled', 'top-10' ); ?></span>
572 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; ?>
573 856 </td>
574 857 </tr>
575 858 <?php endforeach; ?>
576 859
577 - <?php if ( ! Database::are_tables_installed() ) : ?>
860 + <?php if ( ( ! is_multisite() || is_network_admin() ) && ! Database::are_tables_installed() ) : ?>
578 861 <tr>
579 862 <th scope="row"><?php esc_html_e( 'Repair database', 'top-10' ); ?></th>
580 863 <td>
581 - <a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=tptn_dashboard&action=recreate_tables' ), 'tptn-recreate-tables' ) ); ?>" class="button">
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">
582 865 <?php esc_html_e( 'Recreate tables', 'top-10' ); ?>
583 866 </a>
584 867 </td>
585 868 </tr>
@@ -608,18 +891,25 @@
608 891 if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'tptn-create-missing-tables' ) ) {
609 892 wp_die( esc_html__( 'Security check failed', 'top-10' ) );
610 893 }
611 894
612 - if ( ! current_user_can( 'manage_options' ) ) {
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 ) ) {
613 901 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'top-10' ) );
614 902 }
615 903
616 904 Activator::create_tables();
617 905 Database::clear_table_statistics_cache();
906 + Dashboard_Widgets::clear_network_dashboard_cache();
907 + $table_statuses = Database::get_table_installation_status( true );
618 908
619 909 $still_missing = false;
620 - foreach ( array( Database::get_table( false ), Database::get_table( true ), Database::get_log_table(), Database::get_funnel_table() ) as $table ) {
621 - if ( ! Database::is_table_installed( $table ) ) {
910 + foreach ( $table_statuses as $installed ) {
911 + if ( ! $installed ) {
622 912 $still_missing = true;
623 913 break;
624 914 }
625 915 }
@@ -625,9 +915,9 @@
625 915 }
626 916
627 917 $status = $still_missing ? 'failed' : 'created';
628 918 $referer = wp_get_referer();
629 - $fallback = admin_url( 'admin.php?page=' . ( is_network_admin() ? 'tptn_network_tools_page' : 'tptn_tools_page' ) );
919 + $fallback = is_network_admin() ? network_admin_url( 'admin.php?page=tptn_network_tools_page' ) : admin_url( 'admin.php?page=tptn_tools_page' );
630 920 $redirect = add_query_arg( 'tptn_tables_created', $status, $referer ? $referer : $fallback );
631 921
632 922 wp_safe_redirect( $redirect );
633 923 exit;
@@ -646,9 +936,14 @@
646 936 if ( ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'tptn-recreate-tables' ) ) {
647 937 wp_die( esc_html__( 'Security check failed', 'top-10' ) );
648 938 }
649 939
650 - if ( ! current_user_can( 'manage_options' ) ) {
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 ) ) {
651 946 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'top-10' ) );
652 947 }
653 948
654 949 // Recreate tables.
@@ -653,8 +948,12 @@
653 948
654 949 // Recreate tables.
655 950 $result_overall = Database::recreate_overall_table( false );
656 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();
657 956
658 957 // Check for errors.
659 958 if ( is_wp_error( $result_overall ) ) {
660 959 add_settings_error(
@@ -673,10 +972,28 @@
673 972 'error'
674 973 );
675 974 }
676 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 +
677 994 // If no errors, add success message.
678 - if ( ! is_wp_error( $result_overall ) && ! is_wp_error( $result_daily ) ) {
995 + if ( ! is_wp_error( $result_overall ) && ! is_wp_error( $result_daily ) && ! is_wp_error( $result_funnel ) && ! is_wp_error( $result_log ) ) {
679 996 add_settings_error(
680 997 'tptn-notices',
681 998 'tptn-recreate-success',
682 999 __( 'Tables have been recreated successfully.', 'top-10' ),
@@ -687,9 +1004,12 @@
687 1004 Database::clear_table_statistics_cache();
688 1005 }
689 1006
690 1007 // Redirect back to the tools page.
691 - $page = is_network_admin() ? 'tptn_network_tools_page' : 'tptn_tools_page';
692 - wp_safe_redirect( admin_url( 'admin.php?page=' . $page . '&settings-updated=true' ) );
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 + }
693 1013 exit;
694 1014 }
695 1015 }