PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / trunk
Post Views Counter vtrunk
1.7.13 1.7.12 1.7.11 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.2 1.3.2.1 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.7.10 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9
post-views-counter / post-views-counter.php
post-views-counter Last commit date
assets 7 months ago blocks 1 month ago css 2 weeks ago includes 2 weeks ago js 2 weeks ago languages 1 month ago index.php 6 years ago post-views-counter.php 2 weeks ago readme.txt 2 weeks ago
post-views-counter.php
1333 lines
1 <?php
2 /*
3 Plugin Name: Post Views Counter
4 Description: Post Views Counter allows you to collect and display how many times a post, page, or other content has been viewed in a simple, fast and reliable way.
5 Version: 1.7.13
6 Author: dFactory
7 Author URI: https://dfactory.co/
8 Plugin URI: https://postviewscounter.com/
9 License: MIT License
10 License URI: https://opensource.org/licenses/MIT
11 Text Domain: post-views-counter
12 Domain Path: /languages
13
14 Post Views Counter
15 Copyright (C) 2014-2026, Digital Factory - info@digitalfactory.pl
16
17 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
18
19 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
20
21 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 */
23
24 // exit if accessed directly
25 if ( ! defined( 'ABSPATH' ) )
26 exit;
27
28 if ( ! class_exists( 'Post_Views_Counter' ) ) {
29 /**
30 * Post Views Counter final class.
31 *
32 * @class Post_Views_Counter
33 * @version 1.7.13
34 */
35 final class Post_Views_Counter {
36
37 private static $instance;
38 private $notices;
39 public $options;
40 public $defaults = [
41 'general' => [
42 'post_types_count' => [ 'post' ],
43 'taxonomies_count' => false,
44 'users_count' => false,
45 'other_count' => false,
46 'technology_count' => false,
47 'data_storage' => 'cookies',
48 'amp_support' => false,
49 'counter_mode' => 'php',
50 'time_between_counts' => [
51 'number' => 24,
52 'type' => 'hours'
53 ],
54 'count_time' => 'gmt',
55 'reset_counts' => [
56 'number' => 0,
57 'type' => 'days'
58 ],
59 'caching_compatibility' => false,
60 'object_cache' => false,
61 'flush_interval' => [
62 'number' => 5,
63 'type' => 'minutes'
64 ],
65 'exclude' => [
66 'groups' => [],
67 'roles' => []
68 ],
69 'exclude_groups' => [],
70 'exclude_roles' => [],
71 'exclude_ips' => [],
72 'strict_counts' => false,
73 'cron_run' => true,
74 'cron_update' => true,
75 'update_version' => 1,
76 'update_notice' => true,
77 'update_delay_date' => 0
78 ],
79 'display' => [
80 'label' => 'Post Views:',
81 'display_period' => 'total',
82 'taxonomies' => false,
83 'taxonomies_display' => [],
84 'user_display' => false,
85 'post_types_display' => [ 'post' ],
86 'page_types_display' => [ 'singular' ],
87 'restrict_display' => [
88 'groups' => [],
89 'roles' => []
90 ],
91 'restrict_display_groups' => [],
92 'restrict_display_roles' => [],
93 'position' => 'after',
94 'post_views_column' => true,
95 'restrict_edit_views' => false,
96 'dynamic_loading' => false,
97 'use_format' => true,
98 'display_style' => [
99 'icon' => true,
100 'text' => true
101 ],
102 'icon_class' => 'dashicons-chart-bar',
103 'toolbar_statistics' => true,
104 'menu_position' => 'top'
105 ],
106 'other' => [
107 'import_meta_key' => 'views',
108 'deactivation_delete' => false,
109 'license' => ''
110 ],
111 'integrations' => [
112 'integrations' => []
113 ],
114 'emails' => [
115 'enabled' => true,
116 'recipient' => '',
117 'test_recipient' => '',
118 'min_views_threshold' => 25,
119 'include_post_types' => [],
120 'max_top_items' => 5,
121 'include_period_trend' => true,
122 'include_traffic_signals' => true,
123 'send_empty_reports' => false,
124 'include_top_gainers_decliners' => false,
125 'include_author_summary' => false,
126 'include_source_summary' => false,
127 'enabled_frequencies' => [ 'weekly' ],
128 'email_subject_template' => '',
129 'email_body_template' => '',
130 'last_sent_at' => null,
131 'last_period_start' => null,
132 'last_period_end' => null,
133 'last_error' => null,
134 'latest_status' => [
135 'last_attempt_at' => null,
136 'last_success_at' => null,
137 'last_error' => null,
138 'recipient' => '',
139 'source' => '',
140 'is_test' => false,
141 'period_start' => null,
142 'period_end' => null
143 ],
144 'schedule_version' => 1
145 ],
146 'version' => '1.7.13'
147 ];
148
149 // instances
150 public $counter;
151 public $crawler;
152 public $cron;
153 public $dashboard;
154 public $emails_scheduler;
155 public $frontend;
156 public $functions;
157 public $settings;
158 public $settings_api;
159 public $import;
160
161 /**
162 * Disable object cloning.
163 *
164 * @return void
165 */
166 public function __clone() {}
167
168 /**
169 * Disable unserializing of the class.
170 *
171 * @return void
172 */
173 public function __wakeup() {}
174
175 /**
176 * Main plugin instance, insures that only one instance of the class exists in memory at one time.
177 *
178 * @return object
179 */
180 public static function instance() {
181 if ( ! isset( self::$instance ) && ! ( self::$instance instanceof Post_Views_Counter ) ) {
182 self::$instance = new Post_Views_Counter();
183
184 // short init?
185 if ( defined( 'SHORTINIT' ) && SHORTINIT ) {
186 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-counter.php' );
187 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-crawler-detect.php' );
188 include_once( POST_VIEWS_COUNTER_PATH . 'includes/functions.php' );
189
190 self::$instance->counter = new Post_Views_Counter_Counter();
191 self::$instance->crawler = new Post_Views_Counter_Crawler_Detect();
192
193 // we need to initialize crawler here since it is not called in SHORTINIT mode
194 self::$instance->crawler->init();
195 // regular setup
196 } else {
197 self::$instance->includes();
198
199 // create settings API
200 self::$instance->settings_api = new Post_Views_Counter_Settings_API(
201 [
202 'object' => self::$instance,
203 'prefix' => 'pvc',
204 'slug' => 'post-views-counter',
205 'domain' => 'post-views-counter',
206 'plugin' => 'Post Views Counter',
207 'plugin_url' => POST_VIEWS_COUNTER_URL
208 ]
209 );
210
211 // initialize other classes
212 self::$instance->functions = new Post_Views_Counter_Functions();
213
214 new Post_Views_Counter_Update();
215
216 self::$instance->settings = new Post_Views_Counter_Settings();
217 self::$instance->emails_scheduler = new Post_Views_Counter_Emails_Scheduler();
218 self::$instance->import = new Post_Views_Counter_Import();
219
220 new Post_Views_Counter_Admin();
221 new Post_Views_Counter_Query();
222
223 self::$instance->cron = new Post_Views_Counter_Cron();
224 self::$instance->counter = new Post_Views_Counter_Counter();
225
226 new Post_Views_Counter_Columns();
227 new Post_Views_Counter_Columns_Modal();
228 new Post_Views_Counter_Traffic_Signals();
229 new Post_Views_Counter_Toolbar();
230
231 self::$instance->crawler = new Post_Views_Counter_Crawler_Detect();
232 self::$instance->frontend = new Post_Views_Counter_Frontend();
233 self::$instance->dashboard = new Post_Views_Counter_Dashboard();
234
235 new Post_Views_Counter_Widgets();
236 }
237 }
238
239 return self::$instance;
240 }
241
242 /**
243 * Setup plugin constants.
244 *
245 * @return void
246 */
247 private function define_constants() {
248 // fix plugin_basename empty $wp_plugin_paths var
249 if ( ! ( defined( 'SHORTINIT' ) && SHORTINIT ) ) {
250 define( 'POST_VIEWS_COUNTER_URL', plugins_url( '', __FILE__ ) );
251 define( 'POST_VIEWS_COUNTER_BASENAME', plugin_basename( __FILE__ ) );
252 define( 'POST_VIEWS_COUNTER_REL_PATH', dirname( POST_VIEWS_COUNTER_BASENAME ) );
253 }
254
255 define( 'POST_VIEWS_COUNTER_PATH', plugin_dir_path( __FILE__ ) );
256 }
257
258 /**
259 * Include required files.
260 *
261 * @return void
262 */
263 private function includes() {
264 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-functions.php' );
265 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-update.php' );
266 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings-api.php' );
267 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings.php' );
268 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings-general.php' );
269 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings-display.php' );
270 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings-reports.php' );
271 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-integrations.php' );
272 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings-integrations.php' );
273 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-emails-template.php' );
274 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings-emails.php' );
275 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-emails-period.php' );
276 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-emails-query.php' );
277 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-emails.php' );
278 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-emails-mailer.php' );
279 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-emails-scheduler.php' );
280 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings-other.php' );
281 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-import.php' );
282 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-admin.php' );
283 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-columns.php' );
284 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-columns-modal.php' );
285 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-toolbar.php' );
286 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-query.php' );
287 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-cron.php' );
288 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-counter.php' );
289 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-crawler-detect.php' );
290 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-frontend.php' );
291 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-dashboard.php' );
292 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-widgets.php' );
293 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-traffic-signals.php' );
294 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-integration-gutenberg.php' );
295 }
296
297 /**
298 * Initialize email template defaults from the template service.
299 *
300 * @return void
301 */
302 private function initialize_email_template_defaults() {
303 if ( ! class_exists( 'Post_Views_Counter_Emails_Template' ) )
304 include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-emails-template.php' );
305
306 $this->defaults['emails']['email_subject_template'] = Post_Views_Counter_Emails_Template::get_default_subject_template();
307 $this->defaults['emails']['email_body_template'] = Post_Views_Counter_Emails_Template::get_default_body_template();
308 }
309
310 /**
311 * Normalize stored email settings with current defaults.
312 *
313 * @param mixed $stored_emails
314 * @param bool $persist
315 * @return array
316 */
317 private function normalize_email_settings( $stored_emails, $persist = false ) {
318 $defaults = $this->get_default_emails_settings();
319 $stored_emails = is_array( $stored_emails ) ? $stored_emails : [];
320 $normalized = array_merge( $defaults, $stored_emails );
321 $normalized['latest_status'] = array_merge(
322 isset( $defaults['latest_status'] ) && is_array( $defaults['latest_status'] ) ? $defaults['latest_status'] : [],
323 isset( $stored_emails['latest_status'] ) && is_array( $stored_emails['latest_status'] ) ? $stored_emails['latest_status'] : []
324 );
325 $did_update_templates = false;
326
327 foreach ( [ 'email_subject_template', 'email_body_template' ] as $template_key ) {
328 if ( ! isset( $stored_emails[$template_key] ) || $stored_emails[$template_key] === '' ) {
329 $normalized[$template_key] = $defaults[$template_key];
330 $did_update_templates = true;
331 }
332 }
333
334 if ( $persist && $did_update_templates )
335 update_option( 'post_views_counter_settings_emails', $normalized );
336
337 return $normalized;
338 }
339
340 /**
341 * Class constructor.
342 *
343 * @return void
344 */
345 private function __construct() {
346 // define plugin constants
347 $this->define_constants();
348 $this->initialize_email_template_defaults();
349
350 $stored_emails = get_option( 'post_views_counter_settings_emails', false );
351 $emails = $this->normalize_email_settings( $stored_emails, $stored_emails !== false );
352
353 // short init?
354 if ( defined( 'SHORTINIT' ) && SHORTINIT ) {
355 $this->options = [
356 'general' => array_merge( $this->defaults['general'], get_option( 'post_views_counter_settings_general', $this->defaults['general'] ) ),
357 'display' => array_merge( $this->defaults['display'], get_option( 'post_views_counter_settings_display', $this->defaults['display'] ) ),
358 'other' => array_merge( $this->defaults['other'], get_option( 'post_views_counter_settings_other', $this->defaults['other'] ) ),
359 'emails' => $emails
360 ];
361
362 $this->options['general']['time_between_counts'] = $this->normalize_time_between_counts( isset( $this->options['general']['time_between_counts'] ) ? $this->options['general']['time_between_counts'] : null );
363
364 // migrate exclude to new fields
365 if ( isset( $this->options['general']['exclude'] ) && is_array( $this->options['general']['exclude'] ) ) {
366 if ( ! isset( $this->options['general']['exclude_groups'] ) )
367 $this->options['general']['exclude_groups'] = $this->options['general']['exclude']['groups'] ?? [];
368
369 if ( ! isset( $this->options['general']['exclude_roles'] ) )
370 $this->options['general']['exclude_roles'] = $this->options['general']['exclude']['roles'] ?? [];
371 }
372
373 return;
374 }
375
376 // activation hooks
377 register_activation_hook( __FILE__, [ $this, 'activation' ] );
378 register_deactivation_hook( __FILE__, [ $this, 'deactivation' ] );
379
380 // settings
381 $this->options = [
382 'general' => array_merge( $this->defaults['general'], get_option( 'post_views_counter_settings_general', $this->defaults['general'] ) ),
383 'display' => array_merge( $this->defaults['display'], get_option( 'post_views_counter_settings_display', $this->defaults['display'] ) ),
384 'other' => array_merge( $this->defaults['other'], get_option( 'post_views_counter_settings_other', $this->defaults['other'] ) ),
385 'integrations' => array_merge( $this->defaults['integrations'], get_option( 'post_views_counter_settings_integrations', $this->defaults['integrations'] ) ),
386 'emails' => $emails
387 ];
388
389 $this->options['general']['time_between_counts'] = $this->normalize_time_between_counts( isset( $this->options['general']['time_between_counts'] ) ? $this->options['general']['time_between_counts'] : null );
390
391 // 1.5.3+
392 if ( ! isset( $this->options['general']['post_views_column'] ) )
393 $this->options['general']['post_views_column'] = $this->options['display']['post_views_column'];
394
395 if ( ! isset( $this->options['general']['restrict_edit_views'] ) )
396 $this->options['general']['restrict_edit_views'] = $this->options['display']['restrict_edit_views'];
397
398 // migrate exclude to new fields
399 if ( isset( $this->options['general']['exclude'] ) && is_array( $this->options['general']['exclude'] ) ) {
400 if ( ! isset( $this->options['general']['exclude_groups'] ) )
401 $this->options['general']['exclude_groups'] = $this->options['general']['exclude']['groups'] ?? [];
402
403 if ( ! isset( $this->options['general']['exclude_roles'] ) )
404 $this->options['general']['exclude_roles'] = $this->options['general']['exclude']['roles'] ?? [];
405 }
406
407 // actions
408 add_action( 'plugins_loaded', [ $this, 'extend_caching_plugins' ], -1 );
409 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
410 add_action( 'admin_print_styles', [ $this, 'admin_print_styles' ] );
411 add_action( 'wp_loaded', [ $this, 'load_pluggable_functions' ] );
412 add_action( 'init', [ $this, 'load_textdomain' ] );
413 add_action( 'init', [ $this, 'register_blocks' ] );
414 add_action( 'admin_init', [ $this, 'update_notice' ] );
415 add_action( 'wp_initialize_site', [ $this, 'init_new_network_site' ] );
416 add_action( 'wp_ajax_pvc_dismiss_notice', [ $this, 'dismiss_notice' ] );
417
418 // filters
419 add_filter( 'plugin_action_links_' . POST_VIEWS_COUNTER_BASENAME, [ $this, 'plugin_settings_link' ] );
420 }
421
422 /**
423 * Extend list of caching plugins.
424 *
425 * @return void
426 */
427 public function extend_caching_plugins() {
428 // add new caching plugins
429 add_filter( 'pvc_active_caching_plugins', [ $this->settings, 'extend_active_caching_plugins' ] );
430 add_filter( 'pvc_is_plugin_active', [ $this->settings, 'extend_is_plugin_active' ], 10, 2 );
431 }
432
433 /**
434 * Register blocks.
435 *
436 * @return void
437 */
438 public function register_blocks() {
439 // actions
440 add_action( 'enqueue_block_editor_assets', [ $this, 'block_editor_enqueue_scripts' ] );
441
442 // filters
443 add_filter( 'block_categories_all', [ $this, 'add_block_category' ] );
444
445 add_filter( 'register_block_type_args', [ $this, 'update_block_args' ], 10, 2 );
446
447 register_block_type( __DIR__ . '/blocks/most-viewed-posts' );
448 register_block_type( __DIR__ . '/blocks/post-views' );
449
450 // register placeholder blocks when the related blocks are unavailable
451 if ( ! class_exists( 'Post_Views_Counter_Pro' ) ) {
452 $pro_placeholders = [
453 'most-viewed-terms',
454 'most-viewed-users',
455 'term-views',
456 'user-views',
457 'site-views'
458 ];
459
460 foreach ( $pro_placeholders as $block_slug ) {
461 register_block_type( __DIR__ . '/blocks/pro-placeholder/' . $block_slug );
462 }
463 }
464 }
465
466 /**
467 * Enqueue block scripts.
468 *
469 * @return void
470 */
471 public function block_editor_enqueue_scripts() {
472 // register inline-only script handle
473 wp_register_script( 'post-views-counter-block-editor-script', false, [], false, true );
474 wp_enqueue_script( 'post-views-counter-block-editor-script' );
475
476 // enqueue block editor styles
477 wp_enqueue_style( 'pvc-block-editor', POST_VIEWS_COUNTER_URL . '/css/block-editor.css', [], $this->defaults['version'] );
478
479 $block_image_sizes = [];
480
481 // image sizes
482 $image_sizes = array_merge( [ 'full' ], get_intermediate_image_sizes() );
483
484 // sort image sizes by name, ascending
485 sort( $image_sizes, SORT_STRING );
486
487 foreach ( $image_sizes as $image_size ) {
488 $block_image_sizes[] = [
489 'label' => $image_size,
490 'value' => $image_size
491 ];
492 }
493
494 $post_types = Post_Views_Counter()->functions->get_post_types();
495
496 // prepare script data
497 $script_data = [
498 'postTypesKeys' => array_combine( array_keys( $post_types ), array_fill( 0, count( $post_types ), false ) ),
499 'postTypes' => $post_types,
500 'imageSizes' => $block_image_sizes,
501 'upgradeUrl' => $this->get_postviewscounter_url( '/upgrade/', 'link', 'upgrade-to-pro', 'block-editor-pro-placeholder-link', 'free' ),
502 'isProActive' => class_exists( 'Post_Views_Counter_Pro' )
503 ];
504
505 // force post as enabled
506 $script_data['postTypesKeys']['post'] = true;
507
508 // prepare script data
509 $script_data['periods'] = [ [
510 'label' => __( 'Total Views', 'post-views-counter' ),
511 'value' => 'total'
512 ] ];
513
514 $script_data = apply_filters( 'pvc_block_editor_data', $script_data );
515
516 wp_add_inline_script( 'post-views-counter-block-editor-script', 'var pvcBlockEditorData = ' . wp_json_encode( $script_data ) . ";\n", 'before' );
517 }
518
519 /**
520 * Add deterministic UTM parameters to postviewscounter.com URLs.
521 *
522 * @param string $url
523 * @param string $medium
524 * @param string $campaign
525 * @param string $content
526 * @param string $context
527 *
528 * @return string
529 */
530 public function add_postviewscounter_utm_args( $url, $medium = 'link', $campaign = 'general', $content = 'general', $context = 'free' ) {
531 $url = esc_url_raw( $url );
532
533 if ( $url === '' )
534 return '';
535
536 $host = wp_parse_url( $url, PHP_URL_HOST );
537
538 if ( ! is_string( $host ) )
539 return $url;
540
541 $host = strtolower( rtrim( $host, '.' ) );
542
543 if ( ! preg_match( '/(^|\.)postviewscounter\.com$/', $host ) )
544 return $url;
545
546 $allowed_mediums = [ 'button', 'link', 'email' ];
547 $allowed_contexts = [ 'free', 'pro-expired', 'pro-active' ];
548
549 $medium = sanitize_key( $medium );
550 $campaign = sanitize_key( $campaign );
551 $content = sanitize_key( $content );
552 $context = sanitize_key( $context );
553
554 if ( ! in_array( $medium, $allowed_mediums, true ) )
555 $medium = 'link';
556
557 if ( $campaign === '' )
558 $campaign = 'general';
559
560 if ( $content === '' )
561 $content = 'general';
562
563 if ( ! in_array( $context, $allowed_contexts, true ) )
564 $context = 'free';
565
566 $version = ! empty( $this->defaults['version'] ) ? (string) $this->defaults['version'] : (string) get_option( 'post_views_counter_version', '' );
567
568 $query_args = [
569 'utm_source' => 'post-views-counter-lite',
570 'utm_medium' => $medium,
571 'utm_campaign' => $campaign,
572 'utm_content' => $content,
573 'utm_context' => $context
574 ];
575
576 if ( $version !== '' )
577 $query_args['utm_version'] = sanitize_text_field( $version );
578
579 return add_query_arg( $query_args, $url );
580 }
581
582 /**
583 * Get a postviewscounter.com URL with deterministic UTM parameters.
584 *
585 * @param string $path
586 * @param string $medium
587 * @param string $campaign
588 * @param string $content
589 * @param string $context
590 *
591 * @return string
592 */
593 public function get_postviewscounter_url( $path = '/', $medium = 'link', $campaign = 'general', $content = 'general', $context = 'free' ) {
594 if ( ! is_string( $path ) || $path === '' )
595 $path = '/';
596 elseif ( preg_match( '#^https?://#i', $path ) ) {
597 $parsed_path = wp_parse_url( $path, PHP_URL_PATH );
598 $parsed_query = wp_parse_url( $path, PHP_URL_QUERY );
599 $parsed_fragment = wp_parse_url( $path, PHP_URL_FRAGMENT );
600
601 $path = is_string( $parsed_path ) && $parsed_path !== '' ? $parsed_path : '/';
602
603 if ( is_string( $parsed_query ) && $parsed_query !== '' )
604 $path .= '?' . $parsed_query;
605
606 if ( is_string( $parsed_fragment ) && $parsed_fragment !== '' )
607 $path .= '#' . $parsed_fragment;
608 }
609
610 if ( $path === '/' )
611 $url = 'https://postviewscounter.com/';
612 else
613 $url = 'https://postviewscounter.com/' . ltrim( $path, '/' );
614
615 return $this->add_postviewscounter_utm_args( $url, $medium, $campaign, $content, $context );
616 }
617
618 /**
619 * Update block arguments.
620 *
621 * @param array $args
622 * @param string $block_type
623 *
624 * @return array
625 */
626 public function update_block_args( $args, $block_type ) {
627 // most viewed posts block
628 if ( $block_type === 'post-views-counter/most-viewed-posts' )
629 $args['render_callback'] = [ $this, 'most_viewed_posts_render_callback' ];
630 // post views block
631 elseif ( $block_type === 'post-views-counter/post-views' )
632 $args['render_callback'] = [ $this, 'post_views_render_callback' ];
633
634 return $args;
635 }
636
637 /**
638 * Server side block renderer for most viewed posts.
639 *
640 * @param array $attributes
641 * @param string $content
642 *
643 * @return array
644 */
645 public function most_viewed_posts_render_callback( $attributes, $content ) {
646 $post_types = [];
647
648 foreach ( $attributes['postTypes'] as $post_type => $enabled ) {
649 if ( $enabled === true || $enabled === 'true' )
650 $post_types[] = $post_type;
651 }
652
653 // map block attributes
654 $args = [
655 'number_of_posts' => max( 1, (int) $attributes['numberOfPosts'] ),
656 'post_type' => $post_types,
657 'period' => $attributes['period'],
658 'order' => $attributes['order'],
659 'thumbnail_size' => $attributes['thumbnailSize'],
660 'list_type' => $attributes['listType'],
661 'show_post_views' => (bool) $attributes['displayPostViews'],
662 'show_post_thumbnail' => (bool) $attributes['displayPostThumbnail'],
663 'show_post_author' => (bool) $attributes['displayPostAuthor'],
664 'show_post_excerpt' => (bool) $attributes['displayPostExcerpt'],
665 'no_posts_message' => $attributes['noPostsMessage']
666 ];
667
668 $title = trim( $attributes['title'] );
669
670 $html = '<div ' . get_block_wrapper_attributes() . '>';
671
672 if ( $title !== '' )
673 $html .= '<h2 class="block-title">' . esc_html( $title ) . '</h2>';
674
675 $html .= pvc_most_viewed_posts( $args, false );
676 $html .= '</div>';
677
678 return $html;
679 }
680
681 /**
682 * Server side block renderer for post views.
683 *
684 * @param array $attributes
685 * @param string $content
686 *
687 * @return array
688 */
689 public function post_views_render_callback( $attributes, $content ) {
690 $html = '<div ' . get_block_wrapper_attributes() . '>';
691 $html .= pvc_post_views( (int) $attributes['postID'], false, $attributes['period'] );
692 $html .= '</div>';
693
694 return $html;
695 }
696
697 /**
698 * Add new blocks category.
699 *
700 * @param array $categories
701 *
702 * @return array
703 */
704 public function add_block_category( $categories ) {
705 $categories[] = [
706 'slug' => 'post-views-counter',
707 'title' => 'Post Views Counter'
708 ];
709
710 return $categories;
711 }
712
713 /**
714 * Update notice.
715 *
716 * @return void
717 */
718 public function update_notice() {
719 if ( ! current_user_can( 'install_plugins' ) )
720 return;
721
722 $current_update = 2;
723
724 // get current time
725 $current_time = time();
726
727 if ( $this->options['general']['update_version'] < $current_update ) {
728 // check version, if update version is lower than plugin version, set update notice to true
729 $this->options['general'] = array_merge(
730 $this->options['general'],
731 [
732 'update_version' => $current_update,
733 'update_notice' => true
734 ]
735 );
736
737 update_option( 'post_views_counter_settings_general', $this->options['general'] );
738
739 // set activation date
740 $activation_date = get_option( 'post_views_counter_activation_date' );
741
742 if ( $activation_date === false )
743 update_option( 'post_views_counter_activation_date', $current_time );
744 }
745
746 // display current version notice
747 if ( $this->options['general']['update_notice'] === true ) {
748 // include notice js, only if needed
749 add_action( 'admin_print_scripts', [ $this, 'admin_inline_js' ], 999 );
750
751 // get activation date
752 $activation_date = get_option( 'post_views_counter_activation_date' );
753
754 if ( (int) $this->options['general']['update_delay_date'] === 0 ) {
755 if ( $activation_date + 2 * WEEK_IN_SECONDS > $current_time )
756 $this->options['general']['update_delay_date'] = $activation_date + 2 * WEEK_IN_SECONDS;
757 else
758 $this->options['general']['update_delay_date'] = $current_time;
759
760 update_option( 'post_views_counter_settings_general', $this->options['general'] );
761 }
762
763 if ( ( ! empty( $this->options['general']['update_delay_date'] ) ? (int) $this->options['general']['update_delay_date'] : $current_time ) <= $current_time ) {
764 $review_notice_brand_url = esc_url( $this->get_postviewscounter_url( '/', 'link', 'review-notice', 'review-notice-brand-link', 'free' ) );
765
766 $this->add_notice( sprintf( __( "Hey, you've been using <strong>Post Views Counter</strong> for more than %s.", 'post-views-counter' ), human_time_diff( $activation_date, $current_time ) ) . '<br />' . __( 'Could you please do me a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our motivation.', 'post-views-counter' ) . '<br /><br />' . __( 'Your help is much appreciated. Thank you very much', 'post-views-counter' ) . ' ~ <strong>Bartosz Arendt</strong>, ' . __( 'founder of', 'post-views-counter' ) . ' <a href="' . $review_notice_brand_url . '" target="_blank">Post Views Counter</a>.' . '<br /><br />' . '<a href="https://wordpress.org/support/plugin/post-views-counter/reviews/?filter=5#new-post" class="pvc-dismissible-notice" target="_blank" rel="noopener">' . __( 'Ok, you deserve it', 'post-views-counter' ) . '</a><br /><a href="#" class="pvc-dismissible-notice pvc-delay-notice" rel="noopener">' . __( 'Nope, maybe later', 'post-views-counter' ) . '</a><br /><a href="#" class="pvc-dismissible-notice" rel="noopener">' . __( 'I already did', 'post-views-counter' ) . '</a>', 'notice notice-info is-dismissible pvc-notice' );
767 }
768 }
769 }
770
771 /**
772 * Add admin notices.
773 *
774 * @param string $html Notice HTML
775 * @param string $status Notice status
776 * @param bool $paragraph Whether to use paragraph
777 * @param bool $network
778 *
779 * @return void
780 */
781 public function add_notice( $html = '', $status = 'error', $paragraph = true, $network = false ) {
782 $this->notices[] = [
783 'html' => $html,
784 'status' => $status,
785 'paragraph' => $paragraph
786 ];
787
788 add_action( 'admin_notices', [ $this, 'display_notice' ] );
789
790 if ( $network )
791 add_action( 'network_admin_notices', [ $this, 'display_notice' ] );
792 }
793
794 /**
795 * Print admin notices.
796 *
797 * @return void
798 */
799 public function display_notice() {
800 $allowed_html = array_merge(
801 wp_kses_allowed_html( 'post' ),
802 [
803 'input' => [
804 'type' => true,
805 'name' => true,
806 'class' => true,
807 'value' => true
808 ],
809 'form' => [
810 'action' => true,
811 'method' => true
812 ]
813 ]
814 );
815
816 foreach ( $this->notices as $notice ) {
817 echo '
818 <div class="' . esc_attr( $notice['status'] ) . '">
819 ' . ( $notice['paragraph'] ? '<p>' : '' ) . '
820 ' . wp_kses( $notice['html'], $allowed_html ) . '
821 ' . ( $notice['paragraph'] ? '</p>' : '' ) . '
822 </div>';
823 }
824 }
825
826 /**
827 * Print admin scripts.
828 *
829 * @return void
830 */
831 public function admin_inline_js() {
832 if ( ! current_user_can( 'install_plugins' ) )
833 return;
834
835 // register and enqueue styles
836 wp_register_script( 'pvc-notices', false );
837 wp_enqueue_script( 'pvc-notices' );
838
839 // add styles
840 wp_add_inline_script( 'pvc-notices', "
841 ( function( $ ) {
842 // ready event
843 $( function() {
844 // save dismiss state
845 $( '.pvc-notice.is-dismissible' ).on( 'click', '.notice-dismiss, .pvc-dismissible-notice', function( e ) {
846 if ( e.currentTarget.target !== '_blank' )
847 e.preventDefault();
848
849 var notice_action = 'hide';
850
851 if ( e.currentTarget.classList.contains( 'pvc-delay-notice' ) )
852 notice_action = 'delay';
853
854 $.post( ajaxurl, {
855 action: 'pvc_dismiss_notice',
856 notice_action: notice_action,
857 url: '" . esc_url_raw( admin_url( 'admin-ajax.php' ) ) . "',
858 nonce: '" . esc_attr( wp_create_nonce( 'pvc_dismiss_notice' ) ) . "'
859 } );
860
861 $( e.delegateTarget ).slideUp( 'fast' );
862 } );
863 } );
864 } )( jQuery );
865 ", 'after' );
866 }
867
868 /**
869 * Dismiss notice.
870 *
871 * @return void
872 */
873 public function dismiss_notice() {
874 if ( ! current_user_can( 'install_plugins' ) )
875 return;
876
877 if ( isset( $_REQUEST['nonce'] ) && wp_verify_nonce( $_REQUEST['nonce'], 'pvc_dismiss_notice' ) ) {
878 $notice_action = empty( $_REQUEST['notice_action'] ) || $_REQUEST['notice_action'] === 'hide' ? 'hide' : sanitize_text_field( $_REQUEST['notice_action'] );
879
880 switch ( $notice_action ) {
881 // delay notice
882 case 'delay':
883 // set delay period to 1 week from now
884 $this->options['general'] = array_merge(
885 $this->options['general'],
886 [
887 'update_delay_date' => time() + 2 * WEEK_IN_SECONDS
888 ]
889 );
890 update_option( 'post_views_counter_settings_general', $this->options['general'] );
891 break;
892
893 // hide notice
894 default:
895 $this->options['general'] = array_merge(
896 $this->options['general'],
897 [
898 'update_notice' => false
899 ]
900 );
901 $this->options['general'] = array_merge(
902 $this->options['general'],
903 [
904 'update_delay_date' => 0
905 ]
906 );
907
908 update_option( 'post_views_counter_settings_general', $this->options['general'] );
909 }
910 }
911
912 exit;
913 }
914
915 /**
916 * Plugin activation.
917 *
918 * @global object $wpdb
919 *
920 * @param bool $network
921 *
922 * @return void
923 */
924 public function activation( $network ) {
925 // network activation?
926 if ( is_multisite() && $network ) {
927 global $wpdb;
928
929 // get all available sites
930 $blogs_ids = $wpdb->get_col( 'SELECT blog_id FROM ' . $wpdb->blogs );
931
932 foreach ( $blogs_ids as $blog_id ) {
933 // change to another site
934 switch_to_blog( (int) $blog_id );
935
936 // run current site activation process
937 $this->activate_site();
938
939 restore_current_blog();
940 }
941 } else
942 $this->activate_site();
943 }
944
945 /**
946 * Single site activation.
947 *
948 * @global object $wpdb
949 * @global string $charset_collate
950 *
951 * @return void
952 */
953 public function activate_site() {
954 global $wpdb, $charset_collate;
955
956 // required for dbdelta
957 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
958
959 // create post views table
960 dbDelta( '
961 CREATE TABLE IF NOT EXISTS ' . $wpdb->prefix . 'post_views (
962 `id` bigint unsigned NOT NULL,
963 `type` tinyint(1) unsigned NOT NULL,
964 `period` varchar(8) NOT NULL,
965 `count` bigint unsigned NOT NULL,
966 PRIMARY KEY (type, period, id),
967 UNIQUE INDEX id_type_period_count (id, type, period, count) USING BTREE,
968 INDEX type_period_count (type, period, count) USING BTREE
969 ) ' . $charset_collate . ';'
970 );
971
972 // add default options
973 add_option( 'post_views_counter_settings_general', $this->defaults['general'], null, false );
974 add_option( 'post_views_counter_settings_display', $this->defaults['display'], null, false );
975 add_option( 'post_views_counter_settings_other', $this->defaults['other'], null, false );
976 add_option( 'post_views_counter_settings_emails', $this->get_default_emails_settings(), null, false );
977 add_option( 'post_views_counter_version', $this->defaults['version'], null, false );
978
979 if ( $this->emails_scheduler instanceof Post_Views_Counter_Emails_Scheduler )
980 $this->emails_scheduler->maybe_schedule( get_option( 'post_views_counter_settings_emails', $this->get_default_emails_settings() ) );
981 }
982
983 /**
984 * Get emails defaults for new option creation.
985 *
986 * @return array
987 */
988 public function get_default_emails_settings() {
989 $defaults = $this->defaults['emails'];
990 $admin_email = get_option( 'admin_email' );
991
992 if ( is_email( $admin_email ) )
993 $defaults['recipient'] = $admin_email;
994
995 return $defaults;
996 }
997
998 /**
999 * Plugin deactivation.
1000 *
1001 * @global object $wpdb
1002 *
1003 * @param bool $network
1004 *
1005 * @return void
1006 */
1007 public function deactivation( $network ) {
1008 // network deactivation?
1009 if ( is_multisite() && $network ) {
1010 global $wpdb;
1011
1012 // get all available sites
1013 $blogs_ids = $wpdb->get_col( 'SELECT blog_id FROM ' . $wpdb->blogs );
1014
1015 foreach ( $blogs_ids as $blog_id ) {
1016 // change to another site
1017 switch_to_blog( (int) $blog_id );
1018
1019 // run current site deactivation process
1020 $this->deactivate_site( true );
1021
1022 restore_current_blog();
1023 }
1024 } else
1025 $this->deactivate_site();
1026 }
1027
1028 /**
1029 * Single site deactivation.
1030 *
1031 * @global object $wpdb
1032 *
1033 * @param bool $multi
1034 *
1035 * @return void
1036 */
1037 public function deactivate_site( $multi = false ) {
1038 if ( $this->emails_scheduler instanceof Post_Views_Counter_Emails_Scheduler )
1039 $this->emails_scheduler->clear();
1040 else
1041 wp_clear_scheduled_hook( 'pvc_weekly_content_summary_send' );
1042
1043 if ( $multi === true ) {
1044 $options = get_option( 'post_views_counter_settings_other' );
1045 $check = $options['deactivation_delete'];
1046 } else
1047 $check = $this->options['other']['deactivation_delete'];
1048
1049 // delete options if needed
1050 if ( $check ) {
1051 // delete options
1052 delete_option( 'post_views_counter_settings_general' );
1053 delete_option( 'post_views_counter_settings_display' );
1054 delete_option( 'post_views_counter_settings_other' );
1055 delete_option( 'post_views_counter_settings_emails' );
1056 delete_option( 'post_views_counter_activation_date' );
1057 delete_option( 'post_views_counter_version' );
1058
1059 // delete transients
1060 delete_transient( 'post_views_counter_ip_cache' );
1061
1062 global $wpdb;
1063
1064 // delete table from database
1065 $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'post_views' );
1066 }
1067
1068 // remove schedule
1069 wp_clear_scheduled_hook( 'pvc_reset_counts' );
1070
1071 remove_action( 'pvc_reset_counts', [ $this->cron, 'reset_counts' ] );
1072 }
1073
1074 /**
1075 * Initialize new network site.
1076 *
1077 * @param object $site
1078 *
1079 * @return void
1080 */
1081 public function init_new_network_site( $site ) {
1082 if ( is_multisite() ) {
1083 // change to another site
1084 switch_to_blog( $site->blog_id );
1085
1086 // run current site activation process
1087 $this->activate_site();
1088
1089 restore_current_blog();
1090 }
1091 }
1092
1093 /**
1094 * Load text domain.
1095 *
1096 * @return void
1097 */
1098 public function load_textdomain() {
1099 load_plugin_textdomain( 'post-views-counter', false, POST_VIEWS_COUNTER_REL_PATH . '/languages/' );
1100 }
1101
1102 /**
1103 * Load pluggable template functions.
1104 *
1105 * @return void
1106 */
1107 public function load_pluggable_functions() {
1108 include_once( POST_VIEWS_COUNTER_PATH . 'includes/functions.php' );
1109 }
1110
1111 /**
1112 * Enqueue admin scripts and styles.
1113 *
1114 * @global string $post_type
1115 * @global string $wp_version
1116 *
1117 * @param string $page
1118 *
1119 * @return void
1120 */
1121 public function admin_enqueue_scripts( $page ) {
1122 global $post_type;
1123 global $wp_version;
1124
1125 // register styles
1126 wp_register_style( 'pvc-admin', POST_VIEWS_COUNTER_URL . '/css/admin-settings.css', [], $this->defaults['version'] );
1127 wp_register_style( 'pvc-admin-post-style', POST_VIEWS_COUNTER_URL . '/css/admin-post.css', [], $this->defaults['version'] );
1128
1129 // register scripts
1130 wp_register_script( 'pvc-admin-settings', POST_VIEWS_COUNTER_URL . '/js/admin-settings.js', [ 'jquery' ], $this->defaults['version'] );
1131 wp_register_script( 'pvc-admin-post', POST_VIEWS_COUNTER_URL . '/js/admin-post.js', [ 'jquery' ], $this->defaults['version'] );
1132 wp_register_script( 'pvc-admin-quick-edit', POST_VIEWS_COUNTER_URL . '/js/admin-quick-edit.js', [ 'jquery' ], $this->defaults['version'] );
1133
1134 // load on pvc settings page
1135 if ( in_array( $page, [ 'toplevel_page_post-views-counter', 'settings_page_post-views-counter' ], true ) ) {
1136 wp_enqueue_script( 'pvc-admin-settings' );
1137
1138 // prepare script data
1139 $script_data = [
1140 'resetToDefaults' => esc_html__( 'Are you sure you want to reset these settings to defaults?', 'post-views-counter' ),
1141 'resetViews' => esc_html__( 'Are you sure you want to delete all existing data?', 'post-views-counter' ),
1142 'importViews' => esc_html__( 'Are you sure you want to import views now?', 'post-views-counter' ),
1143 'testEmail' => [
1144 'invalidResponse' => esc_html__( 'The test email request did not return a valid response.', 'post-views-counter' ),
1145 'unexpectedError' => esc_html__( 'The test email request could not be completed. Please try again.', 'post-views-counter' )
1146 ]
1147 ];
1148
1149 wp_add_inline_script( 'pvc-admin-settings', 'var pvcArgsSettings = ' . wp_json_encode( $script_data ) . ";\n", 'before' );
1150
1151 wp_enqueue_style( 'pvc-admin' );
1152 // load on single post page
1153 } elseif ( $page === 'post.php' || $page === 'post-new.php' ) {
1154 $post_types = Post_Views_Counter()->options['general']['post_types_count'];
1155
1156 if ( ! in_array( $post_type, (array) $post_types ) )
1157 return;
1158
1159 wp_enqueue_style( 'pvc-admin-post-style' );
1160
1161 // only enqueue post script in classic editor (not block editor)
1162 // block editor doesn't have the #post-views metabox that this script targets
1163 if ( ! function_exists( 'use_block_editor_for_post' ) ) {
1164 // WP < 5.0, always use classic editor
1165 wp_enqueue_script( 'pvc-admin-post' );
1166 } else {
1167 // get post ID from request
1168 $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : ( isset( $_POST['post_ID'] ) ? absint( $_POST['post_ID'] ) : 0 );
1169
1170 // check if classic editor is being used
1171 if ( $post_id && ! use_block_editor_for_post( $post_id ) ) {
1172 wp_enqueue_script( 'pvc-admin-post' );
1173 } elseif ( ! $post_id && $page === 'post-new.php' && ! use_block_editor_for_post_type( $post_type ) ) {
1174 // new post - check if block editor is default for this post type
1175 wp_enqueue_script( 'pvc-admin-post' );
1176 }
1177 }
1178 // edit post
1179 } elseif ( $page === 'edit.php' ) {
1180 $post_types = Post_Views_Counter()->options['general']['post_types_count'];
1181
1182 if ( ! in_array( $post_type, (array) $post_types ) )
1183 return;
1184
1185 wp_enqueue_style( 'pvc-admin-post-style' );
1186
1187 wp_enqueue_script( 'pvc-admin-quick-edit' );
1188
1189 // prepare script data
1190 $script_data = [
1191 'nonce' => wp_create_nonce( 'pvc_save_bulk_post_views' ),
1192 'wpVersion59' => version_compare( $wp_version, '5.9', '>=' )
1193 ];
1194
1195 wp_add_inline_script( 'pvc-admin-quick-edit', 'var pvcArgsQuickEdit = ' . wp_json_encode( $script_data ) . ";\n", 'before' );
1196 // widgets
1197 } elseif ( $page === 'widgets.php' )
1198 wp_enqueue_script( 'pvc-admin-widgets', POST_VIEWS_COUNTER_URL . '/js/admin-widgets.js', [ 'jquery' ], $this->defaults['version'] );
1199 // media
1200 elseif ( $page === 'upload.php' )
1201 wp_enqueue_style( 'pvc-admin-post-style' );
1202 }
1203
1204 /**
1205 * Load admin style inline, for menu badge only.
1206 *
1207 * @return void
1208 */
1209 public function admin_print_styles() {
1210 echo '
1211 <style>
1212 .toplevel_page_post-views-counter .pvc-admin-menu-new,
1213 .settings_page_post-views-counter .pvc-admin-menu-new {
1214 font-size: 10px;
1215 vertical-align: super;
1216 color: #ffc107;
1217 }
1218 </style>
1219 ';
1220 }
1221
1222 /**
1223 * Normalize the Count Interval option to canonical hours.
1224 *
1225 * @param mixed $time_between_counts
1226 * @param array $default_interval
1227 *
1228 * @return array
1229 */
1230 public function normalize_time_between_counts( $time_between_counts, $default_interval = [] ) {
1231 $default_number = isset( $default_interval['number'] ) ? (int) $default_interval['number'] : (int) $this->defaults['general']['time_between_counts']['number'];
1232 $default_interval = [
1233 'number' => $default_number,
1234 'type' => 'hours'
1235 ];
1236
1237 if ( ! is_array( $time_between_counts ) )
1238 return $default_interval;
1239
1240 if ( ! isset( $time_between_counts['number'], $time_between_counts['type'] ) || ! is_scalar( $time_between_counts['number'] ) || ! is_scalar( $time_between_counts['type'] ) )
1241 return $default_interval;
1242
1243 $number = (int) $time_between_counts['number'];
1244 $type = sanitize_key( (string) $time_between_counts['type'] );
1245
1246 switch ( $type ) {
1247 case 'minutes':
1248 $hours = (int) ( $number / 60 );
1249 break;
1250 case 'hours':
1251 $hours = $number;
1252 break;
1253 case 'days':
1254 $hours = $number * 24;
1255 break;
1256 case 'weeks':
1257 $hours = $number * 168;
1258 break;
1259 case 'months':
1260 $hours = $number * 720;
1261 break;
1262 case 'years':
1263 $hours = $number * 24 * 365;
1264 break;
1265 default:
1266 return $default_interval;
1267 }
1268
1269 return [
1270 'number' => max( 0, min( 720, (int) $hours ) ),
1271 'type' => 'hours'
1272 ];
1273 }
1274
1275 /**
1276 * Get the preferred admin menu position.
1277 *
1278 * @return string
1279 */
1280 public function get_menu_position() {
1281 $position = isset( $this->options['display']['menu_position'] ) ? $this->options['display']['menu_position'] : '';
1282
1283 if ( in_array( $position, [ 'top', 'sub' ], true ) )
1284 return $position;
1285
1286 if ( isset( $this->options['other']['menu_position'] ) && in_array( $this->options['other']['menu_position'], [ 'top', 'sub' ], true ) )
1287 return $this->options['other']['menu_position'];
1288
1289 return 'top';
1290 }
1291
1292 /**
1293 * Add link to Settings page.
1294 *
1295 * @param array $links
1296 *
1297 * @return array
1298 */
1299 public function plugin_settings_link( $links ) {
1300 if ( ! current_user_can( 'manage_options' ) )
1301 return $links;
1302
1303 // submenu?
1304 if ( $this->get_menu_position() === 'sub' )
1305 $url = admin_url( 'options-general.php?page=post-views-counter' );
1306 // topmenu?
1307 else
1308 $url = admin_url( 'admin.php?page=post-views-counter' );
1309
1310 array_unshift( $links, sprintf( '<a href="%s">%s</a>', esc_url_raw( $url ), esc_html__( 'Settings', 'post-views-counter' ) ) );
1311
1312 return $links;
1313 }
1314 }
1315 }
1316
1317 /**
1318 * Initialize Post Views Counter.
1319 *
1320 * @return object
1321 */
1322 function Post_Views_Counter() {
1323 static $instance;
1324
1325 // first call to instance() initializes the plugin
1326 if ( $instance === null || ! ( $instance instanceof Post_Views_Counter ) )
1327 $instance = Post_Views_Counter::instance();
1328
1329 return $instance;
1330 }
1331
1332 Post_Views_Counter();
1333