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