PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.7.13
Post Views Counter v1.7.13
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 / includes / class-settings.php
post-views-counter / includes Last commit date
class-admin.php 2 weeks ago class-columns-modal.php 2 weeks ago class-columns.php 2 weeks ago class-counter.php 2 weeks ago class-crawler-detect.php 2 weeks ago class-cron.php 2 weeks ago class-dashboard.php 2 weeks ago class-emails-mailer.php 2 weeks ago class-emails-period.php 2 weeks ago class-emails-query.php 2 weeks ago class-emails-scheduler.php 2 weeks ago class-emails-template.php 2 weeks ago class-emails.php 2 weeks ago class-frontend.php 2 weeks ago class-functions.php 2 weeks ago class-import.php 2 weeks ago class-integration-gutenberg.php 2 weeks ago class-integrations.php 2 weeks ago class-query.php 2 weeks ago class-settings-api.php 2 weeks ago class-settings-display.php 2 weeks ago class-settings-emails.php 2 weeks ago class-settings-general.php 2 weeks ago class-settings-integrations.php 2 weeks ago class-settings-other.php 2 weeks ago class-settings-reports.php 2 weeks ago class-settings.php 2 weeks ago class-toolbar.php 2 weeks ago class-traffic-signals.php 2 weeks ago class-update.php 2 weeks ago class-widgets.php 2 weeks ago functions.php 2 weeks ago
class-settings.php
1092 lines
1 <?php
2 // exit if accessed directly
3 if ( ! defined( 'ABSPATH' ) )
4 exit;
5
6 /**
7 * Post_Views_Counter_Settings class.
8 *
9 * @class Post_Views_Counter_Settings
10 */
11 class Post_Views_Counter_Settings {
12
13 /**
14 * @var Post_Views_Counter_Settings_General
15 */
16 public $general;
17
18 /**
19 * @var Post_Views_Counter_Settings_Display
20 */
21 public $display;
22
23 /**
24 * @var Post_Views_Counter_Settings_Reports
25 */
26 public $reports;
27
28 /**
29 * @var Post_Views_Counter_Settings_Other
30 */
31 public $other;
32
33 /**
34 * @var Post_Views_Counter_Settings_Integrations
35 */
36 public $integrations;
37
38 /**
39 * @var Post_Views_Counter_Settings_Emails
40 */
41 public $emails;
42
43 /**
44 * Class constructor.
45 *
46 * @return void
47 */
48 public function __construct() {
49 // actions
50 add_action( 'pvc_settings_sidebar', [ $this, 'settings_sidebar' ], 12, 4 );
51 add_action( 'pvc_settings_form', [ $this, 'settings_form' ], 10, 4 );
52
53 // filters
54 add_filter( 'pvc_settings_data', [ $this, 'settings_data' ], 1 );
55 add_filter( 'pvc_settings_data', [ $this, 'settings_fields_compat' ], 2 );
56 add_filter( 'pvc_settings_data', [ $this, 'normalize_pro_settings' ], 10 );
57 add_filter( 'pvc_settings_data', [ $this, 'settings_sections_compat' ], 99 );
58 add_filter( 'pvc_settings_pages', [ $this, 'settings_page' ] );
59 add_filter( 'pvc_settings_page_class', [ $this, 'settings_page_class' ] );
60 add_filter( 'pvc_plugin_status_tables', [ $this, 'register_core_tables' ] );
61
62 // instantiate page classes
63 $this->general = new Post_Views_Counter_Settings_General();
64 $this->display = new Post_Views_Counter_Settings_Display();
65 $this->reports = new Post_Views_Counter_Settings_Reports();
66 $this->integrations = new Post_Views_Counter_Settings_Integrations();
67 $this->emails = new Post_Views_Counter_Settings_Emails();
68 $this->other = new Post_Views_Counter_Settings_Other( $this );
69 }
70
71 /**
72 * Magic method to proxy method calls to page classes for backward compatibility.
73 *
74 * @param string $method
75 * @param array $args
76 *
77 * @return mixed
78 */
79 public function __call( $method, $args ) {
80 // Check if method exists in general page class
81 if ( method_exists( $this->general, $method ) ) {
82 return call_user_func_array( [ $this->general, $method ], $args );
83 }
84
85 // Check if method exists in display page class
86 if ( method_exists( $this->display, $method ) ) {
87 return call_user_func_array( [ $this->display, $method ], $args );
88 }
89
90 // Check if method exists in reports page class
91 if ( method_exists( $this->reports, $method ) ) {
92 return call_user_func_array( [ $this->reports, $method ], $args );
93 }
94
95 // Check if method exists in integrations page class
96 if ( method_exists( $this->integrations, $method ) ) {
97 return call_user_func_array( [ $this->integrations, $method ], $args );
98 }
99
100 // Check if method exists in emails page class
101 if ( method_exists( $this->emails, $method ) ) {
102 return call_user_func_array( [ $this->emails, $method ], $args );
103 }
104
105 // Check if method exists in other page class
106 if ( method_exists( $this->other, $method ) ) {
107 return call_user_func_array( [ $this->other, $method ], $args );
108 }
109
110 // Method not found
111 throw new BadMethodCallException( "Method {$method} does not exist" );
112 }
113
114 /**
115 * Add hidden inputs to redirect to valid page after changing menu position.
116 *
117 * @param string $setting
118 * @param string $page_type
119 * @param string $url_page
120 * @param string $tab_key
121 *
122 * @return void
123 */
124 public function settings_form( $setting, $page_type, $url_page, $tab_key ) {
125 // get main instance
126 $pvc = Post_Views_Counter();
127 $menu_position = $pvc->get_menu_position();
128
129 // topmenu referer
130 $topmenu = '<input type="hidden" name="_wp_http_referer" data-pvc-menu="topmenu" value="' .esc_url( admin_url( 'admin.php?page=post-views-counter' . ( $tab_key !== '' ? '&tab=' . $tab_key : '' ) ) ) . '" />';
131
132 // submenu referer
133 $submenu = '<input type="hidden" name="_wp_http_referer" data-pvc-menu="submenu" value="' .esc_url( admin_url( 'options-general.php?page=post-views-counter' . ( $tab_key !== '' ? '&tab=' . $tab_key : '' ) ) ) . '" />';
134
135 if ( $menu_position === 'sub' )
136 echo $topmenu . $submenu;
137 else
138 echo $submenu . $topmenu;
139 }
140
141 /**
142 * Display settings sidebar.
143 *
144 * @param string $setting
145 * @param string $page_type
146 * @param string $url_page
147 * @param string $tab_key
148 *
149 * @return void
150 */
151 public function settings_sidebar( $setting = '', $page_type = '', $url_page = '', $tab_key = 'general' ) {
152 if ( class_exists( 'Post_Views_Counter_Pro' ) ) {
153 return;
154 }
155
156 // get sidebar content for current tab
157 $content = $this->get_sidebar_content();
158
159 // use general tab content if tab not found
160 if ( ! isset( $content[$tab_key] ) ) {
161 $tab_key = 'general';
162 }
163
164 $tab_content = $content[$tab_key];
165
166 // build body HTML
167 $body_html = '<h4 class="pvc-sidebar-subtitle">' . esc_html( $tab_content['subtitle'] ) . '</h4>';
168
169 // add bullets if present
170 if ( ! empty( $tab_content['bullets'] ) ) {
171 foreach ( $tab_content['bullets'] as $bullet ) {
172 $body_html .= '<p><span class="pvc-icon pvc-icon-check"></span>' . $bullet . '</p>';
173 }
174 }
175
176 // add one-liner
177 $body_html .= '<div class="pvc-sidebar-one-liner">' . esc_html( $tab_content['one_liner'] ) . '</div>';
178
179 echo '
180 <div class="post-views-sidebar">
181 <div class="post-views-credits">
182 <div class="inside">
183 <div class="inner">
184 <div class="pvc-sidebar-info">
185 <div class="pvc-sidebar-head">
186 <h3 class="pvc-sidebar-title">' . 'Post Views Counter' . '</h3>
187 </div>
188 <div class="pvc-sidebar-body">
189 ' . $body_html . '
190 </div>
191 <div class="pvc-sidebar-footer">
192 <a href="' . esc_url( Post_Views_Counter()->get_postviewscounter_url( '/upgrade/', 'button', 'upgrade-to-pro', 'settings-sidebar-upgrade-button', 'free' ) ) . '" class="button button-secondary button-hero pvc-button" target="_blank">' . esc_html__( 'Upgrade to Pro', 'post-views-counter' ) . ' &rarr;</a>
193 <p>' . esc_html__( 'Starting from $29 per year', 'post-views-counter' ) . '<br />' . esc_html__( '14-day money back guarantee.', 'post-views-counter' ) . '</p>
194 </div>
195 </div>
196 </div>
197 </div>
198 </div>
199 </div>';
200 }
201
202 /**
203 * Get sidebar content for each settings tab.
204 *
205 * @return array
206 */
207 private function get_sidebar_content() {
208 return [
209 'general' => [
210 'subtitle' => __( 'Reliable view counting', 'post-views-counter' ),
211 'bullets' => [
212 __( 'Accurate counts with caching enabled', 'post-views-counter' ),
213 __( 'Better performance under high traffic', 'post-views-counter' ),
214 __( 'Protection against fake and repeated views', 'post-views-counter' )
215 ],
216 'one_liner' => __( 'Upgrade to Pro to make your view counts reliable on real-world, cached WordPress sites.', 'post-views-counter' )
217 ],
218 'display' => [
219 'subtitle' => __( 'Accurate & meaningful view display', 'post-views-counter' ),
220 'bullets' => [
221 __( 'Choose which time period is displayed', 'post-views-counter' ),
222 __( 'Always show up-to-date view counts', 'post-views-counter' ),
223 __( 'Control where the counter appears', 'post-views-counter' )
224 ],
225 'one_liner' => __( 'Upgrade to Pro to control what view data visitors actually see - not just total counts.', 'post-views-counter' )
226 ],
227 'reports' => [
228 'subtitle' => __( 'Advanced view reports', 'post-views-counter' ),
229 'bullets' => [],
230 'one_liner' => __( 'Upgrade to Pro to access detailed reports and visual insights for your content.', 'post-views-counter' )
231 ],
232 'integrations' => [
233 'subtitle' => __( 'Use view counts where they actually matter', 'post-views-counter' ),
234 'bullets' => [
235 __( 'Display and sort content by popularity', 'post-views-counter' ),
236 __( 'Works with the tools you already use', 'post-views-counter' ),
237 __( 'No custom queries or advanced setup', 'post-views-counter' )
238 ],
239 'one_liner' => __( 'Upgrade to Pro to use view data across layouts and blocks - not just store it.', 'post-views-counter' )
240 ],
241 'emails' => [
242 'subtitle' => __( 'Deeper email summaries', 'post-views-counter' ),
243 'bullets' => [
244 __( 'Spot content gaining or losing momentum', 'post-views-counter' ),
245 __( 'Include author and source summaries', 'post-views-counter' ),
246 __( 'Send daily or monthly summaries', 'post-views-counter' ),
247 __( 'Add CC/BCC recipients for your team', 'post-views-counter' )
248 ],
249 'one_liner' => __( 'Go beyond a weekly top-content list. Pro adds richer summaries with trends, authors, sources, and flexible delivery options.', 'post-views-counter' )
250 ],
251 'other' => [
252 'subtitle' => __( 'Migrate your view data safely', 'post-views-counter' ),
253 'bullets' => [
254 __( 'Import view data from other WordPress plugins', 'post-views-counter' ),
255 __( 'Choose how existing data is handled', 'post-views-counter' ),
256 __( 'Support for posts and other content types', 'post-views-counter' )
257 ],
258 'one_liner' => __( 'Upgrade to Pro to migrate historical view data without losing control over existing counts.', 'post-views-counter' )
259 ]
260 ];
261 }
262
263 /**
264 * Add settings data.
265 *
266 * @param array $settings
267 *
268 * @return array
269 */
270 public function settings_data( $settings ) {
271 // add settings
272 $settings['post-views-counter'] = [
273 'label' => __( 'Post Views Counter', 'post-views-counter' ),
274 'form' => [
275 'reports' => [
276 'buttons' => false
277 ]
278 ],
279 'option_name' => [
280 'general' => 'post_views_counter_settings_general',
281 'display' => 'post_views_counter_settings_display',
282 'reports' => 'post_views_counter_settings_reports',
283 'integrations' => 'post_views_counter_settings_integrations',
284 'emails' => 'post_views_counter_settings_emails',
285 'other' => 'post_views_counter_settings_other'
286 ],
287 'validate' => [ $this, 'validate_settings' ],
288 'sections' => array_merge(
289 $this->general->get_sections(),
290 $this->display->get_sections(),
291 $this->reports->get_sections(),
292 $this->integrations->get_sections(),
293 $this->emails->get_sections(),
294 $this->other->get_sections()
295 ),
296 'fields' => array_merge(
297 $this->general->get_fields(),
298 $this->display->get_fields(),
299 $this->reports->get_fields(),
300 $this->integrations->get_fields(),
301 $this->emails->get_fields(),
302 $this->other->get_fields()
303 )
304 ];
305
306 // Backward compatibility: ensure legacy settings keys exist for older companion versions.
307 if ( ! isset( $settings['post-views-counter'] ) || ! is_array( $settings['post-views-counter'] ) )
308 $settings['post-views-counter'] = [];
309
310 if ( empty( $settings['post-views-counter']['fields'] ) || ! is_array( $settings['post-views-counter']['fields'] ) )
311 $settings['post-views-counter']['fields'] = [];
312
313 if ( empty( $settings['post-views-counter']['sections'] ) || ! is_array( $settings['post-views-counter']['sections'] ) )
314 $settings['post-views-counter']['sections'] = [];
315
316 if ( ! isset( $settings['post-views-counter']['sections']['post_views_counter_reports_settings'] ) || ! is_array( $settings['post-views-counter']['sections']['post_views_counter_reports_settings'] ) ) {
317 $settings['post-views-counter']['sections']['post_views_counter_reports_settings'] = [
318 'tab' => 'reports',
319 'callback' => null
320 ];
321 }
322
323 // Backward compatibility: allow to hook into old filter name
324 if ( has_filter( 'post_views_counter_settings_data' ) ) {
325 // Add compatibility fields BEFORE deprecated filter
326 $settings = $this->add_compat_fields( $settings );
327
328 $settings = apply_filters_deprecated(
329 'post_views_counter_settings_data',
330 [ $settings ],
331 '1.7.1',
332 'pvc_settings_data'
333 );
334
335 // Copy compatibility changes from 'exclude' field back to 'exclude_groups' (v1.7.0 compatibility)
336 $settings = $this->sync_compat_fields( $settings );
337 }
338
339 return $settings;
340 }
341
342 /**
343 * Normalize conditionally enabled settings when available.
344 *
345 * This runs at priority 10 (after settings_fields_compat at priority 2,
346 * before later hooks at priority 11), ensuring the normalizer executes in
347 * the main settings flow.
348 *
349 * @param array $settings
350 * @return array
351 */
352 public function normalize_pro_settings( $settings ) {
353 // Only run when the related features are available
354 if ( ! class_exists( 'Post_Views_Counter_Pro' ) ) {
355 return $settings;
356 }
357
358 // Normalize fields
359 if ( ! empty( $settings['post-views-counter']['fields'] ) ) {
360 $settings['post-views-counter']['fields'] = $this->normalize_pro_fields( $settings['post-views-counter']['fields'] );
361 }
362
363 return $settings;
364 }
365
366 /**
367 * Add compatibility fields before deprecated filter runs.
368 *
369 * @param array $settings
370 * @return array
371 */
372 private function add_compat_fields( $settings ) {
373 if ( empty( $settings['post-views-counter']['fields'] ) )
374 return $settings;
375
376 $fields =& $settings['post-views-counter']['fields'];
377
378 // Define fallback fields
379 $fallback_fields = [
380 'exclude' => isset( $fields['exclude_groups'] ) ? $fields['exclude_groups'] : [
381 'tab' => 'general',
382 'section' => 'post_views_counter_general_exclusions',
383 'type' => 'custom',
384 'class' => 'pvc-pro',
385 'skip_rendering' => true
386 ],
387 'strict_counts' => [
388 'tab' => 'general',
389 'section' => 'post_views_counter_general_tracking_behavior',
390 'type' => 'boolean',
391 'class' => 'pvc-pro',
392 'skip_rendering' => true
393 ],
394 'amp_support' => [
395 'tab' => 'general',
396 'section' => 'post_views_counter_general_tracking_behavior',
397 'type' => 'boolean',
398 'class' => 'pvc-pro',
399 'skip_rendering' => true
400 ]
401 ];
402
403 // Add missing fields
404 foreach ( $fallback_fields as $field_key => $field_def ) {
405 if ( ! isset( $fields[$field_key] ) ) {
406 $fields[$field_key] = $field_def;
407 }
408 }
409
410 return $settings;
411 }
412
413 /**
414 * Sync changes from compatibility fields back to actual fields.
415 *
416 * @param array $settings
417 * @return array
418 */
419 private function sync_compat_fields( $settings ) {
420 if ( empty( $settings['post-views-counter']['fields'] ) )
421 return $settings;
422
423 $fields =& $settings['post-views-counter']['fields'];
424
425 // If modified 'exclude' field, copy ALL changes to 'exclude_groups'
426 if ( isset( $fields['exclude'] ) && isset( $fields['exclude_groups'] ) ) {
427 // Only copy if 'exclude' was actually modified
428 if ( ! isset( $fields['exclude']['skip_rendering'] ) || ! $fields['exclude']['skip_rendering'] ) {
429 // Copy all changes to exclude_groups
430 foreach ( $fields['exclude'] as $key => $value ) {
431 // Don't overwrite critical properties that define the field structure
432 if ( ! in_array( $key, [ 'tab', 'section', 'title', 'name', 'type' ], true ) ) {
433 $fields['exclude_groups'][$key] = $value;
434 }
435 }
436 }
437
438 // Always mark 'exclude' to not be rendered (it's just an alias)
439 $fields['exclude']['skip_rendering'] = true;
440 }
441
442 return $settings;
443 }
444
445 /**
446 * Normalize conditionally enabled fields when available.
447 *
448 * This method removes badges and disabled states from fields that are
449 * controlled by pro_only metadata, while preserving any
450 * disabled states that are based on runtime availability (like missing
451 * caching plugins, object cache, or integration dependencies).
452 *
453 * Runtime availability is checked for specific fields that have dynamic
454 * requirements (not static unavailable flags).
455 *
456 * @param array $fields
457 * @return array
458 */
459 private function normalize_pro_fields( $fields ) {
460 foreach ( $fields as $field_key => &$field ) {
461 // Skip if field doesn't have pro_only flag
462 if ( empty( $field['pro_only'] ) ) {
463 continue;
464 }
465
466 // Keep email-tab controls disabled in this pass, but remove upgrade
467 // badge classes so the UI reflects their current inactive state.
468 if ( ! empty( $field['tab'] ) && $field['tab'] === 'emails' ) {
469 if ( isset( $field['class'] ) ) {
470 $classes = array_filter( array_map( 'trim', explode( ' ', $field['class'] ) ) );
471 $classes = array_diff( $classes, [ 'pvc-pro', 'pvc-pro-extended' ] );
472 $field['class'] = implode( ' ', $classes );
473 }
474
475 continue;
476 }
477
478 // Check runtime availability for fields with dynamic requirements
479 $is_available = $this->check_field_availability( $field_key, $field );
480
481 // Check if pro_only is an array (for option-level gating like counter_mode['ajax'])
482 if ( is_array( $field['pro_only'] ) ) {
483 // Remove the disabled-state class from the field (preserve other classes)
484 if ( isset( $field['class'] ) ) {
485 $classes = array_filter( array_map( 'trim', explode( ' ', $field['class'] ) ) );
486 $classes = array_diff( $classes, [ 'pvc-pro', 'pvc-pro-extended' ] );
487 $field['class'] = implode( ' ', $classes );
488 }
489
490 // Remove pro_only options from disabled array
491 if ( isset( $field['disabled'] ) && is_array( $field['disabled'] ) ) {
492 $field['disabled'] = array_values( array_diff( $field['disabled'], $field['pro_only'] ) );
493
494 // If disabled array is now empty, set to empty array or false
495 if ( empty( $field['disabled'] ) ) {
496 $field['disabled'] = [];
497 }
498 }
499
500 // Clear skip_saving for option-level pro_only fields
501 if ( isset( $field['skip_saving'] ) ) {
502 $field['skip_saving'] = false;
503 }
504 } else {
505 // Field-level gating - only unlock if available
506 if ( $is_available ) {
507 // Remove disabled-state classes (preserve other classes)
508 if ( isset( $field['class'] ) ) {
509 $classes = array_filter( array_map( 'trim', explode( ' ', $field['class'] ) ) );
510 $classes = array_diff( $classes, [ 'pvc-pro', 'pvc-pro-extended' ] );
511 $field['class'] = implode( ' ', $classes );
512 }
513
514 // Remove disabled flag
515 if ( isset( $field['disabled'] ) ) {
516 $field['disabled'] = false;
517 }
518
519 // Remove skip_saving flag
520 if ( isset( $field['skip_saving'] ) ) {
521 $field['skip_saving'] = false;
522 }
523 } else {
524 // Not available - only remove badge, keep disabled
525 if ( isset( $field['class'] ) ) {
526 $classes = array_filter( array_map( 'trim', explode( ' ', $field['class'] ) ) );
527 $classes = array_diff( $classes, [ 'pvc-pro', 'pvc-pro-extended' ] );
528 $field['class'] = implode( ' ', $classes );
529 }
530 }
531 }
532 }
533
534 return $fields;
535 }
536
537 /**
538 * Check runtime availability for fields with dynamic requirements.
539 *
540 * @param string $field_key
541 * @param array $field
542 * @return bool
543 */
544 private function check_field_availability( $field_key, $field ) {
545 // Default to available (pure licensing gate)
546 $is_available = true;
547
548 // Check specific fields with runtime requirements
549 switch ( $field_key ) {
550 case 'caching_compatibility':
551 // Check if any caching plugins are active
552 $active_plugins = $this->get_active_caching_plugins();
553 $is_available = ! empty( $active_plugins );
554 break;
555
556 case 'object_cache':
557 // Check if persistent object cache is available
558 $is_available = wp_using_ext_object_cache();
559 break;
560
561 // Other fields are purely license-gated (no runtime requirements)
562 default:
563 $is_available = true;
564 break;
565 }
566
567 return $is_available;
568 }
569
570 /**
571 * Backward compatibility for missing fields.
572 *
573 * @param array $settings
574 * @return array
575 */
576 public function settings_fields_compat( $settings ) {
577 if ( empty( $settings['post-views-counter']['fields'] ) )
578 return $settings;
579
580 $fields =& $settings['post-views-counter']['fields'];
581
582 $canonical_fields = array_merge(
583 $this->general->get_fields(),
584 $this->display->get_fields(),
585 $this->reports->get_fields(),
586 $this->other->get_fields(),
587 $this->integrations->get_fields()
588 );
589
590 $compat_fields = [
591 'data_storage',
592 'restrict_edit_views',
593 'post_views_column',
594 'count_time',
595 'caching_compatibility',
596 'counter_mode',
597 'other_count'
598 ];
599
600 $fallback_fields = [];
601
602 foreach ( $compat_fields as $field_key ) {
603 if ( empty( $fields[$field_key] ) || ! is_array( $fields[$field_key] ) ) {
604 if ( isset( $canonical_fields[$field_key] ) && is_array( $canonical_fields[$field_key] ) ) {
605 $fields[$field_key] = $canonical_fields[$field_key];
606 } else if ( isset( $fallback_fields[$field_key] ) ) {
607 $fields[$field_key] = $fallback_fields[$field_key];
608 } else {
609 $fields[$field_key] = [];
610 }
611 }
612 }
613
614 return $settings;
615 }
616
617 /**
618 * Add settings page.
619 *
620 * @param array $pages
621 *
622 * @return array
623 */
624 public function settings_page( $pages ) {
625 // get main instance
626 $pvc = Post_Views_Counter();
627 $menu_position = $pvc->get_menu_position();
628
629 // default page
630 $pages['post-views-counter'] = [
631 'menu_slug' => 'post-views-counter',
632 'page_title' => __( 'Post Views Counter', 'post-views-counter' ),
633 'menu_title' => $menu_position === 'sub' ? __( 'Post Views Counter', 'post-views-counter' ) : __( 'Post Views', 'post-views-counter' ),
634 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ),
635 'callback' => null,
636 'tabs' => [
637 'general' => [
638 'label' => __( 'Counting', 'post-views-counter' ),
639 'option_name' => 'post_views_counter_settings_general',
640 'use_plugin_title' => true
641 ],
642 'display' => [
643 'label' => __( 'Display', 'post-views-counter' ),
644 'option_name' => 'post_views_counter_settings_display',
645 'use_plugin_title' => true
646 ],
647 'reports' => [
648 'label' => __( 'Reports', 'post-views-counter' ),
649 'option_name' => 'post_views_counter_settings_reports',
650 'use_plugin_title' => true
651 ],
652 'integrations' => [
653 'label' => __( 'Integrations', 'post-views-counter' ),
654 'option_name' => 'post_views_counter_settings_integrations',
655 'use_plugin_title' => true
656 ],
657 'emails' => [
658 'label' => __( 'Emails', 'post-views-counter' ),
659 'option_name' => 'post_views_counter_settings_emails',
660 'use_plugin_title' => true
661 ],
662 'other' => [
663 'label' => __( 'Other', 'post-views-counter' ),
664 'option_name' => 'post_views_counter_settings_other',
665 'use_plugin_title' => true
666 ]
667 ]
668 ];
669
670 // update admin title
671 add_filter( 'admin_title', [ $this, 'admin_title' ], 10, 2 );
672
673 // submenu?
674 if ( $menu_position === 'sub' ) {
675 $pages['post-views-counter']['type'] = 'settings_page';
676 // topmenu?
677 } else {
678 // highlight submenus
679 add_filter( 'submenu_file', [ $this, 'submenu_file' ], 10, 2 );
680
681 // add parameters
682 $pages['post-views-counter']['type'] = 'page';
683 $pages['post-views-counter']['icon'] = 'dashicons-chart-bar';
684 $pages['post-views-counter']['position'] = '99.301';
685
686 // add subpages
687 $pages['post-views-counter-general'] = [
688 'menu_slug' => 'post-views-counter',
689 'parent_slug' => 'post-views-counter',
690 'type' => 'subpage',
691 'page_title' => __( 'Counting', 'post-views-counter' ),
692 'menu_title' => __( 'Counting', 'post-views-counter' ),
693 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ),
694 'callback' => null
695 ];
696
697 $pages['post-views-counter-display'] = [
698 'menu_slug' => 'post-views-counter&tab=display',
699 'parent_slug' => 'post-views-counter',
700 'type' => 'subpage',
701 'page_title' => __( 'Display', 'post-views-counter' ),
702 'menu_title' => __( 'Display', 'post-views-counter' ),
703 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ),
704 'callback' => null
705 ];
706
707 $pages['post-views-counter-reports'] = [
708 'menu_slug' => 'post-views-counter&tab=reports',
709 'parent_slug' => 'post-views-counter',
710 'type' => 'subpage',
711 'page_title' => __( 'Reports', 'post-views-counter' ),
712 'menu_title' => __( 'Reports', 'post-views-counter' ),
713 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ),
714 'callback' => null
715 ];
716
717 $pages['post-views-counter-integrations'] = [
718 'menu_slug' => 'post-views-counter&tab=integrations',
719 'parent_slug' => 'post-views-counter',
720 'type' => 'subpage',
721 'page_title' => __( 'Integrations', 'post-views-counter' ),
722 'menu_title' => __( 'Integrations', 'post-views-counter' ),
723 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ),
724 'callback' => null
725 ];
726
727 $pages['post-views-counter-emails'] = [
728 'menu_slug' => 'post-views-counter&tab=emails',
729 'parent_slug' => 'post-views-counter',
730 'type' => 'subpage',
731 'page_title' => __( 'Emails', 'post-views-counter' ),
732 'menu_title' => self::mark_new( __( 'Emails', 'post-views-counter' ) ),
733 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ),
734 'callback' => null
735 ];
736
737 $pages['post-views-counter-other'] = [
738 'menu_slug' => 'post-views-counter&tab=other',
739 'parent_slug' => 'post-views-counter',
740 'type' => 'subpage',
741 'page_title' => __( 'Other', 'post-views-counter' ),
742 'menu_title' => __( 'Other', 'post-views-counter' ),
743 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ),
744 'callback' => null
745 ];
746 }
747
748 // Backward compatibility: ensure legacy reports tab exists for older companion versions.
749 if ( ! isset( $pages['post-views-counter']['tabs'] ) || ! is_array( $pages['post-views-counter']['tabs'] ) )
750 $pages['post-views-counter']['tabs'] = [];
751
752 if ( ! isset( $pages['post-views-counter']['tabs']['reports'] ) || ! is_array( $pages['post-views-counter']['tabs']['reports'] ) ) {
753 $pages['post-views-counter']['tabs']['reports'] = [
754 'label' => __( 'Reports', 'post-views-counter' ),
755 'option_name' => 'post_views_counter_settings_reports',
756 'use_plugin_title' => true,
757 'disabled' => true
758 ];
759 }
760
761 // Backward compatibility: allow to hook into old filter name
762 if ( has_filter( 'post_views_counter_settings_pages' ) ) {
763 $pages = apply_filters_deprecated(
764 'post_views_counter_settings_pages',
765 [ $pages ],
766 '1.7.1',
767 'pvc_settings_pages'
768 );
769 }
770
771 return $pages;
772 }
773
774 /**
775 * Settings page CSS class(es).
776 *
777 * @param array $class
778 * @return array
779 */
780 public function settings_page_class( $class ) {
781 $is_pro = class_exists( 'Post_Views_Counter_Pro' );
782
783 if ( ! $is_pro )
784 $class[] = 'has-sidebar';
785
786 return $class;
787 }
788
789 /**
790 * Highlight submenu items.
791 *
792 * @param string|null $submenu_file
793 * @param string $parent_file
794 *
795 * @return string|null
796 */
797 public function submenu_file( $submenu_file, $parent_file ) {
798 if ( $parent_file === 'post-views-counter' ) {
799 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general';
800
801 if ( $tab !== 'general' )
802 return 'post-views-counter&tab=' . $tab;
803 }
804
805 return $submenu_file;
806 }
807
808 /**
809 * Update admin title.
810 *
811 * @global array $submenu
812 * @global string $pagenow
813 *
814 * @param string $admin_title
815 * @param string $title
816 *
817 * @return string
818 */
819 public function admin_title( $admin_title, $title ) {
820 global $submenu, $pagenow;
821
822 // get main instance
823 $pvc = Post_Views_Counter();
824 $menu_position = $pvc->get_menu_position();
825
826 if ( isset( $_GET['page'] ) && $_GET['page'] === 'post-views-counter' ) {
827 if ( $menu_position === 'sub' && $pagenow === 'options-general.php' ) {
828 // get tab
829 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general';
830
831 // get settings pages
832 $pages = $pvc->settings_api->get_pages();
833
834 if ( array_key_exists( $tab, $pages['post-views-counter']['tabs'] ) ) {
835 // update title
836 $admin_title = preg_replace( '/' . $pages['post-views-counter']['page_title'] . '/', $pages['post-views-counter']['page_title'] . ' - ' . $pages['post-views-counter']['tabs'][$tab]['label'], $admin_title, 1 );
837 }
838 } else if ( $menu_position === 'top' && get_admin_page_parent() === 'post-views-counter' && ! empty( $submenu['post-views-counter'] ) ) {
839 // get tab
840 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general';
841
842 // get settings pages
843 $pages = $pvc->settings_api->get_pages();
844
845 if ( array_key_exists( 'post-views-counter-' . $tab, $pages ) ) {
846 // update title
847 $admin_title = $pages['post-views-counter']['page_title'] . ' - ' . preg_replace( '/' . $title . '/', $pages['post-views-counter-' . $tab]['page_title'], $admin_title, 1 );
848 }
849 }
850 }
851
852 return $admin_title;
853 }
854
855 /**
856 * Validate options.
857 *
858 * @global object $wpdb
859 *
860 * @param array $input
861 *
862 * @return array
863 */
864 public function validate_settings( $input ) {
865 // check capability
866 $capability = apply_filters( 'pvc_settings_capability', 'manage_options' );
867
868 if ( ! current_user_can( $capability ) )
869 return $input;
870
871 global $wpdb;
872
873 // get main instance
874 $pvc = Post_Views_Counter();
875
876 // map exclude array to separate fields before validation (fields post as exclude[groups]/exclude[roles])
877 if ( isset( $input['exclude'] ) && is_array( $input['exclude'] ) ) {
878 if ( isset( $input['exclude']['groups'] ) )
879 $input['exclude_groups'] = $input['exclude']['groups'];
880
881 if ( isset( $input['exclude']['roles'] ) )
882 $input['exclude_roles'] = $input['exclude']['roles'];
883 }
884
885 // map restrict_display array to separate fields before validation (fields post as restrict_display[groups]/restrict_display[roles])
886 if ( isset( $input['restrict_display'] ) && is_array( $input['restrict_display'] ) ) {
887 if ( isset( $input['restrict_display']['groups'] ) )
888 $input['restrict_display_groups'] = $input['restrict_display']['groups'];
889
890 if ( isset( $input['restrict_display']['roles'] ) )
891 $input['restrict_display_roles'] = $input['restrict_display']['roles'];
892 }
893
894 // use internal settings api to validate settings first
895 $input = $pvc->settings_api->validate_settings( $input );
896
897 $option_page = isset( $_POST['option_page'] ) ? sanitize_key( wp_unslash( $_POST['option_page'] ) ) : '';
898
899 if ( $option_page === 'post_views_counter_settings_emails' )
900 $input = $this->preserve_email_runtime_settings( $input, $pvc->options['emails'] );
901
902 // merge exclude fields for backward compatibility
903 if ( isset( $input['exclude_groups'] ) || isset( $input['exclude_roles'] ) ) {
904 $input['exclude'] = [
905 'groups' => isset( $input['exclude_groups'] ) ? $input['exclude_groups'] : [],
906 'roles' => isset( $input['exclude_roles'] ) ? $input['exclude_roles'] : []
907 ];
908 }
909
910 // merge restrict display fields for backward compatibility
911 if ( isset( $input['restrict_display_groups'] ) || isset( $input['restrict_display_roles'] ) ) {
912 $input['restrict_display'] = [
913 'groups' => isset( $input['restrict_display_groups'] ) ? $input['restrict_display_groups'] : [],
914 'roles' => isset( $input['restrict_display_roles'] ) ? $input['restrict_display_roles'] : []
915 ];
916 unset( $input['restrict_display_groups'], $input['restrict_display_roles'] );
917 }
918
919 // handle new provider-based import/analyse
920 if ( isset( $_POST['post_views_counter_import_views'] ) || isset( $_POST['post_views_counter_analyse_views'] ) ) {
921 // make sure we do not change anything in the settings
922 $input = $pvc->options['other'];
923
924 // delegate to import class
925 $result = $pvc->import->handle_manual_action( $_POST );
926
927 if ( isset( $result['message'] ) ) {
928 add_settings_error( 'pvc_' . ( isset( $_POST['post_views_counter_analyse_views'] ) ? 'analyse' : 'import' ), 'pvc_' . ( isset( $_POST['post_views_counter_analyse_views'] ) ? 'analyse' : 'import' ), $result['message'], isset( $result['type'] ) ? $result['type'] : 'updated' );
929 }
930
931 if ( isset( $result['provider_settings'] ) ) {
932 $input['import_provider_settings'] = $result['provider_settings'];
933 }
934
935 return $input;
936 // delete all post views data
937 } elseif ( isset( $_POST['post_views_counter_reset_views'] ) ) {
938 // make sure we do not change anything in the settings
939 $input = $pvc->options['other'];
940
941 if ( $wpdb->query( 'TRUNCATE TABLE ' . $wpdb->prefix . 'post_views' ) )
942 add_settings_error( 'reset_post_views', 'reset_post_views', __( 'All existing data deleted successfully.', 'post-views-counter' ), 'updated' );
943 else
944 add_settings_error( 'reset_post_views', 'reset_post_views', __( 'Error occurred. All existing data were not deleted.', 'post-views-counter' ), 'error' );
945 // save general settings
946 } elseif ( isset( $_POST['save_post_views_counter_settings_general'] ) ) {
947 $input['update_version'] = $pvc->options['general']['update_version'];
948 $input['update_notice'] = $pvc->options['general']['update_notice'];
949 $input['update_delay_date'] = $pvc->options['general']['update_delay_date'];
950 // reset general settings
951 } elseif ( isset( $_POST['reset_post_views_counter_settings_general'] ) ) {
952 $input['update_version'] = $pvc->options['general']['update_version'];
953 $input['update_notice'] = $pvc->options['general']['update_notice'];
954 $input['update_delay_date'] = $pvc->options['general']['update_delay_date'];
955 // save other settings (handle provider inputs)
956 } elseif ( isset( $_POST['save_post_views_counter_settings_other'] ) ) {
957 $input['import_provider_settings'] = $pvc->import->prepare_provider_settings_from_request( $_POST );
958
959 // keep menu position for backward compatibility with older add-ons expecting it under "other" settings
960 if ( ! isset( $input['menu_position'] ) ) {
961 $input['menu_position'] = $pvc->get_menu_position();
962 }
963 // save integrations settings
964 } elseif ( isset( $_POST['save_post_views_counter_settings_integrations'] ) ) {
965 // ensure integrations array exists
966 if ( ! isset( $input['integrations'] ) ) {
967 $input['integrations'] = [];
968 }
969
970 // get all registered integrations (base + any added via pvc_integrations filter)
971 $known_integrations = array_keys( Post_Views_Counter_Integrations::get_registered_integrations() );
972
973 // preserve truly unknown slugs (not in the registered list) from existing settings
974 $existing = $pvc->options['integrations']['integrations'];
975 foreach ( $existing as $slug => $status ) {
976 if ( ! in_array( $slug, $known_integrations, true ) ) {
977 $input['integrations'][$slug] = $status;
978 }
979 }
980
981 // set missing registered integrations to false (unchecked boxes don't submit)
982 foreach ( $known_integrations as $slug ) {
983 if ( ! isset( $input['integrations'][$slug] ) ) {
984 $input['integrations'][$slug] = false;
985 }
986 }
987 }
988
989 return $input;
990 }
991
992 /**
993 * Preserve non-field email settings and runtime status metadata on save.
994 *
995 * @param array $input
996 * @param array $current_settings
997 * @return array
998 */
999 private function preserve_email_runtime_settings( $input, $current_settings ) {
1000 if ( ! is_array( $input ) )
1001 $input = [];
1002
1003 if ( ! is_array( $current_settings ) )
1004 return $input;
1005
1006 $email_fields = $this->emails->get_fields();
1007
1008 if ( ! is_array( $email_fields ) )
1009 return $input;
1010
1011 foreach ( $current_settings as $key => $value ) {
1012 if ( array_key_exists( $key, $email_fields ) )
1013 continue;
1014
1015 $input[$key] = $value;
1016 }
1017
1018 return $input;
1019 }
1020
1021 /**
1022 * Register core PVC database tables for status checking.
1023 *
1024 * @param array $tables Existing table definitions
1025 * @return array
1026 */
1027 public function register_core_tables( $tables ) {
1028 $tables[] = [
1029 'name' => 'post_views',
1030 'label' => 'post_views'
1031 ];
1032
1033 return $tables;
1034 }
1035
1036 /**
1037 * Backward compatibility for section IDs.
1038 *
1039 * @param array $settings
1040 * @return array
1041 */
1042 public function settings_sections_compat( $settings ) {
1043 if ( empty( $settings['post-views-counter']['fields'] ) )
1044 return $settings;
1045
1046 $fields =& $settings['post-views-counter']['fields'];
1047
1048 $compat_sections = [
1049 'technology_count' => [
1050 'legacy' => 'post_views_counter_general_settings',
1051 'current' => 'post_views_counter_general_tracking_targets'
1052 ],
1053 'post_views_column' => [
1054 'legacy' => 'post_views_counter_display_settings',
1055 'current' => 'post_views_counter_display_admin'
1056 ],
1057 'restrict_edit_views' => [
1058 'legacy' => 'post_views_counter_display_settings',
1059 'current' => 'post_views_counter_display_admin'
1060 ],
1061 'menu_position' => [
1062 'legacy' => 'post_views_counter_other_management',
1063 'current' => 'post_views_counter_display_admin'
1064 ]
1065 ];
1066
1067 foreach ( $compat_sections as $field => $map ) {
1068 if ( empty( $fields[$field] ) )
1069 continue;
1070
1071 if ( isset( $fields[$field]['section'] ) && $fields[$field]['section'] === $map['legacy'] )
1072 $fields[$field]['section'] = $map['current'];
1073 }
1074
1075 return $settings;
1076 }
1077
1078 /**
1079 * Mark menu item as new.
1080 *
1081 * @param string $text
1082 * @return string
1083 */
1084 public static function mark_new( $text ) {
1085 return sprintf(
1086 '%s<span class="pvc-admin-menu-new">&nbsp;%s</span>',
1087 $text,
1088 __( 'NEW!', 'post-views-counter' )
1089 );
1090 }
1091 }
1092