| 1 |
<?php |
| 2 |
/** |
| 3 |
* WPP Importer class. |
| 4 |
* |
| 5 |
* @package WebberZone\Top_Ten\Admin |
| 6 |
*/ |
| 7 |
|
| 8 |
declare(strict_types=1); |
| 9 |
|
| 10 |
namespace WebberZone\Top_Ten\Admin; |
| 11 |
|
| 12 |
if ( ! defined( 'WPINC' ) ) { |
| 13 |
die; |
| 14 |
} |
| 15 |
|
| 16 |
/** |
| 17 |
* Class WPP_Importer |
| 18 |
* |
| 19 |
* Handles importing WordPress Popular Posts data into Top 10 tables. |
| 20 |
* |
| 21 |
* @since 4.1.0 |
| 22 |
*/ |
| 23 |
class WPP_Importer { |
| 24 |
|
| 25 |
/** |
| 26 |
* Initialize hooks. |
| 27 |
* |
| 28 |
* @since 4.1.0 |
| 29 |
* @return void |
| 30 |
*/ |
| 31 |
public function __construct() { |
| 32 |
add_action( 'tptn_admin_import_export_tab_content', array( $this, 'render_page' ) ); |
| 33 |
add_action( 'wp_ajax_top_ten_import_wpp', array( $this, 'process_ajax_import' ) ); |
| 34 |
add_action( 'admin_post_top_ten_import_wpp', array( $this, 'handle_import_request' ) ); |
| 35 |
} |
| 36 |
|
| 37 |
/** |
| 38 |
* Renders the importer form. |
| 39 |
* |
| 40 |
* This is meant to be called within tools-page.php. |
| 41 |
* |
| 42 |
* @since 4.1.0 |
| 43 |
* @return void |
| 44 |
*/ |
| 45 |
public function render_page() { |
| 46 |
// Enqueue required scripts. |
| 47 |
$this->enqueue_scripts(); |
| 48 |
?> |
| 49 |
<div class="wrap"> |
| 50 |
<hr /> |
| 51 |
<h2 style="padding-left:0px;font-size: 1.5em;"><?php esc_html_e( 'Migrate WordPress Popular Posts Data to Top 10', 'top-10' ); ?></h2> |
| 52 |
|
| 53 |
<?php |
| 54 |
$wpp_active = self::is_wpp_active(); |
| 55 |
$tables_exist = self::wpp_tables_exist(); |
| 56 |
?> |
| 57 |
|
| 58 |
<?php if ( ! $wpp_active && ! $tables_exist ) : ?> |
| 59 |
<p class="notice notice-error"><?php esc_html_e( 'WordPress Popular Posts plugin is not active and no WPP tables were found in the database. Import is not possible.', 'top-10' ); ?></p> |
| 60 |
<?php elseif ( ! $tables_exist ) : ?> |
| 61 |
<p class="notice notice-error"><?php esc_html_e( 'WordPress Popular Posts tables were not found in the database. Import is not possible.', 'top-10' ); ?></p> |
| 62 |
<?php else : ?> |
| 63 |
<p><?php esc_html_e( 'This tool allows you to import post view counts from WordPress Popular Posts into Top 10.', 'top-10' ); ?></p> |
| 64 |
<div id="top-ten-import-notice" class="notice notice-warning"> |
| 65 |
<p> |
| 66 |
<strong><?php esc_html_e( 'Important:', 'top-10' ); ?></strong> |
| 67 |
<?php esc_html_e( 'Please backup your database before proceeding with the import.', 'top-10' ); ?> |
| 68 |
</p> |
| 69 |
</div> |
| 70 |
|
| 71 |
<div class="postbox"> |
| 72 |
<h3 class="hndle"><?php esc_html_e( 'Important Notes', 'top-10' ); ?></h3> |
| 73 |
<div class="inside"> |
| 74 |
<ul style="list-style: disc; padding-left: 20px;"> |
| 75 |
<li><?php esc_html_e( 'WordPress Popular Posts tracks each individual view, while Top 10 tracks cumulative views and daily views by hour.', 'top-10' ); ?></li> |
| 76 |
<li><?php esc_html_e( 'For daily data, the importer will consolidate WPP data by hour to match Top 10\'s format.', 'top-10' ); ?></li> |
| 77 |
<li><?php esc_html_e( 'It\'s recommended to backup your database before running this import.', 'top-10' ); ?></li> |
| 78 |
<li><?php esc_html_e( 'Large sites may experience timeouts during import. If this happens, try importing with a higher minimum view count.', 'top-10' ); ?></li> |
| 79 |
</ul> |
| 80 |
</div> |
| 81 |
</div> |
| 82 |
<?php endif; ?> |
| 83 |
|
| 84 |
<?php if ( $wpp_active && $tables_exist ) : ?> |
| 85 |
<form id="top-ten-wpp-import-form" method="post"> |
| 86 |
<?php wp_nonce_field( 'top_ten_import_wpp_nonce', 'top_ten_import_wpp_nonce_field' ); ?> |
| 87 |
<input type="hidden" name="action" value="top_ten_import_wpp"> |
| 88 |
<table class="form-table"> |
| 89 |
<tr> |
| 90 |
<th scope="row"><?php esc_html_e( 'Import Mode', 'top-10' ); ?></th> |
| 91 |
<td> |
| 92 |
<fieldset> |
| 93 |
<legend class="screen-reader-text"><?php esc_html_e( 'Import Mode', 'top-10' ); ?></legend> |
| 94 |
<label> |
| 95 |
<input type="radio" name="import_mode" value="merge"> |
| 96 |
<span><?php esc_html_e( 'Merge data (add WPP counts to existing Top 10 counts)', 'top-10' ); ?></span> |
| 97 |
</label> |
| 98 |
<br> |
| 99 |
<label> |
| 100 |
<input type="radio" name="import_mode" value="replace" checked="checked"> |
| 101 |
<span><?php esc_html_e( 'Replace data (replace Top 10 counts with WPP counts)', 'top-10' ); ?></span> |
| 102 |
</label> |
| 103 |
</fieldset> |
| 104 |
<p class="description"><?php esc_html_e( 'Merge mode will add WPP counts to existing Top 10 counts, while replace mode will replace Top 10 counts with WPP counts if they exist for the same post.', 'top-10' ); ?></p> |
| 105 |
</td> |
| 106 |
</tr> |
| 107 |
<tr> |
| 108 |
<th scope="row"><?php esc_html_e( 'Data to Import', 'top-10' ); ?></th> |
| 109 |
<td> |
| 110 |
<fieldset> |
| 111 |
<legend class="screen-reader-text"><?php esc_html_e( 'Data to Import', 'top-10' ); ?></legend> |
| 112 |
<label> |
| 113 |
<input type="radio" name="import_data" value="total" checked="checked"> |
| 114 |
<span><?php esc_html_e( 'Total counts only', 'top-10' ); ?></span> |
| 115 |
</label> |
| 116 |
<br> |
| 117 |
<label> |
| 118 |
<input type="radio" name="import_data" value="daily"> |
| 119 |
<span><?php esc_html_e( 'Daily counts only', 'top-10' ); ?></span> |
| 120 |
</label> |
| 121 |
<br> |
| 122 |
<label> |
| 123 |
<input type="radio" name="import_data" value="both"> |
| 124 |
<span><?php esc_html_e( 'Both total and daily counts', 'top-10' ); ?></span> |
| 125 |
</label> |
| 126 |
</fieldset> |
| 127 |
<p class="description"><?php esc_html_e( 'Total counts only will import only the total view counts from WPP, while daily counts only will import only the daily view counts from WPP. Both total and daily counts will import both total and daily view counts from WPP. If you have a very large dataset, it might be better to import total and daily counts separately.', 'top-10' ); ?></p> |
| 128 |
</td> |
| 129 |
</tr> |
| 130 |
<tr> |
| 131 |
<th scope="row"><?php esc_html_e( 'Minimum View Count', 'top-10' ); ?></th> |
| 132 |
<td> |
| 133 |
<input type="number" name="min_views" value="1" min="1" step="1"> |
| 134 |
<p class="description"><?php esc_html_e( 'Only import posts with at least this many views. Use this setting to filter out posts with very few views.', 'top-10' ); ?></p> |
| 135 |
</td> |
| 136 |
</tr> |
| 137 |
<tr> |
| 138 |
<th scope="row"><?php esc_html_e( 'Dry Run', 'top-10' ); ?></th> |
| 139 |
<td> |
| 140 |
<label> |
| 141 |
<input type="checkbox" name="dry_run" value="1" checked="checked"> |
| 142 |
<?php esc_html_e( 'Enable Dry Run (simulate import)', 'top-10' ); ?> |
| 143 |
</label> |
| 144 |
<p class="description"><?php esc_html_e( 'A dry run will simulate the import process without actually updating the database. This is useful for testing the import parameters.', 'top-10' ); ?></p> |
| 145 |
</td> |
| 146 |
</tr> |
| 147 |
<?php if ( is_multisite() && is_network_admin() ) : ?> |
| 148 |
<tr> |
| 149 |
<th scope="row"><?php esc_html_e( 'Select Sites', 'top-10' ); ?></th> |
| 150 |
<td> |
| 151 |
<?php |
| 152 |
$sites = get_sites(); |
| 153 |
if ( ! empty( $sites ) ) { |
| 154 |
foreach ( $sites as $site ) { |
| 155 |
$blog_details = get_blog_details( $site->blog_id ); |
| 156 |
printf( |
| 157 |
'<label><input type="checkbox" name="sites[]" value="%1$d"> %2$s (ID: %1$d)</label><br>', |
| 158 |
absint( $site->blog_id ), |
| 159 |
esc_html( $blog_details->blogname ) |
| 160 |
); |
| 161 |
} |
| 162 |
} |
| 163 |
?> |
| 164 |
</td> |
| 165 |
</tr> |
| 166 |
<?php endif; ?> |
| 167 |
</table> |
| 168 |
<?php submit_button( esc_html__( 'Run Import', 'top-10' ), 'primary', 'submit', true, array( 'id' => 'top-ten-wpp-import-submit' ) ); ?> |
| 169 |
|
| 170 |
<p id="top-ten-wpp-import-progress" class="hidden notice notice-info"> |
| 171 |
</p> |
| 172 |
<p id="top-ten-wpp-import-results" class="hidden notice notice-info"> |
| 173 |
</p> |
| 174 |
</form> |
| 175 |
<?php endif; ?> |
| 176 |
</div> |
| 177 |
<?php |
| 178 |
} |
| 179 |
/** |
| 180 |
* Process import parameters from a request array. |
| 181 |
* |
| 182 |
* @since 4.1.0 |
| 183 |
* @param array $request Request data array. |
| 184 |
* @return array Processed import parameters |
| 185 |
*/ |
| 186 |
private function process_import_parameters( array $request ): array { |
| 187 |
$parameters = array( |
| 188 |
'import_mode' => isset( $request['import_mode'] ) && 'merge' === sanitize_text_field( wp_unslash( $request['import_mode'] ) ) ? 'merge' : 'replace', |
| 189 |
'import_data' => isset( $request['import_data'] ) ? sanitize_text_field( wp_unslash( $request['import_data'] ) ) : 'total', |
| 190 |
'min_views' => isset( $request['min_views'] ) ? absint( $request['min_views'] ) : 1, |
| 191 |
'blog_id' => ! empty( $request['blog_id'] ) ? absint( $request['blog_id'] ) : get_current_blog_id(), |
| 192 |
'dry_run' => isset( $request['dry_run'] ) && ( 1 === absint( $request['dry_run'] ) || true === $request['dry_run'] ), |
| 193 |
); |
| 194 |
|
| 195 |
// Validate import_data is one of the allowed values. |
| 196 |
if ( ! in_array( $parameters['import_data'], array( 'total', 'daily', 'both' ), true ) ) { |
| 197 |
$parameters['import_data'] = 'total'; |
| 198 |
} |
| 199 |
|
| 200 |
return $parameters; |
| 201 |
} |
| 202 |
|
| 203 |
/** |
| 204 |
* Process AJAX import request. |
| 205 |
* |
| 206 |
* @since 4.1.0 |
| 207 |
* @return void |
| 208 |
*/ |
| 209 |
public function process_ajax_import(): void { |
| 210 |
check_ajax_referer( 'top_ten_import_wpp_nonce', 'nonce' ); |
| 211 |
|
| 212 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 213 |
wp_send_json_error( |
| 214 |
array( |
| 215 |
'message' => esc_html__( 'You do not have sufficient permissions to perform this action.', 'top-10' ), |
| 216 |
) |
| 217 |
); |
| 218 |
} |
| 219 |
|
| 220 |
// Validate WPP plugin and tables. |
| 221 |
if ( ! self::is_wpp_active() || ! self::wpp_tables_exist() ) { |
| 222 |
wp_send_json_error( |
| 223 |
array( |
| 224 |
'message' => esc_html__( 'WordPress Popular Posts plugin is not active or its tables are missing.', 'top-10' ), |
| 225 |
) |
| 226 |
); |
| 227 |
} |
| 228 |
|
| 229 |
// Get import options from AJAX request. |
| 230 |
$params = $this->process_import_parameters( $_POST ); |
| 231 |
|
| 232 |
// Determine sites to process. |
| 233 |
$sites = array(); |
| 234 |
$is_network_admin = is_multisite() && isset( $_POST['is_network_admin'] ) && '1' === $_POST['is_network_admin']; |
| 235 |
|
| 236 |
if ( isset( $_POST['sites'] ) && is_array( $_POST['sites'] ) ) { |
| 237 |
$sites = array_map( 'absint', $_POST['sites'] ); |
| 238 |
} elseif ( ! $is_network_admin ) { |
| 239 |
$sites[] = $params['blog_id']; |
| 240 |
} |
| 241 |
|
| 242 |
// No sites to process. |
| 243 |
if ( empty( $sites ) ) { |
| 244 |
wp_send_json_error( |
| 245 |
array( |
| 246 |
'message' => esc_html__( 'No sites selected for import.', 'top-10' ), |
| 247 |
) |
| 248 |
); |
| 249 |
} |
| 250 |
|
| 251 |
$import_results = array(); |
| 252 |
|
| 253 |
// Process each selected site. |
| 254 |
foreach ( $sites as $blog_id ) { |
| 255 |
$result = self::do_import_for_site( |
| 256 |
$blog_id, |
| 257 |
$params['import_mode'], |
| 258 |
$params['dry_run'], |
| 259 |
$params['import_data'], |
| 260 |
$params['min_views'] |
| 261 |
); |
| 262 |
|
| 263 |
$import_results[ $blog_id ] = $result; |
| 264 |
} |
| 265 |
|
| 266 |
// Send success response with dry_run flag and all results. |
| 267 |
wp_send_json_success( |
| 268 |
array( |
| 269 |
'message' => $params['dry_run'] ? esc_html__( 'Dry run completed successfully.', 'top-10' ) : esc_html__( 'Import completed successfully.', 'top-10' ), |
| 270 |
'results' => $import_results, |
| 271 |
'dry_run' => $params['dry_run'], |
| 272 |
'sites_processed' => count( $sites ), |
| 273 |
) |
| 274 |
); |
| 275 |
} |
| 276 |
|
| 277 |
/** |
| 278 |
* Check if WordPress Popular Posts plugin is active |
| 279 |
* |
| 280 |
* @since 4.1.0 |
| 281 |
* @return bool Whether WPP is active. |
| 282 |
*/ |
| 283 |
private function is_wpp_active(): bool { |
| 284 |
return defined( 'WPP_VERSION' ); |
| 285 |
} |
| 286 |
|
| 287 |
/** |
| 288 |
* Check if WPP tables exist in the database |
| 289 |
* |
| 290 |
* @since 4.1.0 |
| 291 |
* @return bool Whether WPP tables exist. |
| 292 |
*/ |
| 293 |
private function wpp_tables_exist(): bool { |
| 294 |
global $wpdb; |
| 295 |
|
| 296 |
$tables = array( |
| 297 |
$wpdb->prefix . 'popularpostsdata', |
| 298 |
$wpdb->prefix . 'popularpostssummary', |
| 299 |
); |
| 300 |
$key = implode( '|', $tables ); |
| 301 |
static $cache = array(); |
| 302 |
|
| 303 |
if ( array_key_exists( $key, $cache ) ) { |
| 304 |
return $cache[ $key ]; |
| 305 |
} |
| 306 |
|
| 307 |
$tables_exist = true; |
| 308 |
foreach ( $tables as $table ) { |
| 309 |
$query = $wpdb->prepare( 'SELECT 1 FROM %i LIMIT 0', $table ); |
| 310 |
$suppress_errors = $wpdb->suppress_errors(); |
| 311 |
|
| 312 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 313 |
if ( false === $wpdb->query( $query ) ) { |
| 314 |
$tables_exist = false; |
| 315 |
} |
| 316 |
|
| 317 |
$wpdb->suppress_errors( $suppress_errors ); |
| 318 |
} |
| 319 |
|
| 320 |
$cache[ $key ] = $tables_exist; |
| 321 |
|
| 322 |
return $tables_exist; |
| 323 |
} |
| 324 |
|
| 325 |
/** |
| 326 |
* Enqueues required scripts and styles. |
| 327 |
* |
| 328 |
* @since 4.1.0 |
| 329 |
* @return void |
| 330 |
*/ |
| 331 |
public function enqueue_scripts(): void { |
| 332 |
wp_enqueue_script( 'top-ten-wpp-importer-js' ); |
| 333 |
wp_localize_script( |
| 334 |
'top-ten-wpp-importer-js', |
| 335 |
'topTenWPPImporter', |
| 336 |
array( |
| 337 |
'nonce' => wp_create_nonce( 'top_ten_import_wpp_nonce' ), |
| 338 |
'ajaxurl' => admin_url( 'admin-ajax.php' ), |
| 339 |
'strings' => array( |
| 340 |
'confirm' => esc_html__( 'Are you sure you want to proceed with the import? This operation cannot be undone.', 'top-10' ), |
| 341 |
'importing' => esc_html__( 'Importing data...', 'top-10' ), |
| 342 |
'import_complete' => esc_html__( 'Import completed successfully!', 'top-10' ), |
| 343 |
'import_error' => esc_html__( 'An error occurred during import:', 'top-10' ), |
| 344 |
'cancel_confirm' => esc_html__( 'Are you sure you want to cancel the import? All progress will be lost.', 'top-10' ), |
| 345 |
'cancel_button' => esc_html__( 'Cancel Import', 'top-10' ), |
| 346 |
'import_cancelled' => esc_html__( 'Import cancelled by user.', 'top-10' ), |
| 347 |
'no_sites_selected' => esc_html__( 'Please select at least one site to import data from.', 'top-10' ), |
| 348 |
'invalid_min_views' => esc_html__( 'Minimum views must be a non-negative number.', 'top-10' ), |
| 349 |
'timeout_error' => esc_html__( 'Request timed out. The server might be processing a large amount of data. Try again with a smaller batch size or reduce the number of sites processed at once.', 'top-10' ), |
| 350 |
'processing_site' => esc_html__( 'Processing Site', 'top-10' ), |
| 351 |
'batch' => esc_html__( 'Batch', 'top-10' ), |
| 352 |
'of' => esc_html__( 'of', 'top-10' ), |
| 353 |
'dry_run' => esc_html__( 'Dry Run -', 'top-10' ), |
| 354 |
'sites_processed' => esc_html__( 'Sites Processed:', 'top-10' ), |
| 355 |
'blog_id' => esc_html__( 'Blog ID', 'top-10' ), |
| 356 |
'posts_processed' => esc_html__( 'Posts Processed:', 'top-10' ), |
| 357 |
'total_records' => esc_html__( 'Total Records:', 'top-10' ), |
| 358 |
'total_views_found' => esc_html__( 'Total Views Found:', 'top-10' ), |
| 359 |
'daily_records' => esc_html__( 'Daily Records:', 'top-10' ), |
| 360 |
'daily_views_found' => esc_html__( 'Daily Views Found:', 'top-10' ), |
| 361 |
'errors' => esc_html__( 'Errors:', 'top-10' ), |
| 362 |
'results' => esc_html__( 'Results:', 'top-10' ), |
| 363 |
'error' => esc_html__( 'Error:', 'top-10' ), |
| 364 |
'server_error' => esc_html__( 'Server error:', 'top-10' ), |
| 365 |
'unknown_error' => esc_html__( 'Unknown error occurred during import', 'top-10' ), |
| 366 |
'starting' => esc_html__( 'Starting...', 'top-10' ), |
| 367 |
), |
| 368 |
) |
| 369 |
); |
| 370 |
} |
| 371 |
|
| 372 |
/** |
| 373 |
* Handles the import form submission. |
| 374 |
* |
| 375 |
* @since 4.1.0 |
| 376 |
* |
| 377 |
* @return void |
| 378 |
*/ |
| 379 |
public function handle_import_request() { |
| 380 |
// Verify nonce. |
| 381 |
if ( ! isset( $_POST['top_ten_import_wpp_nonce_field'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['top_ten_import_wpp_nonce_field'] ) ), 'top_ten_import_wpp_nonce' ) ) { |
| 382 |
wp_die( esc_html__( 'Nonce verification failed', 'top-10' ) ); |
| 383 |
} |
| 384 |
|
| 385 |
// Check user capabilities. |
| 386 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 387 |
wp_die( esc_html__( 'You are not allowed to perform this action', 'top-10' ) ); |
| 388 |
} |
| 389 |
|
| 390 |
// Get import parameters using our helper method. |
| 391 |
$params = $this->process_import_parameters( $_POST ); |
| 392 |
|
| 393 |
// Determine sites. |
| 394 |
$sites = array(); |
| 395 |
$is_network_admin = is_multisite() && is_network_admin(); |
| 396 |
|
| 397 |
if ( isset( $_POST['sites'] ) && is_array( $_POST['sites'] ) ) { |
| 398 |
$sites = array_map( 'absint', $_POST['sites'] ); |
| 399 |
} elseif ( ! $is_network_admin ) { |
| 400 |
$sites[] = $params['blog_id']; |
| 401 |
} |
| 402 |
|
| 403 |
if ( empty( $sites ) ) { |
| 404 |
wp_die( esc_html__( 'No sites selected for import.', 'top-10' ) ); |
| 405 |
} |
| 406 |
|
| 407 |
$import_results = array(); |
| 408 |
|
| 409 |
// Process each selected site. |
| 410 |
foreach ( $sites as $blog_id ) { |
| 411 |
/** |
| 412 |
* Action fired before processing a site. |
| 413 |
* |
| 414 |
* @param int $blog_id The blog ID being processed. |
| 415 |
*/ |
| 416 |
do_action( 'top_10_importer_before', $blog_id ); |
| 417 |
|
| 418 |
$result = self::do_import_for_site( |
| 419 |
$blog_id, |
| 420 |
$params['import_mode'], |
| 421 |
$params['dry_run'], |
| 422 |
$params['import_data'], |
| 423 |
$params['min_views'] |
| 424 |
); |
| 425 |
|
| 426 |
$import_results[ $blog_id ] = $result; |
| 427 |
/** |
| 428 |
* Action fired after processing a site. |
| 429 |
* |
| 430 |
* @param int $blog_id The blog ID that was processed. |
| 431 |
* @param array $result The results array. |
| 432 |
*/ |
| 433 |
do_action( 'top_10_importer_after', $blog_id, $result ); |
| 434 |
} |
| 435 |
|
| 436 |
// Build summary message. |
| 437 |
$message = ''; |
| 438 |
foreach ( $import_results as $blog_id => $result ) { |
| 439 |
$blog_details = get_blog_details( $blog_id ); |
| 440 |
/* translators: 1. Blog ID. */ |
| 441 |
$blog_name = $blog_details ? $blog_details->blogname : sprintf( __( 'Blog ID %d', 'top-10' ), $blog_id ); |
| 442 |
$message .= sprintf( |
| 443 |
/* translators: 1: Site name, 2: Blog ID, 3: Total records processed, 4: Daily records processed, 5: Dry run notice */ |
| 444 |
__( 'Site %1$s (ID: %2$d): Total Counts Processed: %3$d, Daily Records Processed: %4$d%5$s', 'top-10' ), |
| 445 |
esc_html( $blog_name ), |
| 446 |
$blog_id, |
| 447 |
$result['total_counts'] ?? 0, |
| 448 |
$result['daily_counts'] ?? 0, |
| 449 |
$params['dry_run'] ? ' (' . esc_html__( 'Dry Run', 'top-10' ) . ')' : '' |
| 450 |
) . '<br>'; |
| 451 |
} |
| 452 |
|
| 453 |
$redirect_url = add_query_arg( 'top_ten_import_message', rawurlencode( $message ), wp_get_referer() ); |
| 454 |
wp_safe_redirect( $redirect_url ); |
| 455 |
exit; |
| 456 |
} |
| 457 |
|
| 458 |
/** |
| 459 |
* Performs the import for a single site. |
| 460 |
* |
| 461 |
* @since 4.1.0 |
| 462 |
* |
| 463 |
* @param int $blog_id Blog ID to import from. |
| 464 |
* @param string $import_mode Either 'merge' or 'replace'. |
| 465 |
* @param bool $dry_run Whether to simulate the import. |
| 466 |
* @param string $import_data Type of data to import: 'total', 'daily', or 'both'. |
| 467 |
* @param int $min_views Minimum number of views to import. |
| 468 |
* @return array Result counts and status. |
| 469 |
* @throws \Exception If there's an error during the import process. |
| 470 |
*/ |
| 471 |
private function do_import_for_site( int $blog_id, string $import_mode = 'replace', bool $dry_run = false, string $import_data = 'both', int $min_views = 1 ): array { |
| 472 |
global $wpdb; |
| 473 |
|
| 474 |
$results = array( |
| 475 |
'total_counts' => 0, |
| 476 |
'daily_counts' => 0, |
| 477 |
'posts_processed' => 0, |
| 478 |
'total_views_imported' => 0, |
| 479 |
'daily_views_imported' => 0, |
| 480 |
'errors' => array(), |
| 481 |
'status' => 'success', |
| 482 |
); |
| 483 |
|
| 484 |
// If multisite and blog is not current, switch context. |
| 485 |
$switched = false; |
| 486 |
if ( is_multisite() && get_current_blog_id() !== $blog_id ) { |
| 487 |
switch_to_blog( $blog_id ); |
| 488 |
$switched = true; |
| 489 |
} |
| 490 |
|
| 491 |
try { |
| 492 |
// Verify WPP tables exist. |
| 493 |
if ( ! self::wpp_tables_exist() ) { |
| 494 |
throw new \Exception( sprintf( 'WPP tables not found for blog ID: %d', $blog_id ) ); |
| 495 |
} |
| 496 |
|
| 497 |
// Process total counts if needed. |
| 498 |
if ( 'total' === $import_data || 'both' === $import_data ) { |
| 499 |
// Import total view counts using the efficient bulk method. |
| 500 |
if ( ! $dry_run ) { |
| 501 |
$total_import_result = self::import_total_counts( $import_mode, $min_views, $blog_id ); |
| 502 |
|
| 503 |
// Merge any errors from the total import. |
| 504 |
if ( ! empty( $total_import_result['errors'] ) ) { |
| 505 |
$results['errors'] = array_merge( $results['errors'], $total_import_result['errors'] ); |
| 506 |
} |
| 507 |
|
| 508 |
// Update the results counts. |
| 509 |
$results['total_views_imported'] = $total_import_result['views']; |
| 510 |
$results['total_counts'] = $total_import_result['rows_affected'] ?? 0; |
| 511 |
|
| 512 |
// Increment posts processed count. |
| 513 |
$results['posts_processed'] += $results['total_counts']; |
| 514 |
} else { |
| 515 |
// For dry run, just count records that would be imported. |
| 516 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 517 |
$count_results = $wpdb->get_row( |
| 518 |
$wpdb->prepare( |
| 519 |
"SELECT |
| 520 |
COUNT(DISTINCT postid) AS post_count, |
| 521 |
SUM(pageviews) AS total_views |
| 522 |
FROM ( |
| 523 |
SELECT |
| 524 |
postid, |
| 525 |
COUNT(*) AS pageviews |
| 526 |
FROM {$wpdb->prefix}popularpostsdata |
| 527 |
GROUP BY postid |
| 528 |
HAVING pageviews >= %d |
| 529 |
) AS subquery", |
| 530 |
$min_views |
| 531 |
) |
| 532 |
); |
| 533 |
|
| 534 |
$results['total_counts'] = $count_results->post_count ?? 0; |
| 535 |
$results['total_views_imported'] = $count_results->total_views ?? 0; |
| 536 |
$results['posts_processed'] += $results['total_counts']; |
| 537 |
} |
| 538 |
} |
| 539 |
|
| 540 |
// Process daily views. |
| 541 |
if ( 'daily' === $import_data || 'both' === $import_data ) { |
| 542 |
// Import daily view counts from WPP using the efficient bulk method. |
| 543 |
if ( ! $dry_run ) { |
| 544 |
$daily_import_result = self::import_daily_counts( $import_mode, $min_views, $blog_id ); |
| 545 |
|
| 546 |
// Merge any errors from the daily import. |
| 547 |
if ( ! empty( $daily_import_result['errors'] ) ) { |
| 548 |
$results['errors'] = array_merge( $results['errors'], $daily_import_result['errors'] ); |
| 549 |
} |
| 550 |
|
| 551 |
// Update the results counts. |
| 552 |
$results['daily_views_imported'] = $daily_import_result['views']; |
| 553 |
$results['daily_counts'] = $daily_import_result['rows_affected'] ?? 0; |
| 554 |
|
| 555 |
// Increment posts processed count (estimate based on rows affected). |
| 556 |
$results['posts_processed'] += $results['daily_counts']; |
| 557 |
} else { |
| 558 |
// For dry run, just count records that would be imported. |
| 559 |
$count_results = $wpdb->get_row(// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 560 |
$wpdb->prepare( |
| 561 |
"SELECT |
| 562 |
COUNT(*) AS total_entries, |
| 563 |
COUNT(DISTINCT postid) AS post_count, |
| 564 |
SUM(hourly_views) AS total_views |
| 565 |
FROM ( |
| 566 |
SELECT |
| 567 |
postid, |
| 568 |
DATE_FORMAT(MIN(view_datetime), '%%Y-%%m-%%d %%H:00:00') AS dp_date, |
| 569 |
COUNT(*) AS hourly_views |
| 570 |
FROM {$wpdb->prefix}popularpostssummary |
| 571 |
GROUP BY postid, DATE(view_datetime), HOUR(view_datetime) |
| 572 |
HAVING hourly_views >= %d |
| 573 |
) AS subquery", |
| 574 |
$min_views |
| 575 |
) |
| 576 |
); |
| 577 |
|
| 578 |
$results['daily_counts'] = $count_results->post_count ?? 0; |
| 579 |
$results['daily_views_imported'] = $count_results->total_views ?? 0; |
| 580 |
$results['posts_processed'] += $results['daily_counts']; |
| 581 |
} |
| 582 |
} |
| 583 |
} catch ( \Exception $e ) { |
| 584 |
$results['errors'][] = $e->getMessage(); |
| 585 |
$results['status'] = 'error'; |
| 586 |
} finally { |
| 587 |
// Restore original blog if switched. |
| 588 |
if ( $switched ) { |
| 589 |
restore_current_blog(); |
| 590 |
} |
| 591 |
} |
| 592 |
|
| 593 |
return $results; |
| 594 |
} |
| 595 |
|
| 596 |
/** |
| 597 |
* Efficiently import total counts using a bulk operation approach. |
| 598 |
* |
| 599 |
* @since 4.1.0 |
| 600 |
* @param string $import_mode Either 'merge' or 'replace'. |
| 601 |
* @param int $min_views Minimum number of views to import. |
| 602 |
* @param int $blog_id Blog ID for multisite. |
| 603 |
* @return array Result with views count and any errors. |
| 604 |
*/ |
| 605 |
private function import_total_counts( string $import_mode, int $min_views, int $blog_id ): array { |
| 606 |
global $wpdb; |
| 607 |
|
| 608 |
$result = array( |
| 609 |
'views' => 0, |
| 610 |
'errors' => array(), |
| 611 |
'rows_affected' => 0, |
| 612 |
); |
| 613 |
|
| 614 |
try { |
| 615 |
// Prepare a temporary table to hold the aggregated view counts. |
| 616 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 617 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange |
| 618 |
'CREATE TEMPORARY TABLE IF NOT EXISTS temp_total_views ( |
| 619 |
post_id BIGINT, |
| 620 |
total_views BIGINT |
| 621 |
)' |
| 622 |
); |
| 623 |
|
| 624 |
// Clear the temporary table if it exists. |
| 625 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 626 |
'TRUNCATE TABLE temp_total_views' |
| 627 |
); |
| 628 |
|
| 629 |
// Aggregate WPP views into total counts. |
| 630 |
$aggregate_query = $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 631 |
"INSERT INTO temp_total_views (post_id, total_views) |
| 632 |
SELECT |
| 633 |
postid, |
| 634 |
SUM(pageviews) AS total_views |
| 635 |
FROM {$wpdb->prefix}popularpostsdata |
| 636 |
GROUP BY postid |
| 637 |
HAVING total_views >= %d", |
| 638 |
$min_views |
| 639 |
); |
| 640 |
|
| 641 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 642 |
$wpdb->query( $aggregate_query ); |
| 643 |
|
| 644 |
// Get total views before proceeding. |
| 645 |
$total_views = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 646 |
'SELECT SUM(total_views) FROM temp_total_views' |
| 647 |
) ?? 0; |
| 648 |
$result['views'] = (int) $total_views; |
| 649 |
|
| 650 |
if ( 'merge' === $import_mode ) { |
| 651 |
// Merge query using temporary table. |
| 652 |
$merge_query = $wpdb->prepare( |
| 653 |
"INSERT INTO {$wpdb->base_prefix}top_ten (postnumber, cntaccess, blog_id) |
| 654 |
SELECT |
| 655 |
post_id, |
| 656 |
total_views, |
| 657 |
%d |
| 658 |
FROM temp_total_views |
| 659 |
ON DUPLICATE KEY UPDATE |
| 660 |
cntaccess = cntaccess + VALUES(cntaccess)", |
| 661 |
$blog_id |
| 662 |
); |
| 663 |
|
| 664 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 665 |
$rows_affected = $wpdb->query( $merge_query ); |
| 666 |
} else { // Replace mode |
| 667 |
// First, get a list of the post IDs we're about to import. |
| 668 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 669 |
$post_ids = $wpdb->get_col( 'SELECT DISTINCT post_id FROM temp_total_views' ); |
| 670 |
|
| 671 |
if ( ! empty( $post_ids ) ) { |
| 672 |
$post_ids_list = implode( ',', array_map( 'intval', $post_ids ) ); |
| 673 |
|
| 674 |
// Clear existing entries for these posts and this blog_id. |
| 675 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 676 |
$wpdb->prepare( |
| 677 |
"DELETE FROM {$wpdb->base_prefix}top_ten |
| 678 |
WHERE blog_id = %d AND postnumber IN ({$post_ids_list})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 679 |
$blog_id |
| 680 |
) |
| 681 |
); |
| 682 |
} |
| 683 |
|
| 684 |
// Then insert new data from temporary table. |
| 685 |
$insert_query = $wpdb->prepare( |
| 686 |
"INSERT INTO {$wpdb->base_prefix}top_ten (postnumber, cntaccess, blog_id) |
| 687 |
SELECT |
| 688 |
post_id, |
| 689 |
total_views, |
| 690 |
%d |
| 691 |
FROM temp_total_views", |
| 692 |
$blog_id |
| 693 |
); |
| 694 |
|
| 695 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 696 |
$rows_affected = $wpdb->query( $insert_query ); |
| 697 |
} |
| 698 |
|
| 699 |
// Store the number of rows affected. |
| 700 |
$result['rows_affected'] = false !== $rows_affected ? $rows_affected : 0; |
| 701 |
|
| 702 |
// Drop the temporary table. |
| 703 |
$wpdb->query( 'DROP TEMPORARY TABLE IF EXISTS temp_total_views' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange |
| 704 |
|
| 705 |
if ( false === $rows_affected ) { |
| 706 |
$result['errors'][] = __( 'Error importing total counts', 'top-10' ); |
| 707 |
} |
| 708 |
} catch ( \Exception $e ) { |
| 709 |
$result['errors'][] = $e->getMessage(); |
| 710 |
|
| 711 |
// Make sure we clean up the temporary table even if there's an error. |
| 712 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 713 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange |
| 714 |
'DROP TEMPORARY TABLE IF EXISTS temp_total_views' |
| 715 |
); |
| 716 |
} |
| 717 |
|
| 718 |
return $result; |
| 719 |
} |
| 720 |
|
| 721 |
/** |
| 722 |
* Efficiently import daily counts using a temporary table approach. |
| 723 |
* |
| 724 |
* @since 4.1.0 |
| 725 |
* @param string $import_mode Either 'merge' or 'replace'. |
| 726 |
* @param int $min_views Minimum number of views to import. |
| 727 |
* @param int $blog_id Blog ID for multisite. |
| 728 |
* @return array Result with views count and any errors. |
| 729 |
*/ |
| 730 |
private function import_daily_counts( string $import_mode, int $min_views, int $blog_id ): array { |
| 731 |
global $wpdb; |
| 732 |
|
| 733 |
$result = array( |
| 734 |
'views' => 0, |
| 735 |
'errors' => array(), |
| 736 |
'rows_affected' => 0, |
| 737 |
); |
| 738 |
|
| 739 |
try { |
| 740 |
// Prepare a temporary table to aggregate hourly views. |
| 741 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 742 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange |
| 743 |
'CREATE TEMPORARY TABLE IF NOT EXISTS temp_hourly_views ( |
| 744 |
post_id BIGINT, |
| 745 |
dp_date DATETIME, |
| 746 |
hourly_views BIGINT |
| 747 |
)' |
| 748 |
); |
| 749 |
|
| 750 |
// Clear the temporary table if it exists. |
| 751 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 752 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange |
| 753 |
'TRUNCATE TABLE temp_hourly_views' |
| 754 |
); |
| 755 |
|
| 756 |
// Aggregate WPP views into hourly counts. |
| 757 |
$aggregate_query = $wpdb->prepare( |
| 758 |
"INSERT INTO temp_hourly_views (post_id, dp_date, hourly_views) |
| 759 |
SELECT |
| 760 |
postid, |
| 761 |
DATE_FORMAT(MIN(view_datetime), '%%Y-%%m-%%d %%H:00:00') AS dp_date, |
| 762 |
COUNT(*) AS hourly_views |
| 763 |
FROM {$wpdb->prefix}popularpostssummary |
| 764 |
GROUP BY postid, DATE(view_datetime), HOUR(view_datetime) |
| 765 |
HAVING hourly_views >= %d", |
| 766 |
$min_views |
| 767 |
); |
| 768 |
|
| 769 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 770 |
$wpdb->query( $aggregate_query ); |
| 771 |
|
| 772 |
// Get total views before proceeding. |
| 773 |
$total_views = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 774 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange |
| 775 |
'SELECT SUM(hourly_views) FROM temp_hourly_views' |
| 776 |
) ?? 0; |
| 777 |
$result['views'] = (int) $total_views; |
| 778 |
|
| 779 |
if ( 'merge' === $import_mode ) { |
| 780 |
// Merge query using temporary table. |
| 781 |
$merge_query = $wpdb->prepare( |
| 782 |
"INSERT INTO {$wpdb->base_prefix}top_ten_daily (postnumber, cntaccess, dp_date, blog_id) |
| 783 |
SELECT |
| 784 |
post_id, |
| 785 |
hourly_views, |
| 786 |
dp_date, |
| 787 |
%d |
| 788 |
FROM temp_hourly_views |
| 789 |
ON DUPLICATE KEY UPDATE |
| 790 |
cntaccess = cntaccess + VALUES(cntaccess)", |
| 791 |
$blog_id |
| 792 |
); |
| 793 |
|
| 794 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 795 |
$rows_affected = $wpdb->query( $merge_query ); |
| 796 |
} else { // Replace mode |
| 797 |
// First, get a list of the post IDs we're about to import. |
| 798 |
$post_ids = $wpdb->get_col( 'SELECT DISTINCT post_id FROM temp_hourly_views' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 799 |
|
| 800 |
if ( ! empty( $post_ids ) ) { |
| 801 |
$post_ids_list = implode( ',', array_map( 'intval', $post_ids ) ); |
| 802 |
|
| 803 |
// Clear existing entries for these posts and this blog_id. |
| 804 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 805 |
$wpdb->prepare( |
| 806 |
"DELETE FROM {$wpdb->base_prefix}top_ten_daily |
| 807 |
WHERE blog_id = %d AND postnumber IN ({$post_ids_list})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 808 |
$blog_id |
| 809 |
) |
| 810 |
); |
| 811 |
} |
| 812 |
|
| 813 |
// Then insert new data from temporary table. |
| 814 |
$insert_query = $wpdb->prepare( |
| 815 |
"INSERT INTO {$wpdb->base_prefix}top_ten_daily (postnumber, cntaccess, dp_date, blog_id) |
| 816 |
SELECT |
| 817 |
post_id, |
| 818 |
hourly_views, |
| 819 |
dp_date, |
| 820 |
%d |
| 821 |
FROM temp_hourly_views", |
| 822 |
$blog_id |
| 823 |
); |
| 824 |
|
| 825 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 826 |
$rows_affected = $wpdb->query( $insert_query ); |
| 827 |
} |
| 828 |
|
| 829 |
// Store the number of rows affected. |
| 830 |
$result['rows_affected'] = false !== $rows_affected ? $rows_affected : 0; |
| 831 |
|
| 832 |
// Drop the temporary table. |
| 833 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 834 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange |
| 835 |
'DROP TEMPORARY TABLE IF EXISTS temp_hourly_views' |
| 836 |
); |
| 837 |
|
| 838 |
if ( false === $rows_affected ) { |
| 839 |
$result['errors'][] = __( 'Error importing daily counts', 'top-10' ); |
| 840 |
} |
| 841 |
} catch ( \Exception $e ) { |
| 842 |
$result['errors'][] = $e->getMessage(); |
| 843 |
|
| 844 |
// Make sure we clean up the temporary table even if there's an error. |
| 845 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared |
| 846 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange |
| 847 |
'DROP TEMPORARY TABLE IF EXISTS temp_hourly_views' |
| 848 |
); |
| 849 |
} |
| 850 |
|
| 851 |
return $result; |
| 852 |
} |
| 853 |
} |
| 854 |
|