PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / trunk
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar vtrunk
3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 All 156 releases
← All changes | includes/Admin/Settings.php +213 -0 3.2.9trunk View file →
@@ -77,10 +77,23 @@
77 77 * @return void
78 78 */
79 79 function get_form_data() {
80 80 $data = NotificationX::get_instance()->normalize( $this->settings_form() );
81 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
81 82 $settings = apply_filters('nx_settings_page_settings', $this->get( 'settings', [] ));
82 83
84 + /*
85 + * `/builder` resolves `read_notificationx` -- the lowest capability the
86 + * product defines -- and this blob is handed straight to it. The
87 + * `settingsRedirect` flag below only tells the admin app not to render
88 + * the settings screen; it is a client-side routing hint, not a boundary.
89 + * Without this, a view-only delegate received every API key, client
90 + * secret and OAuth token stored on the site.
91 + */
92 + if ( ! current_user_can( 'edit_notificationx_settings' ) ) {
93 + $settings = self::redact_secret_settings( $settings );
94 + }
95 +
83 96 $data['current_page'] = 'settings';
84 97 $data['rest'] = REST::get_instance()->rest_data();
85 98 $data['savedValues'] = $settings;
86 99 $data['values'] = $settings;
@@ -101,8 +114,9 @@
101 114 }
102 115
103 116
104 117 public function settings_form( $nx_id = 0 ) {
118 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
105 119 do_action( 'nx_before_settings_fields' );
106 120 $wp_roles = GlobalFields::get_instance()->normalize_fields( $this->get_roles() );
107 121 $site_name = get_bloginfo( 'name' );
108 122 $settings = [
@@ -126,9 +140,11 @@
126 140 'show' => true,
127 141 'label' => __( 'Save Settings', 'notificationx' ),
128 142 'class' => 'save-settings',
129 143 ],
144 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
130 145 'tabs' => apply_filters('nx_settings_tab', [
146 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
131 147 'tab-general' => apply_filters('nx_settings_tab_general', [
132 148 'id' => 'tab-general',
133 149 'label' => __( 'General', 'notificationx' ),
134 150 'classes' => 'tab-general',
@@ -156,8 +172,9 @@
156 172 ],
157 173 ],
158 174 ]
159 175 ),
176 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
160 177 'advanced-settings-tab' => apply_filters('nx_settings_tab_advanced', [
161 178 'id' => 'tab-advanced-settings',
162 179 'label' => __( 'Advanced Settings', 'notificationx' ),
163 180 'classes' => 'tab-advanced-settings',
@@ -280,8 +297,9 @@
280 297 ),
281 298 ],
282 299 ]
283 300 ),
301 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
284 302 'email-analytics-reporting' => apply_filters('nx_settings_tab_email_analytics', [
285 303 'label' => __( 'Analytics & Reporting', 'notificationx' ),
286 304 'id' => 'email-analytics-reporting',
287 305 'classes' => 'tab-advanced-settings',
@@ -333,8 +351,9 @@
333 351 'rules' => Rules::is( 'enable_analytics', true ),
334 352 ),
335 353 ),
336 354 ),
355 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
337 356 'entries_mail_receiver' => apply_filters( 'nx_settings_entries_mail_receiver', array(
338 357 'name' => 'entries_mail_receiver',
339 358 'priority' => 15,
340 359 'type' => 'section',
@@ -359,8 +378,9 @@
359 378 'entries_mail_subject' => array(
360 379 'name' => 'entries_mail_subject',
361 380 'type' => 'text',
362 381 'label' => __( 'Email Subject', 'notificationx' ),
382 + /* translators: %s: site name */
363 383 'default' => sprintf( __( 'New Entry Received on "%s"', 'notificationx' ), $site_name ),
364 384 'priority' => 2,
365 385 'rules' => Rules::is( 'enable_entries_mail', true ),
366 386 ),
@@ -459,8 +479,9 @@
459 479 'reporting_subject' => array(
460 480 'name' => 'reporting_subject',
461 481 'type' => 'text',
462 482 'label' => __( 'Reporting Email Subject', 'notificationx' ),
483 + /* translators: %s: site name */
463 484 'default' => sprintf( __( 'Weekly Engagement Summary of “%s”', 'notificationx' ), $site_name ),
464 485 'priority' => 4,
465 486 'disable' => true,
466 487 'rules' => Rules::is( 'disable_reporting', false ),
@@ -493,8 +514,9 @@
493 514 ),
494 515 ],
495 516 ]
496 517 ),
518 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
497 519 'entries' => apply_filters('nx_settings_tab_entries', [
498 520 'label' => __( 'Entries', 'notificationx' ),
499 521 'id' => 'entries',
500 522 'classes' => 'tab-advanced-settings',
@@ -508,8 +530,9 @@
508 530 ),
509 531 ],
510 532 ]
511 533 ),
534 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
512 535 'cache_settings_tab' => apply_filters('nx_settings_tab_cache', [
513 536 'id' => 'tab-cache-settings',
514 537 'label' => __( 'Cache Settings', 'notificationx' ),
515 538 'priority' => 40,
@@ -548,8 +571,9 @@
548 571 ),
549 572 ],
550 573 ]
551 574 ),
575 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
552 576 'tab-miscellaneous-settings' => apply_filters('nx_settings_tab_miscellaneous', [
553 577 'id' => 'tab-miscellaneous-settings',
554 578 'label' => __( 'Miscellaneous', 'notificationx' ),
555 579 'priority' => 50,
@@ -611,8 +635,9 @@
611 635 ),
612 636 );
613 637 }
614 638
639 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
615 640 $settings = apply_filters( 'nx_settings_configs', $settings );
616 641 return $settings;
617 642 }
618 643
@@ -656,11 +681,17 @@
656 681 $remove_before_save = [
657 682 'empty',
658 683 ];
659 684
685 + // Must run before the role map is derived below, or the posted values
686 + // would still reach `nx_settings_saved` and be written to the roles.
687 + $settings = $this->preserve_role_settings( $settings );
688 + $settings = $this->preserve_secret_settings( $settings );
689 +
660 690 $roles = $this->get_selected_roles( $settings );
661 691 $settings = array_merge( $settings, $roles );
662 692
693 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
663 694 $settings = apply_filters( 'nx_settings', $settings );
664 695
665 696 foreach ( $remove_before_save as $key ) {
666 697 if ( isset( $settings[ $key ] ) ) {
@@ -666,19 +697,200 @@
666 697 if ( isset( $settings[ $key ] ) ) {
667 698 unset( $settings[ $key ] );
668 699 }
669 700 }
701 + $settings = $this->preserve_protected_settings( $settings );
702 +
670 703 // need this to ensure saved value don't return empty array instead of object.
671 704 if ( ! empty( $settings['delete_settings'] ) ) {
672 705 $settings = [ 'empty' => true ];
706 + // The reset discards everything preserved above, so re-apply the
707 + // role gate: a settings administrator must not be able to rewrite
708 + // role assignments by routing through a settings reset.
709 + $settings = $this->preserve_role_settings( $settings );
673 710 }
674 711
675 712 $this->set( 'settings', $settings );
676 713 delete_transient( 'nx_get_field_names' );
714 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
677 715 do_action( 'nx_settings_saved', $settings );
678 716 return true;
679 717 }
680 718
719 + /**
720 + * Force server-owned settings back to their stored values.
721 + *
722 + * `save_settings()` replaces the whole blob with the posted one, so every key
723 + * the admin app is holding wins -- right for fields a merchant edits, wrong
724 + * for state only the server writes. OAuth tokens are the case that bites:
725 + * they live inside `settings`, so they are handed to the admin app on page
726 + * load and posted straight back. Disconnecting an account and then saving
727 + * from a tab opened *before* the disconnect wrote the revoked token back and
728 + * the integration silently kept working -- with Google Analytics that showed
729 + * up as live viewer counts on a supposedly disconnected site.
730 + *
731 + * Keys listed here can only be changed by the code that owns them.
732 + *
733 + * @param array $settings Incoming settings.
734 + * @return array
735 + */
736 + protected function preserve_protected_settings( $settings ) {
737 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
738 + $protected = apply_filters( 'nx_protected_settings', [
739 + 'nx_pa_settings', // Google Analytics OAuth token.
740 + ] );
741 +
742 + // `get()` returns false for a missing key, and false is also what a
743 + // disconnect stores -- so only a sentinel separates "stored as false"
744 + // from "never stored", and the two need different handling.
745 + $missing = new \stdClass();
746 +
747 + foreach ( (array) $protected as $key ) {
748 + if ( ! is_string( $key ) || '' === $key ) {
749 + continue;
750 + }
751 + $stored = $this->get( "settings.{$key}", $missing );
752 + if ( $missing === $stored ) {
753 + unset( $settings[ $key ] );
754 + continue;
755 + }
756 + $settings[ $key ] = $stored;
757 + }
758 +
759 + return $settings;
760 + }
761 +
762 + /**
763 + * Settings keys holding credentials rather than configuration.
764 + *
765 + * These are values a site owner pastes in once and which grant access to a
766 + * third-party account. They must never reach a client that is not entitled
767 + * to edit settings, and must never be written into an export file.
768 + *
769 + * Pro registers its own integrations, so the list is filterable; keep the
770 + * free baseline broad rather than minimal, because a key omitted here is a
771 + * key that leaks.
772 + *
773 + * @return array
774 + */
775 + public static function get_secret_settings_keys() {
776 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
777 + return (array) apply_filters( 'nx_secret_settings', [
778 + 'nx_pa_settings', // Google Analytics OAuth access + refresh token.
779 + 'token_info',
780 + 'ga_client_secret',
781 + 'yt_client_secret',
782 + 'openai_access_token',
783 + 'mailchimp_api_key',
784 + 'activecampaign_api_key',
785 + 'brevo_api_key',
786 + 'convertkit_api_key',
787 + 'convertkit_api_secret',
788 + 'google_review_api_key',
789 + 'google_youtube_api_key',
790 + 'zapier_api_key',
791 + 'ifttt_api_key',
792 + 'envato_token',
793 + 'gmap_token',
794 + ] );
795 + }
796 +
797 + /**
798 + * Strip credentials out of a settings blob.
799 + *
800 + * Keys are removed outright rather than blanked. A blanked value is
801 + * indistinguishable from "the user cleared this field" once it is posted
802 + * back, which would silently destroy working integrations; an absent key is
803 + * unambiguous and is restored by `preserve_secret_settings()` on save.
804 + *
805 + * @param array $settings
806 + * @return array
807 + */
808 + public static function redact_secret_settings( $settings ) {
809 + if ( ! is_array( $settings ) ) {
810 + return $settings;
811 + }
812 + foreach ( self::get_secret_settings_keys() as $key ) {
813 + unset( $settings[ $key ] );
814 + }
815 + return $settings;
816 + }
817 +
818 + /**
819 + * Carry stored credentials across a save that does not carry them.
820 + *
821 + * Counterpart to `redact_secret_settings()`. An export has its credentials
822 + * stripped, so importing one would otherwise blank every integration on the
823 + * target site. Absent key means "unchanged"; a key that *is* present wins,
824 + * so a settings administrator editing an API key in the UI still works, and
825 + * deliberately clearing a field still clears it.
826 + *
827 + * Unlike `preserve_protected_settings()` these values are user-owned, so
828 + * they must stay editable -- that is why this cannot simply force the
829 + * stored value back.
830 + *
831 + * @param array $settings Incoming settings.
832 + * @return array
833 + */
834 + protected function preserve_secret_settings( $settings ) {
835 + if ( ! is_array( $settings ) ) {
836 + return $settings;
837 + }
838 +
839 + $missing = new \stdClass();
840 +
841 + foreach ( self::get_secret_settings_keys() as $key ) {
842 + if ( array_key_exists( $key, $settings ) ) {
843 + continue;
844 + }
845 + $stored = $this->get( "settings.{$key}", $missing );
846 + if ( $missing !== $stored ) {
847 + $settings[ $key ] = $stored;
848 + }
849 + }
850 +
851 + return $settings;
852 + }
853 +
854 + /**
855 + * Keep role assignments out of reach of a plain settings administrator.
856 + *
857 + * The role selectors grant and revoke capabilities across every role on the
858 + * site, which is authority well beyond "may edit NotificationX settings".
859 + * Pro hides these fields from the settings form unless the user can
860 + * `delete_users` (RoleManagement::tab_advanced), but that is a filter on the
861 + * form schema -- it never runs on save, so posting the keys directly to
862 + * `/settings` bypassed it entirely.
863 + *
864 + * @param array $settings Incoming settings.
865 + * @return array
866 + */
867 + protected function preserve_role_settings( $settings ) {
868 + if ( ! is_array( $settings ) || current_user_can( 'delete_users' ) ) {
869 + return $settings;
870 + }
871 +
872 + $role_keys = [
873 + 'notification_view_roles',
874 + 'notification_roles',
875 + 'settings_roles',
876 + 'analytics_roles',
877 + ];
878 +
879 + $missing = new \stdClass();
880 +
881 + foreach ( $role_keys as $key ) {
882 + $stored = $this->get( "settings.{$key}", $missing );
883 + if ( $missing === $stored ) {
884 + unset( $settings[ $key ] );
885 + continue;
886 + }
887 + $settings[ $key ] = $stored;
888 + }
889 +
890 + return $settings;
891 + }
892 +
681 893 public function get_role_map( $settings = [] ) {
682 894 if ( empty( $settings ) || count( $settings ) == 1 ) {
683 895 $settings = $this->get_selected_roles();
684 896 }
@@ -720,8 +932,9 @@
720 932 if ( ! is_array( $analytics_roles ) ) {
721 933 $analytics_roles = [ $analytics_roles ];
722 934 }
723 935
936 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
724 937 return apply_filters('nx_role_management', [
725 938 'notification_view_roles' => array_values( array_unique( array_merge( [ 'administrator' ], $notification_view_roles ) ) ),
726 939 'notification_roles' => array_values( array_unique( array_merge( [ 'administrator' ], $notification_roles ) ) ),
727 940 'settings_roles' => array_values( array_unique( array_merge( [ 'administrator' ], $settings_roles ) ) ),