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