PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.7.6
Post Views Counter v1.7.6
1.7.13 1.7.12 1.7.11 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.2 1.3.2.1 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.7.10 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9
post-views-counter / includes / class-import.php
post-views-counter / includes Last commit date
class-admin.php 4 months ago class-columns-modal.php 4 months ago class-columns.php 4 months ago class-counter.php 4 months ago class-crawler-detect.php 4 months ago class-cron.php 4 months ago class-dashboard.php 4 months ago class-frontend.php 4 months ago class-functions.php 4 months ago class-import.php 4 months ago class-integration-gutenberg.php 4 months ago class-integrations.php 4 months ago class-query.php 4 months ago class-settings-api.php 4 months ago class-settings-display.php 4 months ago class-settings-general.php 4 months ago class-settings-integrations.php 4 months ago class-settings-other.php 4 months ago class-settings-reports.php 4 months ago class-settings.php 4 months ago class-toolbar.php 4 months ago class-traffic-signals.php 4 months ago class-update.php 4 months ago class-widgets.php 4 months ago functions.php 4 months ago
class-import.php
1835 lines
1 <?php
2 // exit if accessed directly
3 if ( ! defined( 'ABSPATH' ) )
4 exit;
5
6 /**
7 * Post_Views_Counter_Import class.
8 *
9 * @class Post_Views_Counter_Import
10 */
11 class Post_Views_Counter_Import {
12
13 /**
14 * Import providers registry.
15 *
16 * @var array
17 */
18 private $import_providers = [];
19 private $import_provider_labels = [];
20 private $import_strategies = null;
21 private $default_import_strategy = 'merge';
22
23 /**
24 * Whether providers have been initialized.
25 *
26 * @var bool
27 */
28 private $import_providers_initialized = false;
29
30 /**
31 * Class constructor.
32 *
33 * @return void
34 */
35 public function __construct() {
36 // register import providers after translations are available
37 add_action( 'init', [ $this, 'initialize_import_providers' ], 5 );
38 }
39
40 /**
41 * Initialize import providers.
42 *
43 * @return void
44 */
45 public function initialize_import_providers() {
46 if ( $this->import_providers_initialized )
47 return;
48
49 $this->register_import_providers();
50 $this->import_providers_initialized = true;
51 }
52
53 /**
54 * Register import providers.
55 *
56 * @return void
57 */
58 private function register_import_providers() {
59 // custom meta key provider (always available)
60 $this->import_providers['custom_meta_key'] = [
61 'slug' => 'custom_meta_key',
62 'label' => __( 'Custom Meta Key', 'post-views-counter' ),
63 'supports' => [ 'total', 'post_types' ],
64 'is_available' => '__return_true',
65 'render' => [ $this, 'render_provider_custom_meta_key' ],
66 'sanitize' => [ $this, 'sanitize_provider_custom_meta_key' ],
67 'analyse' => [ $this, 'analyse_provider_custom_meta_key' ],
68 'import' => [ $this, 'import_provider_custom_meta_key' ]
69 ];
70
71 // wp-postviews provider (conditional)
72 $this->import_providers['wp_postviews'] = [
73 'slug' => 'wp_postviews',
74 'label' => 'WP-PostViews',
75 'supports' => [ 'total', 'post_types' ],
76 'is_available' => [ $this, 'is_wp_postviews_available' ],
77 'render' => [ $this, 'render_provider_wp_postviews' ],
78 'sanitize' => [ $this, 'sanitize_provider_wp_postviews' ],
79 'analyse' => [ $this, 'analyse_provider_wp_postviews' ],
80 'import' => [ $this, 'import_provider_wp_postviews' ]
81 ];
82
83 // statify provider (conditional)
84 $this->import_providers['statify'] = [
85 'slug' => 'statify',
86 'label' => 'Statify',
87 'supports' => [ 'total', 'yearly', 'monthly', 'weekly', 'daily', 'post_types', 'taxonomies', 'authors', 'other_pages' ],
88 'is_available' => [ $this, 'is_statify_available' ],
89 'render' => [ $this, 'render_provider_statify' ],
90 'sanitize' => [ $this, 'sanitize_provider_statify' ],
91 'analyse' => [ $this, 'analyse_provider_statify' ],
92 'import' => [ $this, 'import_provider_statify' ]
93 ];
94
95 // page views count provider (conditional)
96 $this->import_providers['page_views_count'] = [
97 'slug' => 'page_views_count',
98 'label' => 'Page Views Count',
99 'supports' => [ 'total', 'yearly', 'monthly', 'weekly', 'daily', 'post_types' ],
100 'is_available' => [ $this, 'is_page_views_count_available' ],
101 'render' => [ $this, 'render_provider_page_views_count' ],
102 'sanitize' => [ $this, 'sanitize_provider_page_views_count' ],
103 'analyse' => [ $this, 'analyse_provider_page_views_count' ],
104 'import' => [ $this, 'import_provider_page_views_count' ]
105 ];
106
107 // allow extensions to register additional providers without overriding core ones
108 $additional_providers = apply_filters( 'pvc_import_providers', [] );
109
110 if ( is_array( $additional_providers ) ) {
111 foreach ( $additional_providers as $slug => $provider ) {
112 if ( ! is_string( $slug ) || $slug === '' || isset( $this->import_providers[ $slug ] ) ) {
113 continue;
114 }
115
116 $this->import_providers[ $slug ] = $provider;
117 }
118 }
119
120 // ensure third-party providers have default supports
121 foreach ( $this->import_providers as $slug => $provider ) {
122 if ( ! isset( $provider['supports'] ) || ! is_array( $provider['supports'] ) ) {
123 $this->import_providers[ $slug ]['supports'] = [ 'total', 'post_types' ];
124 }
125 }
126
127 foreach ( $this->import_providers as $slug => $provider ) {
128 $this->import_provider_labels[ $slug ] = isset( $provider['label'] ) ? $provider['label'] : $slug;
129 }
130 }
131
132 /**
133 * Ensure import providers are loaded.
134 *
135 * @return void
136 */
137 private function ensure_import_providers_loaded() {
138 if ( ! $this->import_providers_initialized && did_action( 'init' ) )
139 $this->initialize_import_providers();
140 }
141
142 /**
143 * Get all import providers.
144 *
145 * @return array
146 */
147 public function get_all_providers() {
148 $this->ensure_import_providers_loaded();
149 return $this->import_providers;
150 }
151
152 /**
153 * Get available import providers.
154 *
155 * @return array
156 */
157 public function get_available_providers() {
158 $this->ensure_import_providers_loaded();
159
160 $available = [];
161
162 foreach ( $this->import_providers as $slug => $provider ) {
163 if ( is_callable( $provider['is_available'] ) && call_user_func( $provider['is_available'] ) ) {
164 $available[$slug] = $provider;
165 }
166 }
167
168 return $available;
169 }
170
171 /**
172 * Get a specific provider.
173 *
174 * @param string $slug
175 * @return array|null
176 */
177 public function get_provider( $slug ) {
178 $this->ensure_import_providers_loaded();
179 return isset( $this->import_providers[$slug] ) ? $this->import_providers[$slug] : null;
180 }
181
182 /**
183 * Get supports for a specific provider.
184 *
185 * @param string $slug
186 * @return array
187 */
188 public function get_provider_supports( $slug ) {
189 $provider = $this->get_provider( $slug );
190 $supports = $provider && isset( $provider['supports'] ) ? $provider['supports'] : [];
191 return apply_filters( 'pvc_import_provider_supports', $supports, $slug );
192 }
193
194 /**
195 * Get registered import strategies.
196 *
197 * @return array
198 */
199 public function get_import_strategies() {
200 if ( $this->import_strategies === null ) {
201 $this->import_strategies = [
202 'override' => [
203 'label' => __( 'Override existing views', 'post-views-counter' ),
204 'description' => __( 'Replace stored counts with the imported values.', 'post-views-counter' ),
205 'pro_only' => false,
206 ],
207 'merge' => [
208 'label' => __( 'Merge with existing views', 'post-views-counter' ),
209 'description' => __( 'Add imported counts on top of the existing values.', 'post-views-counter' ),
210 'pro_only' => false,
211 ],
212 'skip_existing' => [
213 'label' => __( 'Skip Existing', 'post-views-counter' ),
214 'description' => __( 'Only import data when the target record does not exist yet.', 'post-views-counter' ),
215 'pro_only' => true,
216 ],
217 'keep_higher_count' => [
218 'label' => __( 'Keep Higher Count', 'post-views-counter' ),
219 'description' => __( 'Keep whichever value is higher when comparing imported and stored counts.', 'post-views-counter' ),
220 'pro_only' => true,
221 ],
222 'fill_empty_only' => [
223 'label' => __( 'Fill Empty Counts', 'post-views-counter' ),
224 'description' => __( 'Only import data for posts or periods that currently store zero views.', 'post-views-counter' ),
225 'pro_only' => true,
226 ],
227 ];
228
229 /**
230 * Filter the available import strategies.
231 *
232 * @since 1.5.10
233 *
234 * @param array $strategies Strategy definitions.
235 * @param Post_Views_Counter_Import $importer Import handler instance.
236 */
237 $this->import_strategies = apply_filters( 'pvc_import_strategies', $this->import_strategies, $this );
238 }
239
240 return $this->import_strategies;
241 }
242
243 /**
244 * Get default import strategy.
245 *
246 * @return string
247 */
248 public function get_default_strategy() {
249 return $this->default_import_strategy;
250 }
251
252 /**
253 * Normalize import strategy against current availability.
254 *
255 * @param string $strategy
256 * @return string
257 */
258 public function normalize_strategy( $strategy ) {
259 $strategy = sanitize_key( $strategy );
260
261 if ( $this->is_strategy_enabled( $strategy ) ) {
262 return $strategy;
263 }
264
265 return $this->get_default_strategy();
266 }
267
268 /**
269 * Check if a strategy can be used in the current environment.
270 *
271 * @param string $strategy
272 * @return bool
273 */
274 public function is_strategy_enabled( $strategy ) {
275 $strategy = sanitize_key( $strategy );
276 $definition = $this->get_strategy_definition( $strategy );
277
278 if ( $definition === null ) {
279 return false;
280 }
281
282 if ( ! empty( $definition['pro_only'] ) && ! $this->is_pro_active() ) {
283 return false;
284 }
285
286 return true;
287 }
288
289 /**
290 * Get strategy definition.
291 *
292 * @param string $strategy
293 * @return array|null
294 */
295 private function get_strategy_definition( $strategy ) {
296 $strategy = sanitize_key( $strategy );
297 $strategies = $this->get_import_strategies();
298
299 return isset( $strategies[ $strategy ] ) ? $strategies[ $strategy ] : null;
300 }
301
302 /**
303 * Check if Post Views Counter Pro is active.
304 *
305 * @return bool
306 */
307 private function is_pro_active() {
308 return class_exists( 'Post_Views_Counter_Pro' );
309 }
310
311 /**
312 * Generate a description for a provider based on its supports.
313 *
314 * @param array $supports
315 * @return string
316 */
317 private function generate_provider_description( $supports ) {
318 $parts = [];
319
320 // Date periods
321 $dates = [];
322 if ( in_array( 'total', $supports, true ) ) {
323 $dates[] = _x( 'total', 'view_counts', 'post-views-counter' );
324 }
325 if ( in_array( 'yearly', $supports, true ) ) {
326 $dates[] = _x( 'yearly', 'view_counts', 'post-views-counter' );
327 }
328 if ( in_array( 'monthly', $supports, true ) ) {
329 $dates[] = _x( 'monthly', 'view_counts', 'post-views-counter' );
330 }
331 if ( in_array( 'weekly', $supports, true ) ) {
332 $dates[] = _x( 'weekly', 'view_counts', 'post-views-counter' );
333 }
334 if ( in_array( 'daily', $supports, true ) ) {
335 $dates[] = _x( 'daily', 'view_counts', 'post-views-counter' );
336 }
337
338 // Content types
339 $content_labels = [
340 'post_types' => _x( 'post types', 'view_counts', 'post-views-counter' ),
341 'taxonomies' => _x( 'taxonomies', 'view_counts', 'post-views-counter' ),
342 'authors' => _x( 'author archives', 'view_counts', 'post-views-counter' ),
343 'other_pages' => _x( 'other pages', 'view_counts', 'post-views-counter' ),
344 'traffic_sources' => _x( 'traffic sources', 'view_counts', 'post-views-counter' )
345 ];
346
347 $content = [];
348 $pro_only_keys = [ 'taxonomies', 'authors', 'other_pages', 'traffic_sources' ];
349 $content_keys = [ 'post_types' ];
350
351 foreach ( $content_keys as $key ) {
352 if ( in_array( $key, $supports, true ) && isset( $content_labels[ $key ] ) ) {
353 $content[] = $content_labels[ $key ];
354 }
355 }
356
357 if ( ! empty( $dates ) && ! empty( $content ) ) {
358 $parts[] = sprintf( __( 'Imports %s view counts for %s', 'post-views-counter' ), $this->format_list( $dates ), $this->format_list( $content ) );
359 } elseif ( ! empty( $dates ) ) {
360 $parts[] = sprintf( __( 'Imports %s view counts', 'post-views-counter' ), $this->format_list( $dates ) );
361 }
362
363 $pro_content = [];
364
365 foreach ( $pro_only_keys as $key ) {
366 if ( in_array( $key, $supports, true ) && isset( $content_labels[ $key ] ) ) {
367 $pro_content[] = $content_labels[ $key ];
368 }
369 }
370
371 if ( ! empty( $pro_content ) ) {
372 $parts[] = sprintf( __( 'PVC Pro additionally imports %s view counts', 'post-views-counter' ), $this->format_list( $pro_content ) );
373 }
374
375 if ( empty( $parts ) ) {
376 return '';
377 }
378
379 return implode( '. ', $parts ) . '.';
380 }
381
382 /**
383 * Format a list of items into a human-readable string.
384 *
385 * @param array $items
386 * @return string
387 */
388 private function format_list( $items ) {
389 if ( count( $items ) === 1 ) {
390 return $items[0];
391 }
392
393 $last = array_pop( $items );
394 return implode( ', ', $items ) . ' ' . _x( 'and', 'view_counts', 'post-views-counter' ) . ' ' . $last;
395 }
396
397 /**
398 * Handle manual import/analyse action.
399 *
400 * @param array $request
401 * @return array
402 */
403 public function handle_manual_action( $request ) {
404 // get provider selection
405 $provider_slug = isset( $request['pvc_import_provider'] ) ? sanitize_key( $request['pvc_import_provider'] ) : 'custom_meta_key';
406
407 // get import strategy and validate
408 $strategy = isset( $request['pvc_import_strategy'] ) ? $this->normalize_strategy( $request['pvc_import_strategy'] ) : $this->get_default_strategy();
409
410 // get provider inputs
411 $provider_inputs = isset( $request['pvc_import_provider_inputs'] ) ? $request['pvc_import_provider_inputs'] : [];
412
413 // get available providers
414 $providers = $this->get_available_providers();
415
416 // validate provider exists
417 if ( ! isset( $providers[$provider_slug] ) ) {
418 return [
419 'success' => false,
420 'message' => __( 'Invalid import provider selected.', 'post-views-counter' ),
421 'type' => 'error'
422 ];
423 }
424
425 $provider = $providers[$provider_slug];
426
427 // sanitize provider inputs
428 $sanitized_inputs = [];
429 if ( is_callable( $provider['sanitize'] ) ) {
430 $sanitized_inputs = call_user_func( $provider['sanitize'], $provider_inputs );
431 }
432
433 // get main instance
434 $pvc = Post_Views_Counter();
435
436 // preserve existing provider settings, only update current provider
437 $existing_settings = isset( $pvc->options['other']['import_provider_settings'] ) ? $pvc->options['other']['import_provider_settings'] : [];
438
439 $provider_settings = array_merge(
440 $existing_settings,
441 [
442 'provider' => $provider_slug,
443 'strategy' => $strategy,
444 $provider_slug => $sanitized_inputs
445 ]
446 );
447
448 $result = [];
449
450 // handle analyse
451 if ( isset( $request['post_views_counter_analyse_views'] ) ) {
452 if ( is_callable( $provider['analyse'] ) ) {
453 $analyse_result = call_user_func( $provider['analyse'], $sanitized_inputs );
454
455 if ( isset( $analyse_result['message'] ) ) {
456 $result = [
457 'success' => true,
458 'message' => $analyse_result['message'],
459 'type' => 'updated',
460 'provider_settings' => $provider_settings
461 ];
462 }
463 }
464 // handle import
465 } elseif ( isset( $request['post_views_counter_import_views'] ) ) {
466 if ( is_callable( $provider['import'] ) ) {
467 $import_result = call_user_func( $provider['import'], $sanitized_inputs, $strategy );
468
469 if ( isset( $import_result['success'] ) && $import_result['success'] ) {
470 $result = [
471 'success' => true,
472 'message' => $import_result['message'],
473 'type' => 'updated',
474 'provider_settings' => $provider_settings
475 ];
476 } else if ( isset( $import_result['message'] ) ) {
477 $result = [
478 'success' => false,
479 'message' => $import_result['message'],
480 'type' => isset( $import_result['success'] ) && ! $import_result['success'] ? 'updated' : 'error',
481 'provider_settings' => $provider_settings
482 ];
483 }
484 }
485 }
486
487 return $result;
488 }
489
490 /**
491 * Prepare provider settings from request.
492 *
493 * @param array $request
494 * @return array
495 */
496 public function prepare_provider_settings_from_request( $request ) {
497 // get existing provider settings or initialize
498 $pvc = Post_Views_Counter();
499 $existing_settings = isset( $pvc->options['other']['import_provider_settings'] ) ? $pvc->options['other']['import_provider_settings'] : [];
500
501 // check if provider inputs were submitted
502 if ( isset( $request['pvc_import_provider'], $request['pvc_import_provider_inputs'], $request['pvc_import_strategy'] ) ) {
503 $provider_slug = sanitize_key( $request['pvc_import_provider'] );
504 $provider_inputs = $request['pvc_import_provider_inputs'];
505 $strategy = $this->normalize_strategy( $request['pvc_import_strategy'] );
506
507 // get available providers
508 $providers = $this->get_available_providers();
509
510 // validate provider exists
511 if ( isset( $providers[$provider_slug] ) ) {
512 $provider = $providers[$provider_slug];
513
514 // sanitize provider inputs
515 $sanitized_inputs = [];
516 if ( is_callable( $provider['sanitize'] ) ) {
517 $sanitized_inputs = call_user_func( $provider['sanitize'], $provider_inputs );
518 }
519
520 // update provider settings
521 return array_merge(
522 $existing_settings,
523 [
524 'provider' => $provider_slug,
525 'strategy' => $strategy,
526 $provider_slug => $sanitized_inputs
527 ]
528 );
529 }
530 }
531
532 // preserve existing settings if not changed
533 return $existing_settings;
534 }
535
536 /**
537 * Check if WP-PostViews is available.
538 *
539 * @return bool
540 */
541 public function is_wp_postviews_available() {
542 return function_exists( 'the_views' );
543 }
544
545 /**
546 * Render custom meta key provider fields.
547 *
548 * @return string
549 */
550 public function render_provider_custom_meta_key() {
551 // get main instance
552 $pvc = Post_Views_Counter();
553
554 // get saved meta key or default
555 $meta_key = isset( $pvc->options['other']['import_provider_settings']['custom_meta_key']['meta_key'] ) ? $pvc->options['other']['import_provider_settings']['custom_meta_key']['meta_key'] : 'views';
556
557 // get provider
558 $provider = $this->get_provider( 'custom_meta_key' );
559
560 // generate description
561 $description = $this->generate_provider_description( $provider['supports'] ) . ' ' . esc_html__( 'Enter the meta key from which the views data is to be retrieved during import.', 'post-views-counter' );
562
563 $html = '
564 <div class="pvc-provider-fields">
565 <input type="text" id="pvc_import_meta_key" class="regular-text" name="pvc_import_provider_inputs[meta_key]" value="' . esc_attr( $meta_key ) . '" />
566 <p class="description">' . $description . '</p>
567 </div>';
568
569 return $html;
570 }
571
572 /**
573 * Sanitize custom meta key provider inputs.
574 *
575 * @param array $inputs
576 * @return array
577 */
578 public function sanitize_provider_custom_meta_key( $inputs ) {
579 $sanitized = [];
580
581 if ( isset( $inputs['meta_key'] ) ) {
582 $sanitized['meta_key'] = sanitize_key( $inputs['meta_key'] );
583 }
584
585 return $sanitized;
586 }
587
588 /**
589 * Analyse custom meta key provider.
590 *
591 * @global object $wpdb
592 *
593 * @param array $inputs
594 * @return array
595 */
596 public function analyse_provider_custom_meta_key( $inputs ) {
597 global $wpdb;
598
599 $meta_key = isset( $inputs['meta_key'] ) ? sanitize_key( $inputs['meta_key'] ) : 'views';
600
601 // get views data
602 $views = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM " . $wpdb->postmeta . " WHERE meta_key = %s AND meta_value > 0", $meta_key ), ARRAY_A );
603
604 if ( empty( $views ) ) {
605 return [
606 'count' => 0,
607 'message' => sprintf( __( 'No valid views data found for %s.', 'post-views-counter' ), sprintf( __( 'meta key: %s', 'post-views-counter' ), esc_html( $meta_key ) ) )
608 ];
609 }
610
611 // calculate total views
612 $total_views = 0;
613 foreach ( $views as $view ) {
614 $total_views += (int) $view['meta_value'];
615 }
616
617 $stats = [
618 'total_views' => $total_views,
619 'posts_processed' => count( $views ),
620 'source' => $this->get_provider_label( 'custom_meta_key' ),
621 'additional_info' => sprintf( __( 'Meta key "%s".', 'post-views-counter' ), esc_html( $meta_key ) )
622 ];
623
624 return [
625 'count' => count( $views ),
626 'message' => $this->generate_import_message( $stats, 'analyze' )
627 ];
628 }
629
630 /**
631 * Import custom meta key provider.
632 *
633 * @global object $wpdb
634 *
635 * @param array $inputs
636 * @param string $strategy
637 * @return array
638 */
639 public function import_provider_custom_meta_key( $inputs, $strategy ) {
640 global $wpdb;
641
642 $meta_key = isset( $inputs['meta_key'] ) ? sanitize_key( $inputs['meta_key'] ) : 'views';
643
644 // get views
645 $views = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM " . $wpdb->postmeta . " WHERE meta_key = %s AND meta_value > 0", $meta_key ), ARRAY_A, 0 );
646
647 if ( empty( $views ) ) {
648 return [
649 'success' => false,
650 'message' => __( 'No valid post data found to import.', 'post-views-counter' )
651 ];
652 }
653
654 $sql = [];
655 $totals_map = [];
656 $total_views = 0;
657
658 foreach ( $views as $view ) {
659 $post_id = (int) $view['post_id'];
660 $count = (int) $view['meta_value'];
661
662 $sql[] = $wpdb->prepare( "(%d, 4, 'total', %d)", $post_id, $count );
663 $total_views += $count;
664 $totals_map[ $post_id ] = ( isset( $totals_map[ $post_id ] ) ? $totals_map[ $post_id ] : 0 ) + $count;
665 }
666
667 $this->execute_provider_insert_query( $sql, $strategy, 'custom_meta_key' );
668
669 $stats = [
670 'total_views' => $total_views,
671 'posts_processed' => count( $totals_map ),
672 'source' => $this->get_provider_label( 'custom_meta_key' ),
673 'additional_info' => sprintf( __( 'Meta key "%s".', 'post-views-counter' ), esc_html( $meta_key ) )
674 ];
675
676 $this->apply_skip_statistics( $stats, $totals_map, $strategy );
677
678 return [
679 'success' => true,
680 'message' => $this->generate_import_message( $stats )
681 ];
682 }
683
684 /**
685 * Render WP-PostViews provider fields.
686 *
687 * @return string
688 */
689 public function render_provider_wp_postviews() {
690 // get provider
691 $provider = $this->get_provider( 'wp_postviews' );
692
693 // generate description
694 $description = $this->generate_provider_description( $provider['supports'] );
695
696 $html = '
697 <div class="pvc-provider-fields">
698 <p class="description">' . $description . '</p>
699 </div>';
700
701 return $html;
702 }
703
704 /**
705 * Sanitize WP-PostViews provider inputs.
706 *
707 * @param array $inputs
708 * @return array
709 */
710 public function sanitize_provider_wp_postviews( $inputs ) {
711 // no inputs needed for WP-PostViews
712 return [];
713 }
714
715 /**
716 * Analyse WP-PostViews provider.
717 *
718 * @global object $wpdb
719 *
720 * @param array $inputs
721 * @return array
722 */
723 public function analyse_provider_wp_postviews( $inputs ) {
724 global $wpdb;
725
726 // wp-postviews uses 'views' meta key
727 $views = $wpdb->get_results( "SELECT post_id, meta_value FROM " . $wpdb->postmeta . " WHERE meta_key = 'views' AND meta_value > 0", ARRAY_A );
728
729 if ( empty( $views ) ) {
730 return [
731 'count' => 0,
732 'message' => sprintf( __( 'No valid views data found for %s.', 'post-views-counter' ), 'WP-PostViews' )
733 ];
734 }
735
736 // calculate total views
737 $total_views = 0;
738 foreach ( $views as $view ) {
739 $total_views += (int) $view['meta_value'];
740 }
741
742 $stats = [
743 'total_views' => $total_views,
744 'posts_processed' => count( $views ),
745 'source' => $this->get_provider_label( 'wp_postviews' )
746 ];
747
748 return [
749 'count' => count( $views ),
750 'message' => $this->generate_import_message( $stats, 'analyze' )
751 ];
752 }
753
754 /**
755 * Import WP-PostViews provider.
756 *
757 * @global object $wpdb
758 *
759 * @param array $inputs
760 * @param string $strategy
761 * @return array
762 */
763 public function import_provider_wp_postviews( $inputs, $strategy ) {
764 global $wpdb;
765
766 // wp-postviews uses 'views' meta key
767 $views = $wpdb->get_results( "SELECT post_id, meta_value FROM " . $wpdb->postmeta . " WHERE meta_key = 'views' AND meta_value > 0", ARRAY_A, 0 );
768
769 if ( empty( $views ) ) {
770 return [
771 'success' => false,
772 'message' => __( 'No valid post data found to import.', 'post-views-counter' )
773 ];
774 }
775
776 $sql = [];
777 $totals_map = [];
778 $total_views = 0;
779
780 foreach ( $views as $view ) {
781 $post_id = (int) $view['post_id'];
782 $count = (int) $view['meta_value'];
783
784 $sql[] = $wpdb->prepare( "(%d, 4, 'total', %d)", $post_id, $count );
785 $total_views += $count;
786 $totals_map[ $post_id ] = ( isset( $totals_map[ $post_id ] ) ? $totals_map[ $post_id ] : 0 ) + $count;
787 }
788
789 $this->execute_provider_insert_query( $sql, $strategy, 'wp_postviews' );
790
791 $stats = [
792 'total_views' => $total_views,
793 'posts_processed' => count( $totals_map ),
794 'source' => $this->get_provider_label( 'wp_postviews' )
795 ];
796
797 $this->apply_skip_statistics( $stats, $totals_map, $strategy );
798
799 return [
800 'success' => true,
801 'message' => $this->generate_import_message( $stats )
802 ];
803 }
804
805 /**
806 * Check if Statify is available.
807 *
808 * @return bool
809 */
810 public function is_statify_available() {
811 global $wpdb;
812 $table = esc_sql( isset( $wpdb->statify ) ? $wpdb->statify : $wpdb->prefix . 'statify' );
813 return class_exists( 'Statify' ) && $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table ) ) === $table;
814 }
815
816 /**
817 * Render Statify provider fields.
818 *
819 * @return string
820 */
821 public function render_provider_statify() {
822 // get provider
823 $provider = $this->get_provider( 'statify' );
824
825 // generate description
826 $description = $this->generate_provider_description( $provider['supports'] );
827
828 $html = '
829 <div class="pvc-provider-fields">
830 <p class="description">' . $description . '</p>
831 </div>';
832
833 return $html;
834 }
835
836 /**
837 * Sanitize Statify provider inputs.
838 *
839 * @param array $inputs
840 * @return array
841 */
842 public function sanitize_provider_statify( $inputs ) {
843 // no inputs needed for Statify
844 return [];
845 }
846
847 /**
848 * Analyse Statify provider.
849 *
850 * @global object $wpdb
851 *
852 * @param array $inputs
853 * @return array
854 */
855 public function analyse_provider_statify( $inputs ) {
856 global $wpdb;
857
858 $table = esc_sql( isset( $wpdb->statify ) ? $wpdb->statify : $wpdb->prefix . 'statify' );
859 $pvc_settings = Post_Views_Counter()->options['general'];
860 $use_gmt = $pvc_settings['count_time'] === 'gmt';
861 $tracked_post_types = array_map( 'sanitize_key', (array) $pvc_settings['post_types_count'] );
862
863 // get aggregated data to analyze
864 $rows = $wpdb->get_results( "SELECT target, created, COUNT(*) AS views FROM `{$table}` GROUP BY target, created", ARRAY_A );
865
866 if ( empty( $rows ) ) {
867 return [
868 'count' => 0,
869 'message' => sprintf( __( 'No valid views data found for %s.', 'post-views-counter' ), 'Statify' )
870 ];
871 }
872
873 $stats = [];
874 $skipped_targets = [];
875 $total_views = 0;
876 $period_counts = [
877 'daily' => 0,
878 'weekly' => 0,
879 'monthly' => 0,
880 'yearly' => 0
881 ];
882
883 foreach ( $rows as $row ) {
884 $content = $this->map_target_to_content( $row['target'], $tracked_post_types, 'statify' );
885 $post_id = isset( $content['content_id'] ) ? (int) $content['content_id'] : 0;
886 if ( ! $post_id ) {
887 $skipped_targets[] = $row['target'];
888 continue;
889 }
890
891 $ts = strtotime( $row['created'] );
892 $period_keys = $this->get_period_keys_from_timestamp( $ts, $use_gmt );
893
894 if ( isset( $content['content_type'] ) && $content['content_type'] !== 'post' ) {
895 /**
896 * Allow to capture provider rows that map to non-post content.
897 *
898 * Returning true from this filter stops default processing for the row.
899 *
900 * @since 1.5.10
901 *
902 * @param bool $handled Whether the row was fully processed.
903 * @param array $context Contextual data about the provider row.
904 * @param Post_Views_Counter_Import $importer Import handler instance.
905 */
906 $handled = apply_filters( 'pvc_import_handle_non_post_row', false, [
907 'mode' => 'analyze',
908 'source' => 'statify',
909 'row' => $row,
910 'content' => $content,
911 'period_keys' => $period_keys,
912 'timestamp' => $ts,
913 'use_gmt' => $use_gmt
914 ], $this );
915
916 if ( $handled )
917 continue;
918
919 $skipped_targets[] = $row['target'];
920 continue;
921 }
922
923 $day_key = $period_keys['day'];
924 $week_key = $period_keys['week'];
925 $month_key = $period_keys['month'];
926 $year_key = $period_keys['year'];
927
928 if ( ! isset( $stats[$post_id] ) ) {
929 $stats[$post_id] = [
930 'daily' => [],
931 'weekly' => [],
932 'monthly' => [],
933 'yearly' => [],
934 'total' => 0
935 ];
936 }
937
938 if ( ! isset( $stats[$post_id]['daily'][$day_key] ) ) {
939 $period_counts['daily']++;
940 }
941 if ( ! isset( $stats[$post_id]['weekly'][$week_key] ) ) {
942 $period_counts['weekly']++;
943 }
944 if ( ! isset( $stats[$post_id]['monthly'][$month_key] ) ) {
945 $period_counts['monthly']++;
946 }
947 if ( ! isset( $stats[$post_id]['yearly'][$year_key] ) ) {
948 $period_counts['yearly']++;
949 }
950
951 $stats[$post_id]['daily'][$day_key] = ( $stats[$post_id]['daily'][$day_key] ?? 0 ) + $row['views'];
952 $stats[$post_id]['weekly'][$week_key] = ( $stats[$post_id]['weekly'][$week_key] ?? 0 ) + $row['views'];
953 $stats[$post_id]['monthly'][$month_key] = ( $stats[$post_id]['monthly'][$month_key] ?? 0 ) + $row['views'];
954 $stats[$post_id]['yearly'][$year_key] = ( $stats[$post_id]['yearly'][$year_key] ?? 0 ) + $row['views'];
955 $stats[$post_id]['total'] += $row['views'];
956 $total_views += $row['views'];
957 }
958
959 // prepare statistics for message generation
960 $provider_slug = 'statify';
961 $provider_label = isset( $this->import_provider_labels[ $provider_slug ] ) ? $this->import_provider_labels[ $provider_slug ] : $provider_slug;
962
963 $message_stats = [
964 'total_views' => $total_views,
965 'posts_processed' => count( $stats ),
966 'periods' => $period_counts,
967 'source' => $provider_label
968 ];
969
970 // add skipped URLs info if any
971 if ( ! empty( $skipped_targets ) ) {
972 $unique_skipped = array_unique( $skipped_targets );
973 $message_stats['additional_info'] = sprintf( __( 'Would skip %s non-post URLs.', 'post-views-counter' ), number_format_i18n( count( $unique_skipped ) ) );
974 }
975
976 /**
977 * Allow to adjust provider analysis statistics before displaying the message.
978 *
979 * @since 1.5.10
980 * @since 1.5.11 Provider parameter moved to the third position.
981 *
982 * @param array $message_stats Prepared statistics for the UI.
983 * @param array $context Additional context such as mode/source.
984 */
985 $message_stats = apply_filters( 'pvc_import_message_stats', $message_stats, [
986 'mode' => 'analyze',
987 'source' => 'statify'
988 ] );
989
990 return [
991 'count' => $total_views,
992 'message' => $this->generate_import_message( $message_stats, 'analyze' )
993 ];
994 }
995
996 /**
997 * Import Statify provider.
998 *
999 * @global object $wpdb
1000 *
1001 * @param array $inputs
1002 * @param string $strategy
1003 * @return array
1004 */
1005 public function import_provider_statify( $inputs, $strategy ) {
1006 global $wpdb;
1007
1008 $table = esc_sql( isset( $wpdb->statify ) ? $wpdb->statify : $wpdb->prefix . 'statify' );
1009
1010 $pvc_settings = Post_Views_Counter()->options['general'];
1011 $use_gmt = $pvc_settings['count_time'] === 'gmt';
1012 $tracked_post_types = array_map( 'sanitize_key', (array) $pvc_settings['post_types_count'] );
1013
1014 // Fetch aggregated data in chunks
1015 $offset = 0;
1016 $limit = 1000; // Adjust for memory
1017 $stats = [];
1018 $skipped_targets = [];
1019
1020 while ( true ) {
1021 $rows = $wpdb->get_results(
1022 $wpdb->prepare(
1023 "SELECT target, created, COUNT(*) AS views FROM `{$table}` GROUP BY target, created ORDER BY target, created LIMIT %d OFFSET %d",
1024 $limit, $offset
1025 ),
1026 ARRAY_A
1027 );
1028
1029 if ( empty( $rows ) ) break;
1030
1031 foreach ( $rows as $row ) {
1032 $content = $this->map_target_to_content( $row['target'], $tracked_post_types, 'statify' );
1033 $post_id = isset( $content['content_id'] ) ? (int) $content['content_id'] : 0;
1034 if ( ! $post_id ) {
1035 $skipped_targets[] = $row['target'];
1036 continue;
1037 }
1038
1039 $ts = strtotime( $row['created'] );
1040 $period_keys = $this->get_period_keys_from_timestamp( $ts, $use_gmt );
1041
1042 if ( isset( $content['content_type'] ) && $content['content_type'] !== 'post' ) {
1043 /** This filter is documented above. */
1044 $handled = apply_filters( 'pvc_import_handle_non_post_row', false, [
1045 'mode' => 'import',
1046 'source' => 'statify',
1047 'row' => $row,
1048 'content' => $content,
1049 'period_keys' => $period_keys,
1050 'timestamp' => $ts,
1051 'use_gmt' => $use_gmt,
1052 'strategy' => $strategy
1053 ], $this );
1054
1055 if ( $handled )
1056 continue;
1057
1058 $skipped_targets[] = $row['target'];
1059 continue;
1060 }
1061
1062 $day_key = $period_keys['day'];
1063 $week_key = $period_keys['week'];
1064 $month_key = $period_keys['month'];
1065 $year_key = $period_keys['year'];
1066
1067 if ( ! isset( $stats[$post_id] ) ) {
1068 $stats[$post_id] = [
1069 'daily' => [],
1070 'weekly' => [],
1071 'monthly' => [],
1072 'yearly' => [],
1073 'total' => 0
1074 ];
1075 }
1076
1077 $stats[$post_id]['daily'][$day_key] = ( $stats[$post_id]['daily'][$day_key] ?? 0 ) + $row['views'];
1078 $stats[$post_id]['weekly'][$week_key] = ( $stats[$post_id]['weekly'][$week_key] ?? 0 ) + $row['views'];
1079 $stats[$post_id]['monthly'][$month_key] = ( $stats[$post_id]['monthly'][$month_key] ?? 0 ) + $row['views'];
1080 $stats[$post_id]['yearly'][$year_key] = ( $stats[$post_id]['yearly'][$year_key] ?? 0 ) + $row['views'];
1081 $stats[$post_id]['total'] += $row['views'];
1082 }
1083
1084 $offset += $limit;
1085 }
1086
1087 // Batch insert and collect statistics
1088 $sql_parts = [];
1089 $total_views = 0;
1090 $posts_processed = 0;
1091 $period_counts = [
1092 'daily' => 0,
1093 'weekly' => 0,
1094 'monthly' => 0,
1095 'yearly' => 0
1096 ];
1097
1098 $totals_map = [];
1099
1100 foreach ( $stats as $post_id => $periods ) {
1101 $posts_processed++;
1102
1103 foreach ( $periods['daily'] as $period => $count ) {
1104 $sql_parts[] = $wpdb->prepare( "(%d, 0, %s, %d)", $post_id, $period, $count );
1105 $period_counts['daily']++;
1106 }
1107 foreach ( $periods['weekly'] as $period => $count ) {
1108 $sql_parts[] = $wpdb->prepare( "(%d, 1, %s, %d)", $post_id, $period, $count );
1109 $period_counts['weekly']++;
1110 }
1111 foreach ( $periods['monthly'] as $period => $count ) {
1112 $sql_parts[] = $wpdb->prepare( "(%d, 2, %s, %d)", $post_id, $period, $count );
1113 $period_counts['monthly']++;
1114 }
1115 foreach ( $periods['yearly'] as $period => $count ) {
1116 $sql_parts[] = $wpdb->prepare( "(%d, 3, %s, %d)", $post_id, $period, $count );
1117 $period_counts['yearly']++;
1118 }
1119 $sql_parts[] = $wpdb->prepare( "(%d, 4, 'total', %d)", $post_id, $periods['total'] );
1120 $total_views += $periods['total'];
1121 $totals_map[ $post_id ] = $periods['total'];
1122 }
1123
1124 if ( empty( $sql_parts ) ) {
1125 $debug_message = __( 'No valid post data found to import.', 'post-views-counter' );
1126
1127 // add helpful debug info
1128 if ( empty( $tracked_post_types ) ) {
1129 $debug_message .= ' ' . __( 'No post types are selected for tracking in settings.', 'post-views-counter' );
1130 } elseif ( ! empty( $skipped_targets ) ) {
1131 $unique_skipped = array_unique( $skipped_targets );
1132 $sample_targets = array_slice( $unique_skipped, 0, 5 );
1133 $debug_message .= ' ' . sprintf(
1134 __( 'All %s URLs were skipped. Sample URLs: %s', 'post-views-counter' ),
1135 number_format_i18n( count( $unique_skipped ) ),
1136 implode( ', ', array_map( 'esc_html', $sample_targets ) )
1137 );
1138 }
1139
1140 return [
1141 'success' => false,
1142 'message' => $debug_message
1143 ];
1144 }
1145
1146 $this->execute_provider_insert_query( $sql_parts, $strategy, 'statify' );
1147
1148 /**
1149 * Fires after a provider's post rows have been inserted.
1150 *
1151 * @since 1.5.10
1152 *
1153 * @param array $context {
1154 * @type string $strategy Selected merge strategy.
1155 * @type string $source Provider slug (statify).
1156 * @type bool $use_gmt Whether GMT dates were used.
1157 * }
1158 */
1159 do_action( 'pvc_import_after_provider', [
1160 'strategy' => $strategy,
1161 'source' => 'statify',
1162 'use_gmt' => $use_gmt
1163 ] );
1164
1165 $this->flush_pvc_caches();
1166
1167 // prepare statistics for message generation
1168 $provider_slug = 'statify';
1169 $provider_label = isset( $this->import_provider_labels[ $provider_slug ] ) ? $this->import_provider_labels[ $provider_slug ] : $provider_slug;
1170
1171 $message_stats = [
1172 'total_views' => $total_views,
1173 'posts_processed' => $posts_processed,
1174 'periods' => $period_counts,
1175 'source' => $provider_label
1176 ];
1177
1178 // add skipped URLs info if any
1179 if ( ! empty( $skipped_targets ) ) {
1180 $unique_skipped = array_unique( $skipped_targets );
1181 $message_stats['additional_info'] = sprintf( __( 'Skipped %s non-post URLs.', 'post-views-counter' ), number_format_i18n( count( $unique_skipped ) ) );
1182 }
1183
1184 $this->apply_skip_statistics( $message_stats, $totals_map, $strategy );
1185
1186 /**
1187 * Allow to adjust provider import statistics before displaying the message.
1188 *
1189 * @since 1.5.10
1190 *
1191 * @param array $stats Prepared statistics for the UI.
1192 * @param array $context Additional context such as mode/source.
1193 */
1194 $message_stats = apply_filters( 'pvc_import_message_stats', $message_stats, [
1195 'mode' => 'import',
1196 'source' => 'statify'
1197 ] );
1198
1199 return [
1200 'success' => true,
1201 'message' => $this->generate_import_message( $message_stats )
1202 ];
1203 }
1204
1205 /**
1206 * Check if Page Views Count is available.
1207 *
1208 * @return bool
1209 */
1210 public function is_page_views_count_available() {
1211 global $wpdb;
1212 $table_total = $wpdb->prefix . 'pvc_total';
1213 $table_daily = $wpdb->prefix . 'pvc_daily';
1214 return $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_total ) ) === $table_total &&
1215 $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_daily ) ) === $table_daily;
1216 }
1217
1218 /**
1219 * Render Page Views Count provider fields.
1220 *
1221 * @return string
1222 */
1223 public function render_provider_page_views_count() {
1224 // get provider
1225 $provider = $this->get_provider( 'page_views_count' );
1226
1227 // generate description
1228 $description = $this->generate_provider_description( $provider['supports'] );
1229
1230 $html = '
1231 <div class="pvc-provider-fields">
1232 <p class="description">' . $description . '</p>
1233 </div>';
1234
1235 return $html;
1236 }
1237
1238 /**
1239 * Sanitize Page Views Count provider inputs.
1240 *
1241 * @param array $inputs
1242 * @return array
1243 */
1244 public function sanitize_provider_page_views_count( $inputs ) {
1245 // no inputs needed for Page Views Count
1246 return [];
1247 }
1248
1249 /**
1250 * Analyse Page Views Count provider.
1251 *
1252 * @global object $wpdb
1253 *
1254 * @param array $inputs
1255 * @return array
1256 */
1257 public function analyse_provider_page_views_count( $inputs ) {
1258 global $wpdb;
1259
1260 $table_total = $wpdb->prefix . 'pvc_total';
1261 $table_daily = $wpdb->prefix . 'pvc_daily';
1262
1263 $pvc_settings = Post_Views_Counter()->options['general'];
1264 $tracked_post_types = array_map( 'sanitize_key', (array) $pvc_settings['post_types_count'] );
1265
1266 $post_types_cache = [];
1267
1268 // get total views
1269 $totals = $wpdb->get_results( "SELECT postnum, postcount FROM {$table_total}", ARRAY_A );
1270 $total_views = 0;
1271 $valid_posts_total = [];
1272 foreach ( $totals as $row ) {
1273 $post_id = (int) $row['postnum'];
1274 if ( ! isset( $post_types_cache[$post_id] ) ) {
1275 $post_types_cache[$post_id] = get_post_type( $post_id );
1276 }
1277 if ( $post_types_cache[$post_id] && in_array( $post_types_cache[$post_id], $tracked_post_types, true ) ) {
1278 $total_views += (int) $row['postcount'];
1279 $valid_posts_total[] = $post_id;
1280 }
1281 }
1282
1283 // get daily views
1284 $dailies = $wpdb->get_results( "SELECT time, postnum, postcount FROM {$table_daily}", ARRAY_A );
1285 $daily_views = 0;
1286 $valid_posts_daily = [];
1287 foreach ( $dailies as $row ) {
1288 $post_id = (int) $row['postnum'];
1289 if ( ! isset( $post_types_cache[$post_id] ) ) {
1290 $post_types_cache[$post_id] = get_post_type( $post_id );
1291 }
1292 if ( $post_types_cache[$post_id] && in_array( $post_types_cache[$post_id], $tracked_post_types, true ) ) {
1293 $daily_views += (int) $row['postcount'];
1294 $valid_posts_daily[] = $post_id;
1295 }
1296 }
1297
1298 $posts_processed = count( array_unique( array_merge( $valid_posts_total, $valid_posts_daily ) ) );
1299
1300 $stats = [
1301 'total_views' => $total_views,
1302 'posts_processed' => $posts_processed,
1303 'source' => $this->get_provider_label( 'page_views_count' ),
1304 ];
1305
1306 return [
1307 'count' => $total_views + $daily_views,
1308 'message' => $this->generate_import_message( $stats, 'analyze' )
1309 ];
1310 }
1311
1312 /**
1313 * Import Page Views Count provider.
1314 *
1315 * @global object $wpdb
1316 *
1317 * @param array $inputs
1318 * @param string $strategy
1319 * @return array
1320 */
1321 public function import_provider_page_views_count( $inputs, $strategy ) {
1322 global $wpdb;
1323
1324 $table_total = $wpdb->prefix . 'pvc_total';
1325 $table_daily = $wpdb->prefix . 'pvc_daily';
1326
1327 $pvc_settings = Post_Views_Counter()->options['general'];
1328 $tracked_post_types = array_map( 'sanitize_key', (array) $pvc_settings['post_types_count'] );
1329
1330 $post_types_cache = [];
1331
1332 $results = $wpdb->get_results(
1333 "SELECT t.postnum, t.postcount AS total, d.time, d.postcount AS daily_count
1334 FROM {$table_total} AS t
1335 LEFT JOIN {$table_daily} AS d ON t.postnum = d.postnum
1336 ORDER BY t.postnum, d.time",
1337 ARRAY_A
1338 );
1339
1340 $sql_parts = [];
1341 $stats = [];
1342 $totals_map = [];
1343 $total_views_imported = 0;
1344
1345 foreach ( $results as $row ) {
1346 $post_id = (int) $row['postnum'];
1347
1348 if ( ! isset( $post_types_cache[ $post_id ] ) ) {
1349 $post_types_cache[ $post_id ] = get_post_type( $post_id );
1350 }
1351
1352 $post_type = $post_types_cache[ $post_id ];
1353
1354 if ( ! $post_type || ! in_array( $post_type, $tracked_post_types, true ) ) {
1355 continue;
1356 }
1357
1358 if ( isset( $row['total'] ) && ! isset( $stats[ $post_id ]['total_imported'] ) ) {
1359 $count = (int) $row['total'];
1360 $total_views_imported += $count;
1361 $sql_parts[] = $wpdb->prepare( "(%d, 4, 'total', %d)", $post_id, $count );
1362 $totals_map[ $post_id ] = $count;
1363 $stats[ $post_id ]['total_imported'] = true;
1364 }
1365
1366 if ( empty( $row['time'] ) ) {
1367 continue;
1368 }
1369
1370 $ts = strtotime( $row['time'] . ' 00:00:00' );
1371 $period_keys = $this->get_period_keys_from_timestamp( $ts, false ); // Use site time as data is in site time
1372
1373 if ( ! isset( $stats[ $post_id ] ) ) {
1374 $stats[ $post_id ] = [
1375 'periods' => [
1376 'daily' => [],
1377 'weekly' => [],
1378 'monthly' => [],
1379 'yearly' => []
1380 ]
1381 ];
1382 }
1383
1384 $stats[ $post_id ]['periods']['daily'][ $period_keys['day'] ] = ( $stats[ $post_id ]['periods']['daily'][ $period_keys['day'] ] ?? 0 ) + (int) $row['daily_count'];
1385 $stats[ $post_id ]['periods']['weekly'][ $period_keys['week'] ] = ( $stats[ $post_id ]['periods']['weekly'][ $period_keys['week'] ] ?? 0 ) + (int) $row['daily_count'];
1386 $stats[ $post_id ]['periods']['monthly'][ $period_keys['month'] ] = ( $stats[ $post_id ]['periods']['monthly'][ $period_keys['month'] ] ?? 0 ) + (int) $row['daily_count'];
1387 $stats[ $post_id ]['periods']['yearly'][ $period_keys['year'] ] = ( $stats[ $post_id ]['periods']['yearly'][ $period_keys['year'] ] ?? 0 ) + (int) $row['daily_count'];
1388 }
1389
1390 if ( empty( $sql_parts ) ) {
1391 return [
1392 'success' => false,
1393 'message' => __( 'No valid post data found to import.', 'post-views-counter' )
1394 ];
1395 }
1396
1397 $period_counts = [
1398 'daily' => 0,
1399 'weekly' => 0,
1400 'monthly' => 0,
1401 'yearly' => 0
1402 ];
1403
1404 foreach ( $stats as $post_id => $data ) {
1405 foreach ( $data['periods']['daily'] as $period => $count ) {
1406 $sql_parts[] = $wpdb->prepare( "(%d, 0, %s, %d)", $post_id, $period, $count );
1407 $period_counts['daily']++;
1408 }
1409 foreach ( $data['periods']['weekly'] as $period => $count ) {
1410 $sql_parts[] = $wpdb->prepare( "(%d, 1, %s, %d)", $post_id, $period, $count );
1411 $period_counts['weekly']++;
1412 }
1413 foreach ( $data['periods']['monthly'] as $period => $count ) {
1414 $sql_parts[] = $wpdb->prepare( "(%d, 2, %s, %d)", $post_id, $period, $count );
1415 $period_counts['monthly']++;
1416 }
1417 foreach ( $data['periods']['yearly'] as $period => $count ) {
1418 $sql_parts[] = $wpdb->prepare( "(%d, 3, %s, %d)", $post_id, $period, $count );
1419 $period_counts['yearly']++;
1420 }
1421 }
1422
1423 $posts_processed = count( array_keys( $stats ) );
1424
1425 $this->execute_provider_insert_query( $sql_parts, $strategy, 'page_views_count' );
1426
1427 do_action( 'pvc_import_after_provider', [
1428 'strategy' => $strategy,
1429 'source' => 'page_views_count',
1430 'use_gmt' => false
1431 ] );
1432
1433 $this->flush_pvc_caches();
1434
1435 $provider_slug = 'page_views_count';
1436 $provider_label = isset( $this->import_provider_labels[ $provider_slug ] ) ? $this->import_provider_labels[ $provider_slug ] : $provider_slug;
1437
1438 $message_stats = [
1439 'total_views' => $total_views_imported,
1440 'posts_processed' => $posts_processed,
1441 'periods' => $period_counts,
1442 'source' => $provider_label
1443 ];
1444
1445 $this->apply_skip_statistics( $message_stats, $totals_map, $strategy );
1446
1447 $message_stats = apply_filters( 'pvc_import_message_stats', $message_stats, [
1448 'mode' => 'import',
1449 'source' => 'page_views_count'
1450 ] );
1451
1452 return [
1453 'success' => true,
1454 'message' => $this->generate_import_message( $message_stats )
1455 ];
1456 }
1457
1458 /**
1459 * Get SQL clause for the provided strategy.
1460 *
1461 * @param string $strategy
1462 * @param string $provider
1463 * @return string
1464 */
1465 private function get_strategy_on_duplicate_clause( $strategy, $provider ) {
1466 $strategy_key = sanitize_key( $strategy );
1467
1468 $clauses = [
1469 'override' => 'count = VALUES(count)',
1470 'merge' => 'count = count + VALUES(count)'
1471 ];
1472
1473 $clause = isset( $clauses[ $strategy_key ] ) ? $clauses[ $strategy_key ] : $clauses['merge'];
1474
1475 /**
1476 * Filter the SQL ON DUPLICATE KEY UPDATE clause used during import.
1477 *
1478 * @since 1.5.10
1479 *
1480 * @param string $clause SQL clause for the duplicate key handler.
1481 * @param array $context {
1482 * @type string $strategy Selected strategy key.
1483 * @type string $provider Provider slug.
1484 * }
1485 * @param Post_Views_Counter_Import $importer Import handler instance.
1486 */
1487 return apply_filters( 'pvc_import_strategy_clause', $clause, [
1488 'strategy' => $strategy_key,
1489 'provider' => $provider
1490 ], $this );
1491 }
1492
1493 /**
1494 * Execute a provider insert query.
1495 *
1496 * @param array $sql_parts Prepared SQL value tuples.
1497 * @param string $strategy Selected strategy.
1498 * @param string $provider Provider slug.
1499 * @return int|false Number of rows affected by the query or false when skipped.
1500 */
1501 private function execute_provider_insert_query( $sql_parts, $strategy, $provider ) {
1502 global $wpdb;
1503
1504 if ( empty( $sql_parts ) ) {
1505 return false;
1506 }
1507
1508 $on_duplicate = $this->get_strategy_on_duplicate_clause( $strategy, $provider );
1509 $query = "INSERT INTO {$wpdb->prefix}post_views (id, type, period, count) VALUES " . implode( ',', $sql_parts ) . " ON DUPLICATE KEY UPDATE {$on_duplicate}";
1510
1511 /**
1512 * Filter the SQL query used when inserting provider data.
1513 *
1514 * Returning false will skip executing the default query. Extensions can run
1515 * their custom SQL before returning false.
1516 *
1517 * @since 1.5.10
1518 *
1519 * @param string|false $query SQL query string or false to skip execution.
1520 * @param array $context {
1521 * @type string $strategy Selected strategy.
1522 * @type string $provider Provider slug.
1523 * @type array $sql_parts Prepared SQL tuples.
1524 * @type string $on_duplicate Default ON DUPLICATE KEY clause.
1525 * }
1526 * @param Post_Views_Counter_Import $importer Import handler instance.
1527 */
1528 $query = apply_filters( 'pvc_import_provider_query', $query, [
1529 'strategy' => sanitize_key( $strategy ),
1530 'provider' => $provider,
1531 'sql_parts' => $sql_parts,
1532 'on_duplicate' => $on_duplicate
1533 ], $this );
1534
1535 if ( $query === false ) {
1536 return false;
1537 }
1538
1539 return $wpdb->query( $query );
1540 }
1541
1542 /**
1543 * Adjust import statistics to include skipped totals information.
1544 *
1545 * @param array $stats
1546 * @param array $totals_map
1547 * @param string $strategy
1548 * @return void
1549 */
1550 private function apply_skip_statistics( &$stats, $totals_map, $strategy ) {
1551 if ( empty( $stats ) || empty( $totals_map ) ) {
1552 return;
1553 }
1554
1555 $skip_stats = $this->calculate_total_skip_stats( $totals_map, $strategy );
1556
1557 if ( empty( $skip_stats ) ) {
1558 return;
1559 }
1560
1561 $stats['skipped'] = $skip_stats;
1562
1563 if ( isset( $stats['total_views'] ) ) {
1564 $stats['total_views'] = max( 0, (int) $stats['total_views'] - $skip_stats['views'] );
1565 }
1566
1567 if ( isset( $stats['posts_processed'] ) ) {
1568 $stats['posts_processed'] = max( 0, (int) $stats['posts_processed'] - $skip_stats['posts'] );
1569 }
1570 }
1571
1572 /**
1573 * Calculate how many totals will be skipped by the given strategy.
1574 *
1575 * @param array $totals_map Array of post_id => total_count.
1576 * @param string $strategy
1577 * @return array
1578 */
1579 private function calculate_total_skip_stats( $totals_map, $strategy ) {
1580 $strategy = sanitize_key( $strategy );
1581 $advanced_strategies = [ 'skip_existing', 'keep_higher_count', 'fill_empty_only' ];
1582
1583 if ( empty( $totals_map ) || ! in_array( $strategy, $advanced_strategies, true ) ) {
1584 return [];
1585 }
1586
1587 $existing = $this->get_existing_total_counts( array_keys( $totals_map ) );
1588 $skipped_views = 0;
1589 $skipped_posts = 0;
1590
1591 foreach ( $totals_map as $post_id => $value ) {
1592 $current = isset( $existing[ $post_id ] ) ? (int) $existing[ $post_id ] : null;
1593 $skip = false;
1594
1595 if ( $strategy === 'skip_existing' && $current !== null ) {
1596 $skip = true;
1597 } elseif ( $strategy === 'keep_higher_count' && $current !== null && $current >= $value ) {
1598 $skip = true;
1599 } elseif ( $strategy === 'fill_empty_only' && $current !== null && $current > 0 ) {
1600 $skip = true;
1601 }
1602
1603 if ( $skip ) {
1604 $skipped_views += (int) $value;
1605 $skipped_posts++;
1606 }
1607 }
1608
1609 if ( $skipped_posts === 0 ) {
1610 return [];
1611 }
1612
1613 return [
1614 'views' => $skipped_views,
1615 'posts' => $skipped_posts
1616 ];
1617 }
1618
1619 /**
1620 * Get existing total counts for selected posts.
1621 *
1622 * @param array $post_ids
1623 * @return array
1624 */
1625 private function get_existing_total_counts( $post_ids ) {
1626 global $wpdb;
1627
1628 $post_ids = array_filter( array_map( 'intval', (array) $post_ids ) );
1629 $post_ids = array_values( array_unique( $post_ids ) );
1630
1631 if ( empty( $post_ids ) ) {
1632 return [];
1633 }
1634
1635 $existing = [];
1636
1637 foreach ( array_chunk( $post_ids, 500 ) as $chunk ) {
1638 $placeholders = implode( ',', array_fill( 0, count( $chunk ), '%d' ) );
1639 $sql = $wpdb->prepare(
1640 "SELECT id, count FROM {$wpdb->prefix}post_views WHERE type = 4 AND period = 'total' AND id IN ({$placeholders})",
1641 $chunk
1642 );
1643 $rows = $wpdb->get_results( $sql, ARRAY_A );
1644
1645 foreach ( (array) $rows as $row ) {
1646 $existing[ (int) $row['id'] ] = (int) $row['count'];
1647 }
1648 }
1649
1650 return $existing;
1651 }
1652
1653 /**
1654 * Generate import/analyze message with statistics.
1655 *
1656 * @param array $stats Import statistics
1657 * @param string $mode Mode: 'import' or 'analyze'
1658 * @return string
1659 */
1660 private function generate_import_message( $stats, $mode = 'import' ) {
1661 $message_parts = [];
1662
1663 $source_label = '';
1664
1665 if ( ! empty( $stats['source'] ) ) {
1666 $source_label = $stats['source'];
1667 }
1668
1669 if ( $source_label !== '' ) {
1670 if ( $mode === 'analyze' ) {
1671 $message_parts[] = sprintf( __( 'Analysis of %s:', 'post-views-counter' ), $source_label );
1672 } else {
1673 $message_parts[] = sprintf( __( 'Import from %s:', 'post-views-counter' ), $source_label );
1674 }
1675 }
1676
1677 // main success message
1678 if ( isset( $stats['total_views'] ) && isset( $stats['posts_processed'] ) ) {
1679 if ( $mode === 'analyze' ) {
1680 $message_parts[] = sprintf(
1681 __( 'Found %s total views for %s posts.', 'post-views-counter' ),
1682 number_format_i18n( $stats['total_views'] ),
1683 number_format_i18n( $stats['posts_processed'] )
1684 );
1685 } else {
1686 $message_parts[] = sprintf(
1687 __( 'Successfully imported %s total views for %s posts.', 'post-views-counter' ),
1688 number_format_i18n( $stats['total_views'] ),
1689 number_format_i18n( $stats['posts_processed'] )
1690 );
1691 }
1692 }
1693
1694 // additional info (like skipped URLs)
1695 if ( ! empty( $stats['additional_info'] ) ) {
1696 $message_parts[] = $stats['additional_info'];
1697 }
1698
1699 if ( isset( $stats['skipped']['views'], $stats['skipped']['posts'] ) ) {
1700 $message_parts[] = sprintf(
1701 __( 'Skipped %1$s total views for %2$s posts.', 'post-views-counter' ),
1702 number_format_i18n( $stats['skipped']['views'] ),
1703 number_format_i18n( $stats['skipped']['posts'] )
1704 );
1705 }
1706
1707 return implode( ' ', $message_parts );
1708 }
1709
1710 /**
1711 * Map provider target URL to content data.
1712 *
1713 * @param string $target
1714 * @param array $tracked_post_types
1715 * @param string $provider
1716 * @return array
1717 */
1718 private function map_target_to_content( $target, $tracked_post_types, $provider ) {
1719 $content = [
1720 'content_type' => 'post',
1721 'content_id' => $this->map_target_to_post_id( $target, $tracked_post_types )
1722 ];
1723
1724 /**
1725 * Filter provider content mapping so it's possible to process non-post URLs.
1726 *
1727 * @since 1.5.10
1728 *
1729 * @param array $content {
1730 * @type string $content_type Resolved content type.
1731 * @type int $content_id Target identifier.
1732 * }
1733 * @param string $target Target path recorded by the provider.
1734 * @param string $provider Current import provider slug.
1735 * @param array $tracked_post_types Allowed post types for PVC.
1736 * @param Post_Views_Counter_Import $importer Import handler instance.
1737 */
1738 return apply_filters( 'pvc_import_map_target_to_content', $content, $target, $provider, $tracked_post_types, $this );
1739 }
1740
1741 /**
1742 * Map Statify target URL to post ID.
1743 *
1744 * @param string $target
1745 * @param array $tracked_post_types
1746 * @return int
1747 */
1748 private function map_target_to_post_id( $target, $tracked_post_types ) {
1749 // handle empty tracked post types
1750 if ( empty( $tracked_post_types ) ) {
1751 return 0;
1752 }
1753
1754 // handle homepage
1755 if ( $target === '/' ) {
1756 $post_id = get_option( 'page_on_front' );
1757 if ( $post_id && in_array( get_post_type( $post_id ), $tracked_post_types, true ) ) {
1758 return (int) $post_id;
1759 }
1760 }
1761
1762 // build full URL from relative target path
1763 $url = home_url( $target );
1764 $post_id = url_to_postid( $url );
1765
1766 // verify post exists and is a tracked type
1767 if ( $post_id ) {
1768 $post_type = get_post_type( $post_id );
1769 if ( $post_type && in_array( $post_type, $tracked_post_types, true ) ) {
1770 return (int) $post_id;
1771 }
1772 }
1773
1774 return 0;
1775 }
1776
1777 /**
1778 * Flush caches specific to Post Views Counter.
1779 *
1780 * @return void
1781 */
1782 private function flush_pvc_caches() {
1783 global $wp_object_cache;
1784
1785 if ( ! is_object( $wp_object_cache ) || ! property_exists( $wp_object_cache, 'cache' ) ) {
1786 return;
1787 }
1788
1789 $groups = [ 'pvc', 'pvc-get_post_views', 'pvc-get_views' ];
1790
1791 foreach ( $groups as $group ) {
1792 if ( empty( $wp_object_cache->cache[$group] ) || ! is_array( $wp_object_cache->cache[$group] ) ) {
1793 continue;
1794 }
1795
1796 foreach ( array_keys( $wp_object_cache->cache[$group] ) as $key ) {
1797 wp_cache_delete( $key, $group );
1798 }
1799 }
1800 }
1801
1802 /**
1803 * Retrieve the human readable label for a provider.
1804 *
1805 * @param string $slug
1806 * @return string
1807 */
1808 private function get_provider_label( $slug ) {
1809 if ( isset( $this->import_provider_labels[ $slug ] ) ) {
1810 return $this->import_provider_labels[ $slug ];
1811 }
1812
1813 return ucwords( str_replace( '_', ' ', $slug ) );
1814 }
1815
1816 /**
1817 * Build period keys for a timestamp.
1818 *
1819 * @param int $timestamp
1820 * @param bool $use_gmt
1821 * @return array
1822 */
1823 private function get_period_keys_from_timestamp( $timestamp, $use_gmt ) {
1824 $date = $use_gmt ? gmdate( 'W-d-m-Y-o', $timestamp ) : date( 'W-d-m-Y-o', $timestamp );
1825 $parts = explode( '-', $date );
1826
1827 return [
1828 'day' => $parts[3] . $parts[2] . $parts[1],
1829 'week' => $parts[4] . $parts[0],
1830 'month' => $parts[3] . $parts[2],
1831 'year' => $parts[3]
1832 ];
1833 }
1834 }
1835