parent_id = add_submenu_page(
'tptn_dashboard',
esc_html__( 'Top 10 Tools', 'top-10' ),
esc_html__( 'Tools', 'top-10' ),
'manage_options',
'tptn_tools_page',
array( $this, 'render_page' )
);
add_action( 'load-' . $this->parent_id, array( $this, 'help_tabs' ) );
}
/**
* Admin Menu.
*
* @since 3.3.0
*/
public function network_admin_menu() {
$this->parent_id = add_submenu_page(
'tptn_dashboard',
esc_html__( 'Top 10 Tools', 'top-10' ),
esc_html__( 'Tools', 'top-10' ),
'manage_network_options',
'tptn_network_tools_page',
array( $this, 'render_page' )
);
add_action( 'load-' . $this->parent_id, array( $this, 'help_tabs' ) );
}
/**
* Enqueue scripts in admin area.
*
* @since 3.3.0
*
* @param string $hook The current admin page.
*/
public function admin_enqueue_scripts( $hook ) {
if ( $hook === $this->parent_id ) {
wp_enqueue_script( 'top-ten-admin-js' );
wp_enqueue_style( 'top-ten-admin-css' );
wp_enqueue_style( 'wp-spinner' );
wp_localize_script(
'top-ten-admin-js',
'top_ten_admin_data',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'security' => wp_create_nonce( 'tptn-admin' ),
'strings' => array(
'confirm_message' => esc_html__( 'Are you sure you want to clear the cache?', 'top-10' ),
'network_confirm_message' => esc_html__( 'Are you sure you want to clear the Top 10 cache across all active sites?', 'top-10' ),
'clearing_text' => esc_html__( 'Clearing...', 'top-10' ),
'fail_message' => esc_html__( 'Failed to clear cache. Please try again.', 'top-10' ),
'request_fail_message' => esc_html__( 'Request failed: ', 'top-10' ),
),
)
);
}
}
/**
* Render the tools settings page.
*
* @since 2.5.0
*
* @return void
*/
public function render_page() {
$screen = get_current_screen();
$network_wide = is_multisite() && is_network_admin();
// Keep the screen check as a fallback for admin screen implementations that
// do not expose is_network_admin() reliably.
if ( $screen && $screen->id === $this->parent_id . '-network' ) {
$network_wide = true;
}
/* Recreate the shared table indexes. */
if ( ( ! is_multisite() || $network_wide ) && isset( $_POST['tptn_recreate_primary_key'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
self::recreate_primary_key();
add_settings_error( 'tptn-notices', '', esc_html__( 'Primary Key has been recreated', 'top-10' ), 'updated' );
}
/* Truncate overall posts table */
if ( isset( $_POST['tptn_reset_overall'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
if ( ! is_multisite() || $network_wide ) {
Database::truncate_table( Database::get_table( false ) );
} else {
Counter::delete_counts( array( 'daily' => false ) );
}
Dashboard_Widgets::clear_network_dashboard_cache();
add_settings_error( 'tptn-notices', '', esc_html__( 'Top 10 popular posts reset', 'top-10' ), 'updated' );
}
/* Truncate daily posts table */
if ( isset( $_POST['tptn_reset_daily'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
if ( ! is_multisite() || $network_wide ) {
Database::truncate_table( Database::get_table( true ) );
} else {
Counter::delete_counts( array( 'daily' => true ) );
}
Dashboard_Widgets::clear_network_dashboard_cache();
add_settings_error( 'tptn-notices', '', esc_html__( 'Top 10 daily popular posts reset', 'top-10' ), 'updated' );
}
/* Recreate the shared tables. */
if ( ( ! is_multisite() || $network_wide ) && isset( $_POST['tptn_recreate_tables'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
$result = self::recreate_tables();
if ( is_wp_error( $result ) ) {
add_settings_error( 'tptn-notices', '', $result->get_error_message(), 'error' );
} else {
add_settings_error( 'tptn-notices', '', esc_html__( 'Top 10 tables have been recreated', 'top-10' ), 'updated' );
}
}
/* Sync funnel (run aggregation now) */
if ( isset( $_POST['tptn_sync_funnel'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
$blog_id = ( is_multisite() && ! $network_wide ) ? get_current_blog_id() : null;
$result = Database::aggregate_visit_log( 10000, $blog_id );
if ( true === $result ) {
$message = $network_wide
? __( 'The network funnel has been synced. Buffered visits from all sites have been aggregated.', 'top-10' )
: __( 'The site funnel has been synced. Buffered visits for this site have been aggregated.', 'top-10' );
add_settings_error( 'tptn-notices', '', esc_html( $message ), 'updated' );
} elseif ( 0 === $result ) {
$message = $network_wide
? __( 'Nothing to sync. The network funnel is empty.', 'top-10' )
: __( 'Nothing to sync. This site funnel is empty.', 'top-10' );
add_settings_error( 'tptn-notices', '', esc_html( $message ), 'updated' );
} elseif ( false === $result ) {
add_settings_error( 'tptn-notices', '', esc_html__( 'Sync skipped: another aggregation is already in progress. Please try again in a moment.', 'top-10' ), 'updated' );
} elseif ( is_wp_error( $result ) ) {
add_settings_error(
'tptn-notices',
'',
sprintf(
/* translators: %s: error message from the database */
esc_html__( 'Sync failed: %s', 'top-10' ),
esc_html( $result->get_error_message() )
),
'error'
);
}
}
/* Fix cron schedules */
if ( isset( $_POST['tptn_fix_crons'] ) && check_admin_referer( 'tptn-tools-settings' ) ) {
$result = self::fix_crons( $network_wide );
if ( is_wp_error( $result ) ) {
add_settings_error( 'tptn-notices', '', implode( ' ', array_map( 'esc_html', $result->get_error_messages() ) ), 'error' );
} else {
add_settings_error( 'tptn-notices', '', esc_html( $result ), 'updated' );
}
}
ob_start();
?>
hide_errors();
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
$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
}
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
$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
}
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
$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
}
$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
$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
$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
$wpdb->show_errors( $show_errors );
}
/**
* Retrieves the SQL code to recreate the PRIMARY KEY.
*
* @since 2.5.7
*/
public static function recreate_primary_key_html() {
$table_name = Database::get_table( false );
$table_name_daily = Database::get_table( true );
$sql = 'ALTER TABLE ' . $table_name . ' DROP PRIMARY KEY; ';
$sql .= '
';
$sql .= 'ALTER TABLE ' . $table_name_daily . ' DROP PRIMARY KEY; ';
$sql .= '
';
$sql .= 'ALTER TABLE ' . $table_name_daily . ' DROP INDEX IF EXISTS blog_date; ';
$sql .= '
';
$sql .= 'ALTER TABLE ' . $table_name . ' ADD PRIMARY KEY(postnumber, blog_id); ';
$sql .= '
';
$sql .= 'ALTER TABLE ' . $table_name_daily . ' ADD PRIMARY KEY(postnumber, dp_date, blog_id); ';
$sql .= '
';
$sql .= 'ALTER TABLE ' . $table_name_daily . ' ADD INDEX blog_date(blog_id, dp_date, postnumber); ';
/**
* Filters the SQL code to recreate the PRIMARY KEY.
*
* @since 2.5.7
* @param string $sql SQL code to recreate PRIMARY KEY.
*/
return apply_filters( 'tptn_recreate_primary_key_html', $sql );
}
/**
* Retrieves the SQL code to recreate the PRIMARY KEY.
*
* @since 2.7.0
*/
public static function recreate_tables() {
$errors = new \WP_Error();
$result = Database::recreate_overall_table( false );
if ( is_wp_error( $result ) ) {
$errors->merge_from( $result );
}
$result = Database::recreate_daily_table( false );
if ( is_wp_error( $result ) ) {
$errors->merge_from( $result );
}
$result = Database::recreate_funnel_table( false );
if ( is_wp_error( $result ) ) {
$errors->merge_from( $result );
}
$result = Database::recreate_log_table( false );
if ( is_wp_error( $result ) ) {
$errors->merge_from( $result );
}
Database::clear_table_installation_cache();
Dashboard_Widgets::clear_network_dashboard_cache();
return $errors->has_errors() ? $errors : true;
}
/**
* Clear and reschedule the Top 10 cron jobs.
*
* @since 4.4.0
*
* @param bool $network_wide Whether to repair cron schedules for all active sites.
* @return string|\WP_Error Success message or WP_Error if a job could not be rescheduled.
*/
public static function fix_crons( $network_wide = false ) {
if ( $network_wide && is_multisite() ) {
$site_ids = wp_list_pluck(
get_sites(
array(
'archived' => 0,
'spam' => 0,
'deleted' => 0,
'number' => 0,
)
),
'blog_id'
);
$errors = new \WP_Error();
$sites_processed = 0;
$current_blog_id = get_current_blog_id();
foreach ( $site_ids as $site_id ) {
$site_id = absint( $site_id );
$switched = $site_id !== $current_blog_id;
if ( $switched ) {
switch_to_blog( $site_id );
}
try {
$result = self::fix_crons_for_site();
if ( is_wp_error( $result ) ) {
foreach ( $result->get_error_messages() as $message ) {
$errors->add(
'tptn-cron-site-' . $site_id,
sprintf(
/* translators: 1: Site ID, 2: Error message. */
__( 'Site %1$d: %2$s', 'top-10' ),
$site_id,
$message
)
);
}
} else {
++$sites_processed;
}
} finally {
if ( $switched ) {
restore_current_blog();
}
}
}
if ( $errors->has_errors() ) {
return $errors;
}
return sprintf(
/* translators: %s: Number of sites. */
__( 'Cron schedules have been repaired for %s active site(s).', 'top-10' ),
number_format_i18n( $sites_processed )
);
}
return self::fix_crons_for_site();
}
/**
* Clear and reschedule the Top 10 cron jobs for the current site.
*
* @since 4.5.0
*
* @return string|\WP_Error Success message or WP_Error if a job could not be rescheduled.
*/
private static function fix_crons_for_site() {
$errors = new \WP_Error();
$messages = array();
$success = true;
if ( self::clear_scheduled_hook_or_error( 'tptn_cron_hook', $errors ) ) {
if ( tptn_get_option( 'cron_on' ) ) {
Cron::enable_run(
(int) tptn_get_option( 'cron_hour' ),
(int) tptn_get_option( 'cron_min' ),
tptn_get_option( 'cron_recurrence' )
);
if ( wp_next_scheduled( 'tptn_cron_hook' ) ) {
$messages[] = __( 'Maintenance cron has been rescheduled.', 'top-10' );
} else {
$errors->add( 'tptn_cron_hook', __( 'The maintenance cron could not be rescheduled as the cron event list could not be saved.', 'top-10' ) );
$success = false;
}
} else {
$messages[] = __( 'Maintenance cron is disabled in the settings and was left unscheduled.', 'top-10' );
}
} else {
$success = false;
}
if ( self::clear_scheduled_hook_or_error( 'tptn_aggregation_cron_hook', $errors ) ) {
Cron::enable_aggregation_run();
if ( wp_next_scheduled( 'tptn_aggregation_cron_hook' ) ) {
$messages[] = __( 'Aggregation cron has been rescheduled.', 'top-10' );
} else {
$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' ) );
$success = false;
}
} else {
$success = false;
}
// Only drop the previously recorded WP-Cron errors once both jobs are confirmed rebuilt (or intentionally left off).
if ( $success ) {
Cron::clear_reschedule_errors();
}
return $errors->has_errors() ? $errors : implode( ' ', $messages );
}
/**
* Clear a scheduled hook, surfacing any WP_Error from WordPress core instead of silently ignoring it.
*
* If the existing event cannot be cleared, the caller must not attempt to reschedule it: the stale
* event would still satisfy wp_next_scheduled(), making a broken cron write look like a successful repair.
*
* @since 4.4.0
*
* @param string $hook Hook name to clear.
* @param \WP_Error $errors Error collector to append to on failure.
* @return bool True if the hook was cleared (or had nothing scheduled), false on failure.
*/
private static function clear_scheduled_hook_or_error( $hook, \WP_Error $errors ) {
$result = wp_clear_scheduled_hook( $hook, array(), true );
if ( is_wp_error( $result ) ) {
$errors->add(
$hook,
sprintf(
/* translators: 1: Hook name, 2: Error message from WordPress core. */
__( 'Could not clear the existing schedule for %1$s: %2$s', 'top-10' ),
$hook,
$result->get_error_message()
)
);
return false;
}
return true;
}
/**
* Generates the Tools help page.
*
* @since 3.3.0
*/
public static function help_tabs() {
$screen = get_current_screen();
$screen->set_help_sidebar(
/* translators: 1: Support link. */
'' . sprintf( __( 'For more information or how to get support visit the WebberZone support site.', 'top-10' ), esc_url( 'https://webberzone.com/support/' ) ) . '
' .
/* translators: 1: Forum link. */
'' . sprintf( __( 'Support queries should be posted in the WordPress.org support forums.', 'top-10' ), esc_url( 'https://wordpress.org/support/plugin/top-10' ) ) . '
' .
'' . sprintf(
/* translators: 1: Github Issues link, 2: Github page. */
__( 'Post an issue on GitHub (bug reports only).', 'top-10' ),
esc_url( 'https://github.com/WebberZone/top-10/issues' ),
esc_url( 'https://github.com/WebberZone/top-10' )
) . '
'
);
$screen->add_help_tab(
array(
'id' => 'tptn-settings-general',
'title' => __( 'General', 'top-10' ),
'content' =>
'' . __( 'This screen provides some tools that help maintain certain features of Top 10.', 'top-10' ) . '
' .
'' . __( 'Clear the cache, reset the popular posts tables plus some miscellaneous fixes for older versions of Top 10.', 'top-10' ) . '
',
)
);
/**
* Fires after the Tools page help tabs have been registered.
*
* @since 2.5.0
*
* @param \WP_Screen $screen The current screen object.
*/
do_action( 'tptn_settings_tools_help', $screen );
}
/**
* Check if tables exist and create them if they don't.
*
* @since 4.2.0
*
* @return array Array of table statuses indicating whether they are installed.
*/
public static function check_table_status() {
$tables = array(
'top_ten' => Database::get_table( false ),
'top_ten_daily' => Database::get_table( true ),
'top_ten_visits_funnel' => Database::get_funnel_table(),
'top_ten_visits_log' => Database::get_log_table(),
);
$statuses = array();
$installed_label = '' . __( 'Installed', 'top-10' ) . '';
$not_installed_label = '' . __( 'Not Installed', 'top-10' ) . '';
$table_statuses = Database::get_table_installation_status();
foreach ( $tables as $key => $table_name ) {
$statuses[ $key ] = ! empty( $table_statuses[ $table_name ] ) ? $installed_label : $not_installed_label;
}
// Create tables if they don't exist.
if ( empty( $table_statuses[ Database::get_table( false ) ] ) || empty( $table_statuses[ Database::get_table( true ) ] ) ) {
// Use Activator to create tables.
Activator::create_tables();
Database::clear_table_statistics_cache();
$table_statuses = Database::get_table_installation_status( true );
// Refresh statuses after creating tables.
foreach ( $tables as $key => $table_name ) {
$statuses[ $key ] = ! empty( $table_statuses[ $table_name ] ) ? $installed_label : $not_installed_label;
}
}
/**
* Filter the table statuses report.
*
* @since 4.1.0
*
* @param array $statuses Array of table statuses.
*/
return apply_filters( 'tptn_table_statuses', $statuses );
}
/**
* Get database status report.
*
* @since 4.2.0
*
* @return string HTML output for the database status report.
*/
public static function get_db_status_report() {
global $tptn_db_version;
// Get table statuses.
$statuses = self::check_table_status();
// Get table statistics from Database class.
$table_stats = Database::get_table_statistics();
ob_start();
?>
get_error_message(),
'error'
);
}
if ( is_wp_error( $result_daily ) ) {
add_settings_error(
'tptn-notices',
'tptn-recreate-daily-error',
$result_daily->get_error_message(),
'error'
);
}
if ( is_wp_error( $result_funnel ) ) {
add_settings_error(
'tptn-notices',
'tptn-recreate-funnel-error',
$result_funnel->get_error_message(),
'error'
);
}
if ( is_wp_error( $result_log ) ) {
add_settings_error(
'tptn-notices',
'tptn-recreate-log-error',
$result_log->get_error_message(),
'error'
);
}
// If no errors, add success message.
if ( ! is_wp_error( $result_overall ) && ! is_wp_error( $result_daily ) && ! is_wp_error( $result_funnel ) && ! is_wp_error( $result_log ) ) {
add_settings_error(
'tptn-notices',
'tptn-recreate-success',
__( 'Tables have been recreated successfully.', 'top-10' ),
'success'
);
// Clear table statistics cache since tables were recreated.
Database::clear_table_statistics_cache();
}
// Redirect back to the tools page.
if ( is_network_admin() ) {
wp_safe_redirect( network_admin_url( 'admin.php?page=tptn_network_tools_page&settings-updated=true' ) );
} else {
wp_safe_redirect( admin_url( 'admin.php?page=tptn_tools_page&settings-updated=true' ) );
}
exit;
}
}