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