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