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