Plugin Directory

source: wordpress-popular-posts/trunk/src/Admin/Admin.php

Last change on this file was 3377030, checked in by hcabrera, 6 months ago

7.3.4

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