PluginProbe ʕ •ᴥ•ʔ
WP Popular Posts / 6.4.2
WP Popular Posts v6.4.2
4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 5.0.0 5.0.1 5.0.2 5.1.0 5.2.0 5.2.1 5.2.2 5.2.3 5.2.4 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 6.0.0 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.1.0 6.1.1 6.1.2 6.1.3 6.1.4 6.2.0 6.2.1 6.3.0 6.3.1 6.3.2 6.3.3 6.3.4 6.4.0 6.4.1 6.4.2 7.0.0 7.0.1 7.1.0 7.2.0 7.3.0 7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 7.3.6 7.3.7 7.3.8 7.4.0 trunk 2.3.7 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.2 4.0.3 4.0.5 4.0.6
wordpress-popular-posts / src / Admin / Admin.php
wordpress-popular-posts / src / Admin Last commit date
Admin.php 2 years ago admin-page.php 2 years ago screen-debug.php 2 years ago screen-stats.php 2 years ago screen-tools.php 2 years ago
Admin.php
1519 lines
1 <?php
2 /**
3 * The admin-facing functionality of the plugin.
4 *
5 * Defines hooks to enqueue the admin-specific stylesheet and JavaScript,
6 * plugin settings and other admin stuff.
7 *
8 * @package WordPressPopularPosts
9 * @subpackage WordPressPopularPosts/Admin
10 * @author Hector Cabrera <me@cabrerahector.com>
11 */
12
13 namespace WordPressPopularPosts\Admin;
14
15 use WordPressPopularPosts\{Helper, Image, Output, Query};
16
17 class Admin {
18
19 /**
20 * Slug of the plugin screen.
21 *
22 * @since 3.0.0
23 * @var string
24 */
25 protected $screen_hook_suffix = null;
26
27 /**
28 * Plugin options.
29 *
30 * @var array $config
31 * @access private
32 */
33 private $config;
34
35 /**
36 * Image object
37 *
38 * @since 4.0.2
39 * @var WordPressPopularPosts\Image
40 */
41 private $thumbnail;
42
43 /**
44 * Construct.
45 *
46 * @since 5.0.0
47 * @param array $config Admin settings.
48 * @param \WordPressPopularPosts\Image $thumbnail Image class.
49 */
50 public function __construct(array $config, Image $thumbnail)
51 {
52 $this->config = $config;
53 $this->thumbnail = $thumbnail;
54
55 // Delete old data on demand
56 if ( 1 == $this->config['tools']['log']['limit'] ) {
57 if ( ! wp_next_scheduled('wpp_cache_event') ) {
58 $midnight = strtotime('midnight') - ( get_option('gmt_offset') * HOUR_IN_SECONDS ) + DAY_IN_SECONDS;
59 wp_schedule_event($midnight, 'daily', 'wpp_cache_event');
60 }
61 } else {
62 // Remove the scheduled event if exists
63 $timestamp = wp_next_scheduled('wpp_cache_event');
64
65 if ( $timestamp ) {
66 wp_unschedule_event($timestamp, 'wpp_cache_event');
67 }
68 }
69
70 // Allow WP themers / coders to override data sampling status (active/inactive)
71 $this->config['tools']['sampling']['active'] = apply_filters('wpp_data_sampling', $this->config['tools']['sampling']['active']);
72
73 if (
74 ! ( wp_using_ext_object_cache() && defined('WPP_CACHE_VIEWS') && WPP_CACHE_VIEWS ) // Not using a persistent object cache
75 && ! $this->config['tools']['sampling']['active'] // Not using Data Sampling
76 ) {
77 // Schedule performance nag
78 if ( ! wp_next_scheduled('wpp_maybe_performance_nag') ) {
79 wp_schedule_event(time(), 'hourly', 'wpp_maybe_performance_nag');
80 }
81 } else {
82 // Remove the scheduled performance nag if found
83 $timestamp = wp_next_scheduled('wpp_maybe_performance_nag');
84
85 if ( $timestamp ) {
86 wp_unschedule_event($timestamp, 'wpp_maybe_performance_nag');
87 }
88 }
89 }
90
91 /**
92 * WordPress public-facing hooks.
93 *
94 * @since 5.0.0
95 */
96 public function hooks()
97 {
98 // Upgrade check
99 add_action('init', [$this, 'upgrade_check']);
100 // Hook fired when a new blog is activated on WP Multisite
101 add_action('wpmu_new_blog', [$this, 'activate_new_site']);
102 // Hook fired when a blog is deleted on WP Multisite
103 add_filter('wpmu_drop_tables', [$this, 'delete_site_data'], 10, 2);
104 // At-A-Glance
105 add_filter('dashboard_glance_items', [$this, 'at_a_glance_stats']);
106 add_action('admin_head', [$this, 'at_a_glance_stats_css']);
107 // Dashboard Trending Now widget
108 add_action('wp_dashboard_setup', [$this, 'add_dashboard_widgets']);
109 // Load WPP's admin styles and scripts
110 add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
111 // Add admin screen
112 add_action('admin_menu', [$this, 'add_plugin_admin_menu']);
113 // Contextual help
114 add_action('admin_head', [$this, 'add_contextual_help']);
115 // Add plugin settings link
116 add_filter('plugin_action_links', [$this, 'add_plugin_settings_link'], 10, 2);
117 // Update chart
118 add_action('wp_ajax_wpp_update_chart', [$this, 'update_chart']);
119 // Get lists
120 add_action('wp_ajax_wpp_get_most_viewed', [$this, 'get_popular_items']);
121 add_action('wp_ajax_wpp_get_most_commented', [$this, 'get_popular_items']);
122 add_action('wp_ajax_wpp_get_trending', [$this, 'get_popular_items']);
123 // Delete plugin data
124 add_action('wp_ajax_wpp_clear_data', [$this, 'clear_data']);
125 // Reset plugin's default thumbnail
126 add_action('wp_ajax_wpp_reset_thumbnail', [$this, 'get_default_thumbnail']);
127 // Empty plugin's images cache
128 add_action('wp_ajax_wpp_clear_thumbnail', [$this, 'clear_thumbnails']);
129 // Flush cached thumbnail on featured image change/deletion
130 add_action('updated_post_meta', [$this, 'updated_post_meta'], 10, 4);
131 add_action('deleted_post_meta', [$this, 'deleted_post_meta'], 10, 4);
132 // Purge transients when sending post/page to trash
133 add_action('wp_trash_post', [$this, 'purge_data_cache']);
134 // Purge post data on post/page deletion
135 add_action('admin_init', [$this, 'purge_post_data']);
136 // Purge old data on demand
137 add_action('wpp_cache_event', [$this, 'purge_data']);
138 // Maybe performance nag
139 add_action('wpp_maybe_performance_nag', [$this, 'performance_check']);
140 add_action('wp_ajax_wpp_handle_performance_notice', [$this, 'handle_performance_notice']);
141 // Show notices
142 add_action('admin_notices', [$this, 'notices']);
143 }
144
145 /**
146 * Checks if an upgrade procedure is required.
147 *
148 * @since 2.4.0
149 */
150 public function upgrade_check()
151 {
152 $this->upgrade_site();
153 }
154
155 /**
156 * Checks whether a performance tweak may be necessary.
157 *
158 * @since 5.0.2
159 */
160 public function performance_check()
161 {
162 $performance_nag = get_option('wpp_performance_nag');
163
164 if ( ! $performance_nag ) {
165 $performance_nag = [
166 'status' => 0,
167 'last_checked' => null
168 ];
169 add_option('wpp_performance_nag', $performance_nag);
170 }
171
172 if ( 3 != $performance_nag['status'] ) { // 0 = inactive, 1 = active, 2 = remind me later, 3 = dismissed
173 global $wpdb;
174
175 $views_count = $wpdb->get_var(
176 $wpdb->prepare(
177 "SELECT IFNULL(SUM(pageviews), 0) AS views FROM {$wpdb->prefix}popularpostssummary WHERE view_datetime > DATE_SUB(%s, INTERVAL 1 HOUR);",
178 Helper::now()
179 )
180 );
181
182 // This site is probably a mid/high traffic one,
183 // display performance nag
184 if ( $views_count >= 420 ) {
185 if ( 0 == $performance_nag['status'] ) {
186 $performance_nag['status'] = 1;
187 $performance_nag['last_checked'] = Helper::timestamp();
188 update_option('wpp_performance_nag', $performance_nag);
189 }
190 }
191 }
192 }
193
194 /**
195 * Upgrades single site.
196 *
197 * @since 4.0.7
198 */
199 private function upgrade_site()
200 {
201 // Get WPP version
202 $wpp_ver = get_option('wpp_ver');
203
204 if ( ! $wpp_ver ) {
205 add_option('wpp_ver', WPP_VERSION);
206 } elseif ( version_compare($wpp_ver, WPP_VERSION, '<') ) {
207 $this->upgrade();
208 }
209 }
210
211 /**
212 * On plugin upgrade, performs a number of actions: update WPP database tables structures (if needed),
213 * run the setup wizard (if needed), and some other checks.
214 *
215 * @since 2.4.0
216 * @access private
217 * @global object $wpdb
218 */
219 private function upgrade()
220 {
221 $now = Helper::now();
222
223 // Keep the upgrade process from running too many times
224 $wpp_update = get_option('wpp_update');
225
226 if ( $wpp_update ) {
227 $from_time = strtotime($wpp_update);
228 $to_time = strtotime($now);
229 $difference_in_minutes = round(abs($to_time - $from_time)/60, 2);
230
231 // Upgrade flag is still valid, abort
232 if ( $difference_in_minutes <= 15 ) {
233 return;
234 }
235
236 // Upgrade flag expired, delete it and continue
237 delete_option('wpp_update');
238 }
239
240 global $wpdb;
241
242 // Upgrade flag
243 add_option('wpp_update', $now);
244
245 // Set table name
246 $prefix = $wpdb->prefix . 'popularposts';
247
248 // Update data table structure and indexes
249 $dataFields = $wpdb->get_results("SHOW FIELDS FROM {$prefix}data;"); //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $prefix is safe to use
250
251 foreach ( $dataFields as $column ) {
252 if ( 'day' == $column->Field ) {
253 $wpdb->query("ALTER TABLE {$prefix}data ALTER COLUMN day DROP DEFAULT;");
254 }
255
256 if ( 'last_viewed' == $column->Field ) {
257 $wpdb->query("ALTER TABLE {$prefix}data ALTER COLUMN last_viewed DROP DEFAULT;");
258 }
259 }
260
261 // Update summary table structure and indexes
262 $summaryFields = $wpdb->get_results("SHOW FIELDS FROM {$prefix}summary;");
263
264 foreach ( $summaryFields as $column ) {
265 if ( 'last_viewed' == $column->Field ) {
266 $wpdb->query("ALTER TABLE {$prefix}summary CHANGE last_viewed view_datetime datetime NOT NULL, ADD KEY view_datetime (view_datetime);");
267 }
268
269 if ( 'view_date' == $column->Field ) {
270 $wpdb->query("ALTER TABLE {$prefix}summary ALTER COLUMN view_date DROP DEFAULT;");
271 }
272
273 if ( 'view_datetime' == $column->Field ) {
274 $wpdb->query("ALTER TABLE {$prefix}summary ALTER COLUMN view_datetime DROP DEFAULT;");
275 }
276 }
277
278 $summaryIndexes = $wpdb->get_results("SHOW INDEX FROM {$prefix}summary;");
279
280 foreach( $summaryIndexes as $index ) {
281 if ( 'ID_date' == $index->Key_name ) {
282 $wpdb->query("ALTER TABLE {$prefix}summary DROP INDEX ID_date;");
283 }
284
285 if ( 'last_viewed' == $index->Key_name ) {
286 $wpdb->query("ALTER TABLE {$prefix}summary DROP INDEX last_viewed;");
287 }
288 }
289
290 // Validate the structure of the tables, create missing tables / fields if necessary
291 \WordPressPopularPosts\Activation\Activator::track_new_site();
292
293 // Check storage engine
294 $storage_engine_data = $wpdb->get_var("SELECT `ENGINE` FROM `information_schema`.`TABLES` WHERE `TABLE_SCHEMA`='{$wpdb->dbname}' AND `TABLE_NAME`='{$prefix}data';");
295
296 if ( 'InnoDB' != $storage_engine_data ) {
297 $wpdb->query("ALTER TABLE {$prefix}data ENGINE=InnoDB;");
298 }
299
300 $storage_engine_summary = $wpdb->get_var("SELECT `ENGINE` FROM `information_schema`.`TABLES` WHERE `TABLE_SCHEMA`='{$wpdb->dbname}' AND `TABLE_NAME`='{$prefix}summary';");
301
302 if ( 'InnoDB' != $storage_engine_summary ) {
303 $wpdb->query("ALTER TABLE {$prefix}summary ENGINE=InnoDB;");
304 }
305
306 // Update WPP version
307 update_option('wpp_ver', WPP_VERSION);
308 // Remove upgrade flag
309 delete_option('wpp_update');
310 }
311
312 /**
313 * Fired when a new blog is activated on WP Multisite.
314 *
315 * @since 3.0.0
316 * @param int $blog_id New blog ID
317 */
318 public function activate_new_site(int $blog_id)
319 {
320 if ( 1 !== did_action('wpmu_new_blog') ) {
321 return;
322 }
323
324 // run activation for the new blog
325 switch_to_blog($blog_id);
326 \WordPressPopularPosts\Activation\Activator::track_new_site();
327 // switch back to current blog
328 restore_current_blog();
329 }
330
331 /**
332 * Fired when a blog is deleted on WP Multisite.
333 *
334 * @since 4.0.0
335 * @param array $tables
336 * @param int $blog_id
337 * @return array
338 */
339 public function delete_site_data(array $tables, int $blog_id)
340 {
341 global $wpdb;
342
343 $tables[] = $wpdb->prefix . 'popularpostsdata';
344 $tables[] = $wpdb->prefix . 'popularpostssummary';
345
346 return $tables;
347 }
348
349 /**
350 * Display some statistics at the "At a Glance" box from the Dashboard.
351 *
352 * @since 4.1.0
353 */
354 public function at_a_glance_stats()
355 {
356 global $wpdb;
357
358 $glances = [];
359 $args = ['post', 'page'];
360 $post_type_placeholders = '%s, %s';
361
362 if (
363 isset($this->config['stats']['post_type'])
364 && ! empty($this->config['stats']['post_type'])
365 ) {
366 $args = array_map('trim', explode(',', $this->config['stats']['post_type']));
367 $post_type_placeholders = implode(', ', array_fill(0, count($args), '%s'));
368 }
369
370 $args[] = Helper::now();
371
372 $query = $wpdb->prepare(
373 "SELECT SUM(pageviews) AS total
374 FROM `{$wpdb->prefix}popularpostssummary` v LEFT JOIN `{$wpdb->prefix}posts` p ON v.postid = p.ID
375 WHERE p.post_type IN({$post_type_placeholders}) AND p.post_status = 'publish' AND p.post_password = '' AND v.view_datetime > DATE_SUB(%s, INTERVAL 1 HOUR);",
376 $args
377 );
378
379 $total_views = $wpdb->get_var($query); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query is built and prepared dynamically, see above
380 $total_views = (float) $total_views;
381
382 $pageviews = sprintf(
383 _n('%s view in the last hour', '%s views in the last hour', $total_views, 'wordpress-popular-posts'),
384 number_format_i18n($total_views)
385 );
386
387 if ( current_user_can('edit_published_posts') ) {
388 $glances[] = '<a class="wpp-views-count" href="' . admin_url('options-general.php?page=wordpress-popular-posts') . '">' . $pageviews . '</a>';
389 }
390 else {
391 $glances[] = '<span class="wpp-views-count">' . $pageviews . '</a>';
392 }
393
394 return $glances;
395 }
396
397 /**
398 * Add custom inline CSS styles for At a Glance stats.
399 *
400 * @since 4.1.0
401 */
402 public function at_a_glance_stats_css()
403 {
404 echo '<style>#dashboard_right_now a.wpp-views-count:before, #dashboard_right_now span.wpp-views-count:before {content: "\f177";}</style>';
405 }
406
407 /**
408 * Adds a widget to the dashboard.
409 *
410 * @since 5.0.0
411 */
412 public function add_dashboard_widgets()
413 {
414 if ( current_user_can('edit_published_posts') ) {
415 wp_add_dashboard_widget(
416 'wpp_trending_dashboard_widget',
417 __('Trending now', 'wordpress-popular-posts'),
418 [$this, 'trending_dashboard_widget']
419 );
420 }
421 }
422
423 /**
424 * Outputs the contents of our Trending Dashboard Widget.
425 *
426 * @since 5.0.0
427 */
428 public function trending_dashboard_widget()
429 {
430 ?>
431 <style>
432 #wpp_trending_dashboard_widget .inside {
433 overflow: hidden;
434 position: relative;
435 min-height: 150px;
436 padding-bottom: 22px;
437 }
438
439 #wpp_trending_dashboard_widget .inside::after {
440 position: absolute;
441 top: 0;
442 left: 0;
443 opacity: 0.2;
444 display: block;
445 content: '';
446 width: 100%;
447 height: 100%;
448 z-index: 1;
449 background-image: url('<?php echo esc_url(plugin_dir_url(dirname(dirname(__FILE__)))) . 'assets/images/flame.png'; ?>');
450 background-position: right bottom;
451 background-repeat: no-repeat;
452 background-size: 34% auto;
453 }
454
455 #wpp_trending_dashboard_widget .inside .no-data {
456 position: absolute;
457 top: calc(50% - 11px);
458 left: 50%;
459 z-index: 2;
460 margin: 0;
461 padding: 0;
462 width: 96%;
463 transform: translate(-50.0001%, -50.0001%);
464 }
465
466 #wpp_trending_dashboard_widget .inside .popular-posts-list,
467 #wpp_trending_dashboard_widget .inside p#wpp_read_more {
468 position: relative;
469 z-index: 2;
470 }
471
472 #wpp_trending_dashboard_widget .inside .popular-posts-list {
473 margin: 1em 0;
474 }
475
476 #wpp_trending_dashboard_widget .inside p#wpp_read_more {
477 position: absolute;
478 left: 0;
479 bottom: 0;
480 width: 100%;
481 text-align: center;
482 }
483 </style>
484 <?php
485 $args = [
486 'range' => 'custom',
487 'time_quantity' => 1,
488 'time_unit' => 'HOUR',
489 'post_type' => $this->config['stats']['post_type'],
490 'limit' => 5,
491 'stats_tag' => [
492 'views' => 1,
493 'comment_count' => 1
494 ]
495 ];
496 $options = apply_filters('wpp_trending_dashboard_widget_args', []);
497
498 if ( is_array($options) && ! empty($options) ) {
499 $args = Helper::merge_array_r($args, $options);
500 }
501
502 $query = new Query($args);
503 $posts = $query->get_posts();
504
505 $this->render_list($posts, 'trending');
506 echo '<p id="wpp_read_more"><a href="' . esc_url(admin_url('options-general.php?page=wordpress-popular-posts')) . '">' . esc_html(__('View more', 'wordpress-popular-posts')) . '</a><p>';
507
508 }
509
510 /**
511 * Enqueues admin facing assets.
512 *
513 * @since 5.0.0
514 */
515 public function enqueue_assets()
516 {
517 $screen = get_current_screen();
518
519 if ( isset($screen->id) ) {
520 if ( $screen->id == $this->screen_hook_suffix ) {
521 wp_enqueue_style('wpp-datepicker-theme', plugin_dir_url(dirname(dirname(__FILE__))) . 'assets/css/datepicker.css', [], WPP_VERSION, 'all');
522
523 wp_enqueue_media();
524 wp_enqueue_script('jquery-ui-datepicker');
525 wp_enqueue_script('chartjs', plugin_dir_url(dirname(dirname(__FILE__))) . 'assets/js/vendor/chart.3.8.0.min.js', [], WPP_VERSION);
526
527 wp_register_script('wpp-chart', plugin_dir_url(dirname(dirname(__FILE__))) . 'assets/js/chart.js', ['chartjs'], WPP_VERSION);
528 wp_localize_script('wpp-chart', 'wpp_chart_params', [
529 'colors' => $this->get_admin_color_scheme()
530 ]);
531 wp_enqueue_script('wpp-chart');
532
533 wp_register_script('wordpress-popular-posts-admin-script', plugin_dir_url(dirname(dirname(__FILE__))) . 'assets/js/admin.js', ['jquery'], WPP_VERSION, true);
534 wp_localize_script('wordpress-popular-posts-admin-script', 'wpp_admin_params', [
535 'label_media_upload_button' => __('Use this image', 'wordpress-popular-posts'),
536 'nonce' => wp_create_nonce('wpp_admin_nonce'),
537 'nonce_reset_data' => wp_create_nonce('wpp_nonce_reset_data'),
538 'nonce_reset_thumbnails' => wp_create_nonce('wpp_nonce_reset_thumbnails'),
539 'text_confirm_reset_cache_table' => __("This operation will delete all entries from WordPress Popular Posts' cache table and cannot be undone.", 'wordpress-popular-posts'),
540 'text_cache_table_cleared' => __('Success! The cache table has been cleared!', 'wordpress-popular-posts'),
541 'text_cache_table_missing' => __('Error: cache table does not exist.', 'wordpress-popular-posts'),
542 'text_confirm_reset_all_tables' => __("This operation will delete all stored info from WordPress Popular Posts' data tables and cannot be undone.", 'wordpress-popular-posts'),
543 'text_all_table_cleared' => __('Success! All data have been cleared!', 'wordpress-popular-posts'),
544 'text_tables_missing' => __('Error: one or both data tables are missing.', 'wordpress-popular-posts'),
545 'text_confirm_image_cache_reset' => __('This operation will delete all cached thumbnails and cannot be undone.', 'wordpress-popular-posts'),
546 'text_image_cache_cleared' => __('Success! All files have been deleted!', 'wordpress-popular-posts'),
547 'text_image_cache_already_empty' => __('The thumbnail cache is already empty!', 'wordpress-popular-posts'),
548 'text_continue' => __('Do you want to continue?', 'wordpress-popular-posts'),
549 'text_insufficient_permissions' => __('Sorry, you do not have enough permissions to do this. Please contact the site administrator for support.', 'wordpress-popular-posts'),
550 'text_invalid_action' => __('Invalid action.', 'wordpress-popular-posts')
551 ]);
552 wp_enqueue_script('wordpress-popular-posts-admin-script');
553 }
554
555 if ( $screen->id == $this->screen_hook_suffix || 'dashboard' == $screen->id ) {
556 // Fontello icons
557 wp_enqueue_style('wpp-fontello', plugin_dir_url(dirname(dirname(__FILE__))) . 'assets/css/fontello.css', [], WPP_VERSION, 'all');
558 wp_enqueue_style('wordpress-popular-posts-admin-styles', plugin_dir_url(dirname(dirname(__FILE__))) . 'assets/css/admin.css', [], WPP_VERSION, 'all');
559 }
560 }
561
562 $performance_nag = get_option('wpp_performance_nag');
563
564 if (
565 isset($performance_nag['status'])
566 && 3 != $performance_nag['status'] // 0 = inactive, 1 = active, 2 = remind me later, 3 = dismissed
567 ) {
568 $now = Helper::timestamp();
569
570 // How much time has passed since the notice was last displayed?
571 $last_checked = isset($performance_nag['last_checked']) ? $performance_nag['last_checked'] : 0;
572
573 if ( $last_checked ) {
574 $last_checked = ($now - $last_checked) / (60 * 60);
575 }
576
577 if (
578 1 == $performance_nag['status']
579 || ( 2 == $performance_nag['status'] && $last_checked && $last_checked >= 24 )
580 ) {
581 wp_register_script('wpp-admin-notices', plugin_dir_url(dirname(dirname(__FILE__))) . 'assets/js/admin-notices.js', [], WPP_VERSION);
582 wp_localize_script('wpp-admin-notices', 'wpp_admin_notices_params', [
583 'nonce_performance_nag' => wp_create_nonce('wpp_nonce_performance_nag')
584 ]);
585 wp_enqueue_script('wpp-admin-notices');
586 }
587 }
588 }
589
590 /**
591 * Register the administration menu for this plugin into the WordPress Dashboard menu.
592 *
593 * @since 1.0.0
594 */
595 public function add_plugin_admin_menu()
596 {
597 $this->screen_hook_suffix = add_options_page(
598 'WordPress Popular Posts',
599 'WordPress Popular Posts',
600 'edit_published_posts',
601 'wordpress-popular-posts',
602 [$this, 'display_plugin_admin_page']
603 );
604 }
605
606 /**
607 * Render the settings page for this plugin.
608 *
609 * @since 1.0.0
610 */
611 public function display_plugin_admin_page()
612 {
613 include_once plugin_dir_path(__FILE__) . 'admin-page.php';
614 }
615
616 /**
617 * Adds contextual help menu.
618 *
619 * @since 4.0.0
620 */
621 public function add_contextual_help()
622 {
623 $screen = get_current_screen();
624
625 if ( isset($screen->id) && $screen->id == $this->screen_hook_suffix ){
626 $screen->add_help_tab(
627 [
628 'id' => 'wpp_help_overview',
629 'title' => __('Overview', 'wordpress-popular-posts'),
630 'content' => '<p>' . __("Welcome to WordPress Popular Posts' Dashboard! In this screen you will find statistics on what's popular on your site, tools to further tweak WPP to your needs, and more!", 'wordpress-popular-posts') . '</p>'
631 ]
632 );
633 $screen->add_help_tab(
634 [
635 'id' => 'wpp_help_donate',
636 'title' => __('Like this plugin?', 'wordpress-popular-posts'),
637 'content' => '
638 <p style="text-align: center;">' . __('Each donation motivates me to keep releasing free stuff for the WordPress community!', 'wordpress-popular-posts') . '</p>
639 <form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top" style="margin: 0; padding: 0; text-align: center;">
640 <input type="hidden" name="cmd" value="_s-xclick">
641 <input type="hidden" name="hosted_button_id" value="RP9SK8KVQHRKS">
642 <input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!" style="display: inline; margin: 0;">
643 <img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
644 </form>
645 <p style="text-align: center;">' . sprintf(__('You can <a href="%s" target="_blank">leave a review</a>, too!', 'wordpress-popular-posts'), 'https://wordpress.org/support/view/plugin-reviews/wordpress-popular-posts?rate=5#postform') . '</p>'
646 ]
647 );
648
649 // Help sidebar
650 $screen->set_help_sidebar(
651 sprintf(
652 __('<p><strong>For more information:</strong></p><ul><li><a href="%1$s">Documentation</a></li><li><a href="%2$s">Support</a></li></ul>', 'wordpress-popular-posts'),
653 'https://github.com/cabrerahector/wordpress-popular-posts/',
654 'https://wordpress.org/support/plugin/wordpress-popular-posts/'
655 )
656 );
657 }
658 }
659
660 /**
661 * Registers Settings link on plugin description.
662 *
663 * @since 2.3.3
664 * @param array $links
665 * @param string $file
666 * @return array
667 */
668 public function add_plugin_settings_link(array $links, string $file)
669 {
670 $plugin_file = 'wordpress-popular-posts/wordpress-popular-posts.php';
671
672 if (
673 is_plugin_active($plugin_file)
674 && $plugin_file == $file
675 ) {
676 array_unshift(
677 $links,
678 '<a href="' . admin_url('options-general.php?page=wordpress-popular-posts') . '">' . __('Settings') . '</a>', // phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- We're using WordPress' translation here
679 '<a href="https://wordpress.org/support/plugin/wordpress-popular-posts/">' . __('Support', 'wordpress-popular-posts') . '</a>'
680 );
681 }
682
683 return $links;
684 }
685
686 /**
687 * Gets current admin color scheme.
688 *
689 * @since 4.0.0
690 * @return array
691 */
692 private function get_admin_color_scheme()
693 {
694 global $_wp_admin_css_colors;
695
696 if (
697 is_array($_wp_admin_css_colors)
698 && count($_wp_admin_css_colors)
699 ) {
700 $current_user = wp_get_current_user();
701 $color_scheme = get_user_option('admin_color', $current_user->ID);
702
703 if (
704 empty($color_scheme)
705 || ! isset($_wp_admin_css_colors[ $color_scheme])
706 ) {
707 $color_scheme = 'fresh';
708 }
709
710 if ( isset($_wp_admin_css_colors[$color_scheme]) && isset($_wp_admin_css_colors[$color_scheme]->colors) ) {
711 return $_wp_admin_css_colors[$color_scheme]->colors;
712 }
713
714 }
715
716 // Fallback, just in case
717 return ['#333', '#999', '#881111', '#a80000'];
718 }
719
720 /**
721 * Fetches chart data.
722 *
723 * @since 4.0.0
724 * @return string
725 */
726 public function get_chart_data(string $range = 'last7days', string $time_unit = 'HOUR', int $time_quantity = 24)
727 {
728 $dates = $this->get_dates($range, $time_unit, $time_quantity);
729 $start_date = $dates[0];
730 $end_date = $dates[count($dates) - 1];
731 $date_range = Helper::get_date_range($start_date, $end_date, 'Y-m-d H:i:s');
732 $views_data = $this->get_range_item_count($start_date, $end_date, 'views');
733 $views = [];
734 $comments_data = $this->get_range_item_count($start_date, $end_date, 'comments');
735 $comments = [];
736
737 if ( 'today' != $range ) {
738 foreach($date_range as $date) {
739 $key = date('Y-m-d', strtotime($date));
740 $views[] = ( ! isset($views_data[$key]) ) ? 0 : $views_data[$key]->pageviews;
741 $comments[] = ( ! isset($comments_data[$key]) ) ? 0 : $comments_data[$key]->comments;
742 }
743 } else {
744 $key = date('Y-m-d', strtotime($dates[0]));
745 $views[] = ( ! isset($views_data[$key]) ) ? 0 : $views_data[$key]->pageviews;
746 $comments[] = ( ! isset($comments_data[$key]) ) ? 0 : $comments_data[$key]->comments;
747 }
748
749 if ( $start_date != $end_date ) {
750 $label_date_range = date_i18n('M, D d', strtotime($start_date)) . ' &mdash; ' . date_i18n('M, D d', strtotime($end_date));
751 } else {
752 $label_date_range = date_i18n('M, D d', strtotime($start_date));
753 }
754
755 $total_views = array_sum($views);
756 $total_comments = array_sum($comments);
757
758 $label_summary = sprintf(_n('%s view', '%s views', $total_views, 'wordpress-popular-posts'), '<strong>' . number_format_i18n($total_views) . '</strong>') . ' / ' . sprintf(_n('%s comment', '%s comments', $total_comments, 'wordpress-popular-posts'), '<strong>' . number_format_i18n($total_comments) . '</strong>');
759
760 // Format labels
761 if ( 'today' != $range ) {
762 $date_range = array_map(function($d) {
763 return date_i18n('D d', strtotime($d));
764 }, $date_range);
765 } else {
766 $date_range = [date_i18n('D d', strtotime($date_range[0]))];
767 $comments = [array_sum($comments)];
768 $views = [array_sum($views)];
769 }
770
771 $response = [
772 'totals' => [
773 'label_summary' => $label_summary,
774 'label_date_range' => $label_date_range,
775 ],
776 'labels' => $date_range,
777 'datasets' => [
778 [
779 'label' => __('Comments', 'wordpress-popular-posts'),
780 'data' => $comments
781 ],
782 [
783 'label' => __('Views', 'wordpress-popular-posts'),
784 'data' => $views
785 ]
786 ]
787 ];
788
789 return json_encode($response);
790 }
791
792 /**
793 * Returns an array of dates.
794 *
795 * @since 5.0.0
796 * @return array|bool
797 */
798 private function get_dates(string $range = 'last7days', string $time_unit = 'HOUR', int $time_quantity = 24)
799 {
800 $valid_ranges = ['today', 'daily', 'last24hours', 'weekly', 'last7days', 'monthly', 'last30days', 'all', 'custom'];
801 $range = in_array($range, $valid_ranges) ? $range : 'last7days';
802 $now = new \DateTime(Helper::now(), wp_timezone());
803
804 // Determine time range
805 switch( $range ){
806 case 'last24hours':
807 case 'daily':
808 $end_date = $now->format('Y-m-d H:i:s');
809 $start_date = $now->modify('-1 day')->format('Y-m-d H:i:s');
810 break;
811
812 case 'today':
813 $start_date = $now->format('Y-m-d') . ' 00:00:00';
814 $end_date = $now->format('Y-m-d') . ' 23:59:59';
815 break;
816
817 case 'last7days':
818 case 'weekly':
819 $end_date = $now->format('Y-m-d') . ' 23:59:59';
820 $start_date = $now->modify('-6 day')->format('Y-m-d') . ' 00:00:00';
821 break;
822
823 case 'last30days':
824 case 'monthly':
825 $end_date = $now->format('Y-m-d') . ' 23:59:59';
826 $start_date = $now->modify('-29 day')->format('Y-m-d') . ' 00:00:00';
827 break;
828
829 case 'custom':
830 $end_date = $now->format('Y-m-d H:i:s');
831
832 if (
833 Helper::is_number($time_quantity)
834 && $time_quantity >= 1
835 ) {
836 $end_date = $now->format('Y-m-d H:i:s');
837 $time_unit = strtoupper($time_unit);
838
839 if ( 'MINUTE' == $time_unit ) {
840 $start_date = $now->sub(new \DateInterval('PT' . (60 * $time_quantity) . 'S'))->format('Y-m-d H:i:s');
841 } elseif ( 'HOUR' == $time_unit ) {
842 $start_date = $now->sub(new \DateInterval('PT' . ((60 * $time_quantity) - 1) . 'M59S'))->format('Y-m-d H:i:s');
843 } else {
844 $end_date = $now->format('Y-m-d') . ' 23:59:59';
845 $start_date = $now->sub(new \DateInterval('P' . ($time_quantity - 1) . 'D'))->format('Y-m-d') . ' 00:00:00';
846 }
847 } // fallback to last 24 hours
848 else {
849 $start_date = $now->modify('-1 day')->format('Y-m-d H:i:s');
850 }
851
852 // Check if custom date range has been requested
853 $dates = null;
854
855 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- 'dates' are date strings, and we're validating those below
856 if ( isset($_GET['dates']) ) {
857 $dates = explode(' ~ ', esc_html($_GET['dates']));
858
859 if (
860 ! is_array($dates)
861 || empty($dates)
862 || ! Helper::is_valid_date($dates[0])
863 ) {
864 $dates = null;
865 } else {
866 if (
867 ! isset($dates[1])
868 || ! Helper::is_valid_date($dates[1])
869 ) {
870 $dates[1] = $dates[0];
871 }
872
873 $start_date = $dates[0] . ' 00:00:00';
874 $end_date = $dates[1] . ' 23:59:59';
875 }
876 }
877 // phpcs:enable
878
879 break;
880
881 default:
882 $end_date = $now->format('Y-m-d') . ' 23:59:59';
883 $start_date = $now->modify('-6 day')->format('Y-m-d') . ' 00:00:00';
884 break;
885 }
886
887 return [$start_date, $end_date];
888 }
889
890 /**
891 * Returns an array of dates with views/comments count.
892 *
893 * @since 5.0.0
894 * @param string $start_date
895 * @param string $end_date
896 * @param string $item
897 * @return array
898 */
899 public function get_range_item_count(string $start_date, string $end_date, string $item = 'views')
900 {
901 global $wpdb;
902
903 $args = array_map('trim', explode(',', $this->config['stats']['post_type']));
904
905 $types = get_post_types([
906 'public' => true
907 ], 'names' );
908 $types = array_values($types);
909
910 // Let's make sure we're getting valid post types
911 $args = array_intersect($types, $args);
912
913 if ( empty($args) ) {
914 $args = ['post', 'page'];
915 }
916
917 $post_type_placeholders = array_fill(0, count($args), '%s');
918
919 if ( $this->config['stats']['freshness'] ) {
920 $args[] = $start_date;
921 }
922
923 // Append dates to arguments list
924 array_unshift($args, $start_date, $end_date);
925
926 if ( $item == 'comments' ) {
927 //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $post_type_placeholders is already prepared above
928 $query = $wpdb->prepare(
929 "SELECT DATE(`c`.`comment_date_gmt`) AS `c_date`, COUNT(*) AS `comments`
930 FROM `{$wpdb->comments}` c INNER JOIN `{$wpdb->posts}` p ON `c`.`comment_post_ID` = `p`.`ID`
931 WHERE (`c`.`comment_date_gmt` BETWEEN %s AND %s) AND `c`.`comment_approved` = '1' AND `p`.`post_type` IN (" . implode(', ', $post_type_placeholders) . ") AND `p`.`post_status` = 'publish' AND `p`.`post_password` = ''
932 " . ( $this->config['stats']['freshness'] ? ' AND `p`.`post_date` >= %s' : '' ) . '
933 GROUP BY `c_date` ORDER BY `c_date` DESC;',
934 $args
935 );
936 //phpcs:enable
937 } else {
938 //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $post_type_placeholders is already prepared above
939 $query = $wpdb->prepare(
940 "SELECT `v`.`view_date`, SUM(`v`.`pageviews`) AS `pageviews`
941 FROM `{$wpdb->prefix}popularpostssummary` v INNER JOIN `{$wpdb->posts}` p ON `v`.`postid` = `p`.`ID`
942 WHERE (`v`.`view_datetime` BETWEEN %s AND %s) AND `p`.`post_type` IN (" . implode(', ', $post_type_placeholders) . ") AND `p`.`post_status` = 'publish' AND `p`.`post_password` = ''
943 " . ( $this->config['stats']['freshness'] ? ' AND `p`.`post_date` >= %s' : '' ) . '
944 GROUP BY `v`.`view_date` ORDER BY `v`.`view_date` DESC;',
945 $args
946 );
947 //phpcs:enable
948
949 //error_log($query);
950 }
951
952 return $wpdb->get_results($query, OBJECT_K); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- at this point $query has been prepared already
953 }
954
955 /**
956 * Updates chart via AJAX.
957 *
958 * @since 4.0.0
959 */
960 public function update_chart()
961 {
962 $response = [
963 'status' => 'error'
964 ];
965 $nonce = isset($_GET['nonce']) ? $_GET['nonce'] : null; //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a nonce
966
967 if ( wp_verify_nonce($nonce, 'wpp_admin_nonce') ) {
968
969 $valid_ranges = ['today', 'daily', 'last24hours', 'weekly', 'last7days', 'monthly', 'last30days', 'all', 'custom'];
970 $time_units = ['MINUTE', 'HOUR', 'DAY'];
971
972 $range = ( isset($_GET['range']) && in_array($_GET['range'], $valid_ranges) ) ? $_GET['range'] : 'last7days';
973 $time_quantity = ( isset($_GET['time_quantity']) && filter_var($_GET['time_quantity'], FILTER_VALIDATE_INT) ) ? $_GET['time_quantity'] : 24;
974 $time_unit = ( isset($_GET['time_unit']) && in_array(strtoupper($_GET['time_unit']), $time_units) ) ? $_GET['time_unit'] : 'hour';
975
976 $this->config['stats']['range'] = $range;
977 $this->config['stats']['time_quantity'] = $time_quantity;
978 $this->config['stats']['time_unit'] = $time_unit;
979
980 update_option('wpp_settings_config', $this->config);
981
982 $response = [
983 'status' => 'ok',
984 'data' => json_decode(
985 $this->get_chart_data($this->config['stats']['range'], $this->config['stats']['time_unit'], $this->config['stats']['time_quantity']),
986 true
987 )
988 ];
989 }
990
991 wp_send_json($response);
992 }
993
994 /**
995 * Fetches most viewed/commented/trending posts via AJAX.
996 *
997 * @since 5.0.0
998 */
999 public function get_popular_items()
1000 {
1001 $items = isset($_GET['items']) ? $_GET['items'] : null; //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification happens below
1002 $nonce = isset($_GET['nonce']) ? $_GET['nonce'] : null; //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a nonce
1003
1004 if ( wp_verify_nonce($nonce, 'wpp_admin_nonce') ) {
1005 $args = [
1006 'range' => $this->config['stats']['range'],
1007 'time_quantity' => $this->config['stats']['time_quantity'],
1008 'time_unit' => $this->config['stats']['time_unit'],
1009 'post_type' => $this->config['stats']['post_type'],
1010 'freshness' => $this->config['stats']['freshness'],
1011 'limit' => $this->config['stats']['limit'],
1012 'stats_tag' => [
1013 'date' => [
1014 'active' => 1
1015 ]
1016 ]
1017 ];
1018
1019 if ( 'most-commented' == $items ) {
1020 $args['order_by'] = 'comments';
1021 $args['stats_tag']['comment_count'] = 1;
1022 $args['stats_tag']['views'] = 0;
1023 } elseif ( 'trending' == $items ) {
1024 $args['range'] = 'custom';
1025 $args['time_quantity'] = 1;
1026 $args['time_unit'] = 'HOUR';
1027 $args['stats_tag']['comment_count'] = 1;
1028 $args['stats_tag']['views'] = 1;
1029 } else {
1030 $args['stats_tag']['comment_count'] = 0;
1031 $args['stats_tag']['views'] = 1;
1032 }
1033
1034 if ( 'trending' != $items ) {
1035
1036 add_filter('wpp_query_join', function($join, $options) use ($items) {
1037 global $wpdb;
1038 $dates = null;
1039
1040 if ( isset($_GET['dates']) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is checked above, 'dates' is verified below
1041 $dates = explode(' ~ ', esc_html($_GET['dates'])); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
1042
1043 if (
1044 ! is_array($dates)
1045 || empty($dates)
1046 || ! Helper::is_valid_date($dates[0])
1047 ) {
1048 $dates = null;
1049 } else {
1050 if (
1051 ! isset($dates[1])
1052 || ! Helper::is_valid_date($dates[1])
1053 ) {
1054 $dates[1] = $dates[0];
1055 }
1056
1057 $start_date = $dates[0];
1058 $end_date = $dates[1];
1059 }
1060
1061 }
1062
1063 if ( $dates ) {
1064 if ( 'most-commented' == $items ) {
1065 return "INNER JOIN (SELECT comment_post_ID, COUNT(comment_post_ID) AS comment_count, comment_date_gmt FROM `{$wpdb->comments}` WHERE comment_date_gmt BETWEEN '{$dates[0]} 00:00:00' AND '{$dates[1]} 23:59:59' AND comment_approved = '1' GROUP BY comment_post_ID) c ON p.ID = c.comment_post_ID";
1066 }
1067
1068 return "INNER JOIN (SELECT SUM(pageviews) AS pageviews, view_date, postid FROM `{$wpdb->prefix}popularpostssummary` WHERE view_datetime BETWEEN '{$dates[0]} 00:00:00' AND '{$dates[1]} 23:59:59' GROUP BY postid) v ON p.ID = v.postid";
1069 }
1070
1071 $now = Helper::now();
1072
1073 // Determine time range
1074 switch( $options['range'] ){
1075 case 'last24hours':
1076 case 'daily':
1077 $interval = '24 HOUR';
1078 break;
1079
1080 case 'today':
1081 $hours = date('H', strtotime($now));
1082 $minutes = $hours * 60 + (int) date( 'i', strtotime($now) );
1083 $interval = "{$minutes} MINUTE";
1084 break;
1085
1086 case 'last7days':
1087 case 'weekly':
1088 $interval = '6 DAY';
1089 break;
1090
1091 case 'last30days':
1092 case 'monthly':
1093 $interval = '29 DAY';
1094 break;
1095
1096 case 'custom':
1097 $time_units = ['MINUTE', 'HOUR', 'DAY'];
1098 $interval = '24 HOUR';
1099
1100 // Valid time unit
1101 if (
1102 isset($options['time_unit'])
1103 && in_array(strtoupper($options['time_unit']), $time_units)
1104 && isset($options['time_quantity'])
1105 && filter_var($options['time_quantity'], FILTER_VALIDATE_INT)
1106 && $options['time_quantity'] > 0
1107 ) {
1108 $interval = "{$options['time_quantity']} " . strtoupper($options['time_unit']);
1109 }
1110
1111 break;
1112
1113 default:
1114 $interval = '1 DAY';
1115 break;
1116 }
1117
1118 if ( 'most-commented' == $items ) {
1119 return "INNER JOIN (SELECT comment_post_ID, COUNT(comment_post_ID) AS comment_count, comment_date_gmt FROM `{$wpdb->comments}` WHERE comment_date_gmt > DATE_SUB('{$now}', INTERVAL {$interval}) AND comment_approved = '1' GROUP BY comment_post_ID) c ON p.ID = c.comment_post_ID";
1120 }
1121
1122 return "INNER JOIN (SELECT SUM(pageviews) AS pageviews, view_date, postid FROM `{$wpdb->prefix}popularpostssummary` WHERE view_datetime > DATE_SUB('{$now}', INTERVAL {$interval}) GROUP BY postid) v ON p.ID = v.postid";
1123 }, 1, 2);
1124
1125 }
1126
1127 $query = new Query($args);
1128 $posts = $query->get_posts();
1129
1130 if ( 'trending' != $items ) {
1131 remove_all_filters('wpp_query_join', 1);
1132 }
1133
1134 $this->render_list($posts, $items);
1135 }
1136
1137 wp_die();
1138 }
1139
1140 /**
1141 * Renders popular posts lists.
1142 *
1143 * @since 5.0.0
1144 * @param array
1145 */
1146 public function render_list(array $posts, $list = 'most-viewed')
1147 {
1148 if ( ! empty($posts) ) {
1149 ?>
1150 <ol class="popular-posts-list">
1151 <?php
1152 foreach( $posts as $post ) {
1153 $pageviews = isset($post->pageviews) ? (int) $post->pageviews : 0;
1154 $comments_count = isset($post->comment_count) ? (int) $post->comment_count : 0;
1155 ?>
1156 <li>
1157 <a href="<?php echo esc_url(get_permalink($post->id)); ?>" class="wpp-title"><?php echo esc_html(sanitize_text_field(apply_filters('the_title', $post->title, $post->id))); ?></a>
1158 <div>
1159 <?php if ( 'most-viewed' == $list ) : ?>
1160 <span><?php printf(esc_html(_n('%s view', '%s views', $pageviews, 'wordpress-popular-posts')), esc_html(number_format_i18n($pageviews))); ?></span>
1161 <?php elseif ( 'most-commented' == $list ) : ?>
1162 <span><?php printf(esc_html(_n('%s comment', '%s comments', $comments_count, 'wordpress-popular-posts')), esc_html(number_format_i18n($comments_count))); ?></span>
1163 <?php else : ?>
1164 <span><?php printf(esc_html(_n('%s view', '%s views', $pageviews, 'wordpress-popular-posts')), esc_html(number_format_i18n($pageviews))); ?></span>, <span><?php printf(esc_html(_n('%s comment', '%s comments', $comments_count, 'wordpress-popular-posts')), esc_html(number_format_i18n($comments_count))); ?></span>
1165 <?php endif; ?>
1166 <small> &mdash; <a href="<?php echo esc_url(get_permalink($post->id)); ?>"><?php esc_html_e('View'); ?></a><?php if ( current_user_can('edit_others_posts') ): ?> | <a href="<?php echo esc_url(get_edit_post_link($post->id)); ?>"><?php esc_html_e('Edit'); ?></a><?php endif; ?></small>
1167 </div>
1168 </li>
1169 <?php
1170 }
1171 ?>
1172 </ol>
1173 <?php
1174 }
1175 else {
1176 ?>
1177 <p class="no-data" style="text-align: center;"><?php _e("Looks like your site's activity is a little low right now. <br />Spread the word and come back later!", 'wordpress-popular-posts'); //phpcs:ignore WordPress.Security.EscapeOutput.UnsafePrintingFunction ?></p>
1178 <?php
1179 }
1180 }
1181
1182 /**
1183 * Truncates data and cache on demand.
1184 *
1185 * @since 2.0.0
1186 * @global object $wpdb
1187 */
1188 public function clear_data()
1189 {
1190 $token = isset($_POST['token']) ? $_POST['token'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This is a nonce
1191 $clear = isset($_POST['clear']) ? $_POST['clear'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1192
1193 if (
1194 current_user_can('manage_options')
1195 && wp_verify_nonce($token, 'wpp_nonce_reset_data')
1196 && $clear
1197 ) {
1198 global $wpdb;
1199
1200 // set table name
1201 $prefix = $wpdb->prefix . 'popularposts';
1202
1203 //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $prefix is safe to use
1204 if ( $clear == 'cache' ) {
1205 if ( $wpdb->get_var("SHOW TABLES LIKE '{$prefix}summary'") ) {
1206 $wpdb->query("TRUNCATE TABLE {$prefix}summary;");
1207 $this->flush_transients();
1208
1209 echo 1;
1210 } else {
1211 echo 2;
1212 }
1213 } elseif ( $clear == 'all' ) {
1214 if ( $wpdb->get_var("SHOW TABLES LIKE '{$prefix}data'") && $wpdb->get_var("SHOW TABLES LIKE '{$prefix}summary'") ) {
1215 $wpdb->query("TRUNCATE TABLE {$prefix}data;");
1216 $wpdb->query("TRUNCATE TABLE {$prefix}summary;");
1217 $this->flush_transients();
1218
1219 echo 1;
1220 } else {
1221 echo 2;
1222 }
1223 } else {
1224 echo 3;
1225 }
1226 //phpcs:enable
1227 } else {
1228 echo 4;
1229 }
1230
1231 wp_die();
1232 }
1233
1234 /**
1235 * Deletes cached (transient) data.
1236 *
1237 * @since 3.0.0
1238 * @access private
1239 */
1240 private function flush_transients()
1241 {
1242 global $wpdb;
1243
1244 $wpp_transients = $wpdb->get_results("SELECT tkey FROM {$wpdb->prefix}popularpoststransients;");
1245
1246 if ( $wpp_transients && is_array($wpp_transients) && ! empty($wpp_transients) ) {
1247 foreach( $wpp_transients as $wpp_transient ) {
1248 try {
1249 delete_transient($wpp_transient->tkey);
1250 } catch (\Throwable $e) {
1251 if ( defined('WP_DEBUG') && WP_DEBUG ) {
1252 error_log( "Error: " . $e->getMessage() );
1253 }
1254 continue;
1255 }
1256 }
1257
1258 $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}popularpoststransients;");
1259 }
1260 }
1261
1262 /**
1263 * Returns WPP's default thumbnail.
1264 *
1265 * @since 6.3.4
1266 */
1267 public function get_default_thumbnail()
1268 {
1269 echo esc_url(plugins_url('assets/images/no_thumb.jpg', dirname(__FILE__, 2)));
1270 wp_die();
1271 }
1272
1273 /**
1274 * Truncates thumbnails cache on demand.
1275 *
1276 * @since 2.0.0
1277 * @global object $wpdb
1278 */
1279 public function clear_thumbnails()
1280 {
1281 $wpp_uploads_dir = $this->thumbnail->get_plugin_uploads_dir();
1282
1283 if ( is_array($wpp_uploads_dir) && ! empty($wpp_uploads_dir) ) {
1284 $token = isset($_POST['token']) ? $_POST['token'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This is a nonce
1285
1286 if (
1287 current_user_can('edit_published_posts')
1288 && wp_verify_nonce($token, 'wpp_nonce_reset_thumbnails')
1289 ) {
1290 if ( is_dir($wpp_uploads_dir['basedir']) ) {
1291 $files = glob("{$wpp_uploads_dir['basedir']}/*"); // get all related images
1292
1293 if ( is_array($files) && ! empty($files) ) {
1294 foreach( $files as $file ){ // iterate files
1295 if ( is_file($file) ) {
1296 @unlink($file); // delete file
1297 }
1298 }
1299 echo 1;
1300 } else {
1301 echo 2;
1302 }
1303 } else {
1304 echo 3;
1305 }
1306 } else {
1307 echo 4;
1308 }
1309 }
1310
1311 wp_die();
1312 }
1313
1314 /**
1315 * Fires immediately after deleting metadata of a post.
1316 *
1317 * @since 5.0.0
1318 *
1319 * @param int $meta_id Metadata ID.
1320 * @param int $post_id Post ID.
1321 * @param string $meta_key Meta key.
1322 * @param mixed $meta_value Meta value.
1323 */
1324 public function updated_post_meta(int $meta_id, int $post_id, string $meta_key, $meta_value) /** @TODO: starting PHP 8.0 $meta_valued can be declared as mixed $meta_value, see https://www.php.net/manual/en/language.types.declarations.php */
1325 {
1326 if ( '_thumbnail_id' == $meta_key ) {
1327 $this->flush_post_thumbnail($post_id);
1328 }
1329 }
1330
1331 /**
1332 * Fires immediately after deleting metadata of a post.
1333 *
1334 * @since 5.0.0
1335 *
1336 * @param array $meta_ids An array of deleted metadata entry IDs.
1337 * @param int $post_id Post ID.
1338 * @param string $meta_key Meta key.
1339 * @param mixed $meta_value Meta value.
1340 */
1341 public function deleted_post_meta(array $meta_ids, int $post_id, string $meta_key, $meta_value) /** @TODO: starting PHP 8.0 $meta_valued can be declared as mixed $meta_value */
1342 {
1343 if ( '_thumbnail_id' == $meta_key ) {
1344 $this->flush_post_thumbnail($post_id);
1345 }
1346 }
1347
1348 /**
1349 * Flushes post's cached thumbnail(s).
1350 *
1351 * @since 3.3.4
1352 *
1353 * @param integer $post_id Post ID
1354 */
1355 public function flush_post_thumbnail(int $post_id)
1356 {
1357 $wpp_uploads_dir = $this->thumbnail->get_plugin_uploads_dir();
1358
1359 if ( is_array($wpp_uploads_dir) && ! empty($wpp_uploads_dir) ) {
1360 $files = glob("{$wpp_uploads_dir['basedir']}/{$post_id}-*.*"); // get all related images
1361
1362 if ( is_array($files) && ! empty($files) ) {
1363 foreach( $files as $file ){ // iterate files
1364 if ( is_file($file) ) {
1365 @unlink($file); // delete file
1366 }
1367 }
1368 }
1369 }
1370 }
1371
1372 /**
1373 * Purges data cache when a post/page is trashed.
1374 *
1375 * @since 5.5.0
1376 */
1377 public function purge_data_cache()
1378 {
1379 $this->flush_transients();
1380 }
1381
1382 /**
1383 * Purges post from data/summary tables.
1384 *
1385 * @since 3.3.0
1386 */
1387 public function purge_post_data()
1388 {
1389 if ( current_user_can('delete_posts') ) {
1390 add_action('delete_post', [$this, 'purge_post']);
1391 }
1392 }
1393
1394 /**
1395 * Purges post from data/summary tables.
1396 *
1397 * @since 3.3.0
1398 * @param int $post_ID
1399 * @global object $wpdb
1400 */
1401 public function purge_post(int $post_ID)
1402 {
1403 global $wpdb;
1404
1405 if ( $wpdb->get_var($wpdb->prepare("SELECT postid FROM {$wpdb->prefix}popularpostsdata WHERE postid = %d", $post_ID)) ) {
1406 // Delete from data table
1407 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}popularpostsdata WHERE postid = %d;", $post_ID));
1408 // Delete from summary table
1409 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}popularpostssummary WHERE postid = %d;", $post_ID));
1410 }
1411
1412 // Delete cached thumbnail(s) as well
1413 $this->flush_post_thumbnail($post_ID);
1414 }
1415
1416 /**
1417 * Purges old post data from summary table.
1418 *
1419 * @since 2.0.0
1420 * @global object $wpdb
1421 */
1422 public function purge_data()
1423 {
1424 global $wpdb;
1425 $wpdb->query(
1426 $wpdb->prepare(
1427 "DELETE FROM {$wpdb->prefix}popularpostssummary WHERE view_date < DATE_SUB(%s, INTERVAL %d DAY);",
1428 Helper::curdate(),
1429 $this->config['tools']['log']['expires_after']
1430 )
1431 );
1432 }
1433
1434 /**
1435 * Displays admin notices.
1436 *
1437 * @since 5.0.2
1438 */
1439 public function notices()
1440 {
1441 /** Performance nag */
1442 $performance_nag = get_option('wpp_performance_nag');
1443
1444 if (
1445 isset($performance_nag['status'])
1446 && 3 != $performance_nag['status'] // 0 = inactive, 1 = active, 2 = remind me later, 3 = dismissed
1447 ) {
1448 $now = Helper::timestamp();
1449
1450 // How much time has passed since the notice was last displayed?
1451 $last_checked = isset($performance_nag['last_checked']) ? $performance_nag['last_checked'] : 0;
1452
1453 if ( $last_checked ) {
1454 $last_checked = ($now - $last_checked) / (60 * 60);
1455 }
1456
1457 if (
1458 1 == $performance_nag['status']
1459 || ( 2 == $performance_nag['status'] && $last_checked && $last_checked >= 24 )
1460 ) {
1461 ?>
1462 <div class="notice notice-warning">
1463 <p>
1464 <?php
1465 printf(
1466 __("<strong>WordPress Popular Posts:</strong> It seems your site is popular (great!) You may want to check <a href=\"%s\">these recommendations</a> to make sure your website's performance stays up to par.", 'wordpress-popular-posts'), //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1467 'https://github.com/cabrerahector/wordpress-popular-posts/wiki/7.-Performance'
1468 );
1469 ?>
1470 </p>
1471 <?php if ( current_user_can('manage_options') ) : ?>
1472 <p><a class="button button-primary wpp-dismiss-performance-notice" href="<?php echo esc_url(add_query_arg('wpp_dismiss_performance_notice', '1')); ?>"><?php esc_html_e('Dismiss', 'wordpress-popular-posts'); ?></a> <a class="button wpp-remind-performance-notice" href="<?php echo esc_url(add_query_arg('wpp_remind_performance_notice', '1')); ?>"><?php esc_html_e('Remind me later', 'wordpress-popular-posts'); ?></a> <span class="spinner" style="float: none;"></span></p>
1473 <?php endif; ?>
1474 </div>
1475 <?php
1476 }
1477 }
1478 }
1479
1480 /**
1481 * Handles performance notice click event.
1482 *
1483 * @since
1484 */
1485 public function handle_performance_notice()
1486 {
1487 $response = [
1488 'status' => 'error'
1489 ];
1490 $token = isset($_POST['token']) ? $_POST['token'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This is a nonce
1491 $dismiss = isset($_POST['dismiss']) ? (int) $_POST['dismiss'] : 0;
1492
1493 if (
1494 current_user_can('manage_options')
1495 && wp_verify_nonce($token, 'wpp_nonce_performance_nag')
1496 ) {
1497 $now = Helper::timestamp();
1498
1499 // User dismissed the notice
1500 if ( 1 == $dismiss ) {
1501 $performance_nag['status'] = 3;
1502 } // User asked us to remind them later
1503 else {
1504 $performance_nag['status'] = 2;
1505 }
1506
1507 $performance_nag['last_checked'] = $now;
1508
1509 update_option('wpp_performance_nag', $performance_nag);
1510
1511 $response = [
1512 'status' => 'success'
1513 ];
1514 }
1515
1516 wp_send_json($response);
1517 }
1518 }
1519