PluginProbe
WANotifier for Forms and Actions / 3.2.0
WANotifier for Forms and Actions v3.2.0
3.2.0 3.1.1 3.1.0 3.0.4 2.7.10 2.7.11 2.7.12 2.7.13 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 3.0.0 3.0.1 3.0.2 3.0.3 trunk 0.1.0 0.1.1 1.0.0 1.0.1 All 68 releases
← All changes | includes/classes/class-notifier-backend.php +374 -0 3.0.0 → 3.2.0 View file →
@@ -190,8 +190,13 @@
190 190 'permission_callback' => array( __CLASS__, 'rest_permission_check' ),
191 191 ),
192 192 ) );
193 193
194 + register_rest_route( $namespace, '/admin/search-content', array(
195 + 'methods' => 'GET',
196 + 'callback' => array( __CLASS__, 'rest_search_content' ),
197 + 'permission_callback' => array( __CLASS__, 'rest_permission_check' ),
198 + ) );
194 199 register_rest_route( $namespace, '/admin/sync-triggers', array(
195 200 'methods' => 'POST',
196 201 'callback' => array( __CLASS__, 'rest_sync_triggers' ),
197 202 'permission_callback' => array( __CLASS__, 'rest_permission_check' ),
@@ -307,8 +312,18 @@
307 312 $data = $request->get_json_params();
308 313 $errors = array();
309 314 $allowed = self::get_settings_field_definitions();
310 315
316 + // The chat button can't be enabled without a WhatsApp number (it would silently not render).
317 + $enabling_ctc = isset( $data['ctc_enable'] ) && ( 'yes' === $data['ctc_enable'] || '1' === $data['ctc_enable'] || true === $data['ctc_enable'] );
318 + $ctc_number = isset( $data['ctc_whatsapp_number'] ) ? trim( (string) $data['ctc_whatsapp_number'] ) : get_option( 'notifier_ctc_whatsapp_number', '' );
319 + if ( $enabling_ctc && '' === $ctc_number ) {
320 + return new WP_REST_Response( array(
321 + 'success' => false,
322 + 'errors' => array( __( 'Please enter a WhatsApp number to enable the chat button.', 'notifier' ) ),
323 + ), 200 );
324 + }
325 +
311 326 foreach ( $data as $key => $value ) {
312 327 if ( ! isset( $allowed[ $key ] ) ) {
313 328 continue;
314 329 }
@@ -333,10 +348,16 @@
333 348 break;
334 349 case 'textarea':
335 350 $value = wp_kses_post( trim( $value ) );
336 351 break;
352 + case 'display_rules':
353 + $value = self::sanitize_ctc_display_rules( is_array( $value ) ? $value : array() );
354 + break;
337 355 case 'array':
338 356 $value = is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : array();
357 + if ( ! empty( $def['options'] ) ) {
358 + $value = array_values( array_intersect( $value, array_keys( $def['options'] ) ) );
359 + }
339 360 break;
340 361 default:
341 362 $value = sanitize_text_field( $value );
342 363 break;
@@ -778,17 +799,30 @@
778 799
779 800 public static function get_all_settings() {
780 801 return array(
781 802 'default_country_code' => get_option( 'notifier_default_country_code', '' ),
803 + 'disable_background_processing' => get_option( 'notifier_disable_background_processing', 'no' ),
782 804 'ctc_enable' => get_option( 'notifier_ctc_enable', 'no' ),
783 805 'ctc_whatsapp_number' => get_option( 'notifier_ctc_whatsapp_number', '' ),
784 806 'ctc_greeting_text' => get_option( 'notifier_ctc_greeting_text', '' ),
785 807 'ctc_button_style' => get_option( 'notifier_ctc_button_style', 'default' ),
786 808 'ctc_custom_button_image_url' => get_option( 'notifier_ctc_custom_button_image_url', '' ),
809 + 'ctc_position' => get_option( 'notifier_ctc_position', 'bottom-right' ),
810 + 'ctc_offset_x' => get_option( 'notifier_ctc_offset_x', 20 ),
811 + 'ctc_offset_y' => get_option( 'notifier_ctc_offset_y', 20 ),
812 + 'ctc_mobile_position_enable' => get_option( 'notifier_ctc_mobile_position_enable', 'no' ),
813 + 'ctc_mobile_position' => get_option( 'notifier_ctc_mobile_position', 'bottom-right' ),
814 + 'ctc_mobile_offset_x' => get_option( 'notifier_ctc_mobile_offset_x', 20 ),
815 + 'ctc_mobile_offset_y' => get_option( 'notifier_ctc_mobile_offset_y', 20 ),
816 + 'ctc_display_rules' => self::get_ctc_display_rules_for_ui(),
817 + 'ctc_devices' => get_option( 'notifier_ctc_devices', array_keys( self::get_ctc_device_options() ) ),
787 818 'enable_activity_log' => get_option( 'notifier_enable_activity_log', 'no' ),
788 819 'wp_enabled_merge_tags' => get_option( 'notifier_wp_enabled_merge_tags', array() ),
789 820 'wp_enabled_recipient_fields' => get_option( 'notifier_wp_enabled_recipient_fields', array() ),
790 821 'button_styles' => self::get_button_styles(),
822 + 'ctc_position_options' => self::get_ctc_position_options(),
823 + 'ctc_location_options' => self::get_ctc_location_options(),
824 + 'ctc_device_options' => self::get_ctc_device_options(),
791 825 );
792 826 }
793 827
794 828 private static function get_settings_field_definitions() {
@@ -793,13 +827,23 @@
793 827
794 828 private static function get_settings_field_definitions() {
795 829 return array(
796 830 'default_country_code' => array( 'type' => 'text' ),
831 + 'disable_background_processing' => array( 'type' => 'checkbox' ),
797 832 'ctc_enable' => array( 'type' => 'checkbox' ),
798 833 'ctc_whatsapp_number' => array( 'type' => 'text' ),
799 834 'ctc_greeting_text' => array( 'type' => 'text' ),
800 835 'ctc_button_style' => array( 'type' => 'select', 'options' => self::get_button_styles(), 'default' => 'default' ),
801 836 'ctc_custom_button_image_url' => array( 'type' => 'text' ),
837 + 'ctc_position' => array( 'type' => 'select', 'options' => self::get_ctc_position_options(), 'default' => 'bottom-right' ),
838 + 'ctc_offset_x' => array( 'type' => 'number', 'min' => 0 ),
839 + 'ctc_offset_y' => array( 'type' => 'number', 'min' => 0 ),
840 + 'ctc_mobile_position_enable' => array( 'type' => 'checkbox' ),
841 + 'ctc_mobile_position' => array( 'type' => 'select', 'options' => self::get_ctc_position_options(), 'default' => 'bottom-right' ),
842 + 'ctc_mobile_offset_x' => array( 'type' => 'number', 'min' => 0 ),
843 + 'ctc_mobile_offset_y' => array( 'type' => 'number', 'min' => 0 ),
844 + 'ctc_display_rules' => array( 'type' => 'display_rules' ),
845 + 'ctc_devices' => array( 'type' => 'array', 'options' => self::get_ctc_device_options() ),
802 846 'enable_activity_log' => array( 'type' => 'checkbox' ),
803 847 'wp_enabled_merge_tags' => array( 'type' => 'array' ),
804 848 'wp_enabled_recipient_fields' => array( 'type' => 'array' ),
805 849 );
@@ -812,8 +856,338 @@
812 856 'btn-style-2' => 'Style 2',
813 857 'btn-style-3' => 'Style 3',
814 858 'btn-style-4' => 'Style 4',
815 859 'btn-custom-image' => 'Add your own image',
860 + );
861 + }
862 +
863 + public static function get_ctc_position_options() {
864 + return array(
865 + 'bottom-right' => 'Bottom Right',
866 + 'bottom-left' => 'Bottom Left',
867 + 'top-right' => 'Top Right',
868 + 'top-left' => 'Top Left',
869 + );
870 + }
871 +
872 + // ----------------------------------------
873 + // Chat button display rules
874 + // ----------------------------------------
875 +
876 + /**
877 + * Grouped location options for the display / exclude rule dropdowns (react-select format).
878 + *
879 + * Rule keys:
880 + * basic-global | basic-singulars | basic-archives
881 + * special-front | special-blog | special-404 | special-search | special-date | special-author
882 + * {post_type}|all | {post_type}|archive | {post_type}|tax|{taxonomy}
883 + * wc-shop | wc-cart | wc-checkout | wc-account | wc-order-received
884 + * specifics (with items: post-{id}, tax-{term_id}, tax-{term_id}-posts)
885 + */
886 + public static function get_ctc_location_options() {
887 + $groups = array(
888 + array(
889 + 'label' => 'Basic',
890 + 'options' => array(
891 + array('value' => 'basic-global', 'label' => 'Entire Website'),
892 + array('value' => 'basic-singulars', 'label' => 'All Singulars'),
893 + array('value' => 'basic-archives', 'label' => 'All Archives'),
894 + ),
895 + ),
896 + array(
897 + 'label' => 'Special Pages',
898 + 'options' => array(
899 + array('value' => 'special-front', 'label' => 'Front Page'),
900 + array('value' => 'special-blog', 'label' => 'Blog / Posts Page'),
901 + array('value' => 'special-404', 'label' => '404 Page'),
902 + array('value' => 'special-search', 'label' => 'Search Results'),
903 + array('value' => 'special-date', 'label' => 'Date Archive'),
904 + array('value' => 'special-author', 'label' => 'Author Archive'),
905 + ),
906 + ),
907 + );
908 +
909 + $has_woo = class_exists('WooCommerce');
910 +
911 + foreach (self::get_ctc_rule_post_types() as $post_type) {
912 + $options = array(
913 + array('value' => $post_type->name . '|all', 'label' => 'All ' . $post_type->labels->name),
914 + );
915 +
916 + // The product archive is the WooCommerce shop page, listed under WooCommerce below.
917 + if ($post_type->has_archive && ! ($has_woo && 'product' === $post_type->name)) {
918 + $options[] = array('value' => $post_type->name . '|archive', 'label' => $post_type->labels->name . ' Archive');
919 + }
920 +
921 + foreach (get_object_taxonomies($post_type->name, 'objects') as $taxonomy) {
922 + if (! self::is_ctc_rule_taxonomy($taxonomy)) {
923 + continue;
924 + }
925 + $options[] = array(
926 + 'value' => $post_type->name . '|tax|' . $taxonomy->name,
927 + 'label' => 'All ' . $taxonomy->labels->singular_name . ' Archives',
928 + );
929 + }
930 +
931 + $groups[] = array('label' => $post_type->labels->name, 'options' => $options);
932 + }
933 +
934 + if ($has_woo) {
935 + $groups[] = array(
936 + 'label' => 'WooCommerce',
937 + 'options' => array(
938 + array('value' => 'wc-shop', 'label' => 'Shop Page'),
939 + array('value' => 'wc-cart', 'label' => 'Cart Page'),
940 + array('value' => 'wc-checkout', 'label' => 'Checkout Page'),
941 + array('value' => 'wc-account', 'label' => 'My Account Page'),
942 + array('value' => 'wc-order-received', 'label' => 'Order Received (Thank You) Page'),
943 + ),
944 + );
945 + }
946 +
947 + $groups[] = array(
948 + 'label' => 'Specific Target',
949 + 'options' => array(
950 + array('value' => 'specifics', 'label' => 'Specific Pages / Posts / Taxonomies'),
951 + ),
952 + );
953 +
954 + return $groups;
955 + }
956 +
957 + /**
958 + * Flat list of valid rule keys.
959 + */
960 + public static function get_ctc_location_keys() {
961 + $keys = array();
962 + foreach (self::get_ctc_location_options() as $group) {
963 + foreach ($group['options'] as $option) {
964 + $keys[] = $option['value'];
965 + }
966 + }
967 + return $keys;
968 + }
969 +
970 + /**
971 + * Public, frontend-viewable post types that rules can target.
972 + *
973 + * @return WP_Post_Type[]
974 + */
975 + public static function get_ctc_rule_post_types() {
976 + $post_types = array();
977 + foreach (get_post_types(array('public' => true), 'objects') as $post_type) {
978 + if ('attachment' === $post_type->name || ! is_post_type_viewable($post_type)) {
979 + continue;
980 + }
981 + $post_types[] = $post_type;
982 + }
983 + return $post_types;
984 + }
985 +
986 + /**
987 + * Whether a taxonomy has frontend archives worth targeting.
988 + *
989 + * @param WP_Taxonomy $taxonomy
990 + */
991 + public static function is_ctc_rule_taxonomy($taxonomy) {
992 + // Post formats and WooCommerce shipping classes are queryable but have no real archive pages.
993 + if (in_array($taxonomy->name, array('post_format', 'product_shipping_class'), true)) {
994 + return false;
995 + }
996 + return is_taxonomy_viewable($taxonomy);
997 + }
998 +
999 + public static function get_default_ctc_display_rules() {
1000 + return array(
1001 + 'display' => array(array('rule' => 'basic-global', 'specifics' => array())),
1002 + 'exclude' => array(),
1003 + );
1004 + }
1005 +
1006 + /**
1007 + * Validate submitted rules: unknown rule keys and malformed specific items are dropped,
1008 + * as are "Specific" rows with nothing selected.
1009 + */
1010 + public static function sanitize_ctc_display_rules($value) {
1011 + $valid = self::get_ctc_location_keys();
1012 + $clean = array('display' => array(), 'exclude' => array());
1013 +
1014 + foreach (array('display', 'exclude') as $list) {
1015 + if (empty($value[$list]) || ! is_array($value[$list])) {
1016 + continue;
1017 + }
1018 + foreach ($value[$list] as $row) {
1019 + $rule = isset($row['rule']) ? sanitize_text_field((string) $row['rule']) : '';
1020 + if (! in_array($rule, $valid, true)) {
1021 + continue;
1022 + }
1023 +
1024 + $specifics = array();
1025 + if ('specifics' === $rule) {
1026 + foreach ((array) ($row['specifics'] ?? array()) as $item) {
1027 + $item = is_array($item) ? ($item['value'] ?? '') : $item;
1028 + if (preg_match('/^(post|tax)-\d+(-posts)?$/', (string) $item)) {
1029 + $specifics[] = (string) $item;
1030 + }
1031 + }
1032 + if (empty($specifics)) {
1033 + continue;
1034 + }
1035 + }
1036 +
1037 + $clean[$list][] = array('rule' => $rule, 'specifics' => array_values(array_unique($specifics)));
1038 + }
1039 + }
1040 +
1041 + return $clean;
1042 + }
1043 +
1044 + /**
1045 + * Saved rules (raw keys), falling back to "Entire Website".
1046 + */
1047 + public static function get_ctc_display_rules() {
1048 + $rules = get_option('notifier_ctc_display_rules', null);
1049 + if (! is_array($rules) || ! isset($rules['display'])) {
1050 + return self::get_default_ctc_display_rules();
1051 + }
1052 + return wp_parse_args($rules, array('display' => array(), 'exclude' => array()));
1053 + }
1054 +
1055 + /**
1056 + * Saved rules with the specific items expanded to {value,label} options for the settings UI.
1057 + * Items whose post/term no longer exists are left out.
1058 + */
1059 + public static function get_ctc_display_rules_for_ui() {
1060 + $rules = self::get_ctc_display_rules();
1061 + foreach ($rules as &$rows) {
1062 + foreach ($rows as &$row) {
1063 + $row['specifics'] = array_values(array_filter(array_map(array(__CLASS__, 'get_ctc_specific_option'), (array) $row['specifics'])));
1064 + }
1065 + unset($row);
1066 + }
1067 + unset($rows);
1068 + return $rules;
1069 + }
1070 +
1071 + /**
1072 + * Build the {value,label} option for a specific item key (post-{id}, tax-{id}, tax-{id}-posts).
1073 + *
1074 + * @return array|null Null if the post/term no longer exists.
1075 + */
1076 + public static function get_ctc_specific_option($key) {
1077 + if (! preg_match('/^(post|tax)-(\d+)(-posts)?$/', (string) $key, $m)) {
1078 + return null;
1079 + }
1080 + $id = (int) $m[2];
1081 +
1082 + if ('post' === $m[1]) {
1083 + $post = get_post($id);
1084 + if (! $post || 'trash' === $post->post_status) {
1085 + return null;
1086 + }
1087 + $type = get_post_type_object($post->post_type);
1088 + // Plain text: React renders labels as text nodes, so entities from wptexturize/kses would show literally.
1089 + $title = trim(html_entity_decode(wp_strip_all_tags($post->post_title), ENT_QUOTES, 'UTF-8'));
1090 + return array(
1091 + 'value' => $key,
1092 + 'label' => ('' !== $title ? $title : '#' . $id) . ' (' . ($type ? $type->labels->singular_name : $post->post_type) . ')',
1093 + );
1094 + }
1095 +
1096 + $term = get_term($id);
1097 + if (! $term || is_wp_error($term)) {
1098 + return null;
1099 + }
1100 + $taxonomy = get_taxonomy($term->taxonomy);
1101 + $tax_label = $taxonomy ? $taxonomy->labels->singular_name : $term->taxonomy;
1102 + $term_name = html_entity_decode($term->name, ENT_QUOTES, 'UTF-8');
1103 +
1104 + return array(
1105 + 'value' => $key,
1106 + 'label' => ! empty($m[3])
1107 + ? sprintf('%s (%s) – all items in it', $term_name, $tax_label)
1108 + : sprintf('%s (%s) – archive page', $term_name, $tax_label),
1109 + );
1110 + }
1111 +
1112 + /**
1113 + * Search posts (all public post types) and terms (all public taxonomies) for the
1114 + * "Specific Pages / Posts / Taxonomies" picker. Returns react-select option groups.
1115 + */
1116 + public static function rest_search_content(WP_REST_Request $request) {
1117 + $query = trim(sanitize_text_field((string) $request->get_param('q')));
1118 + $groups = array();
1119 +
1120 + foreach (self::get_ctc_rule_post_types() as $post_type) {
1121 + $args = array(
1122 + 'post_type' => $post_type->name,
1123 + 'post_status' => 'publish',
1124 + 'posts_per_page' => 10,
1125 + 'orderby' => '' !== $query ? 'relevance' : 'title',
1126 + 'order' => 'ASC',
1127 + 'fields' => 'ids',
1128 + 'no_found_rows' => true,
1129 + );
1130 + if ('' !== $query) {
1131 + $args['s'] = $query;
1132 + }
1133 + $ids = get_posts($args);
1134 +
1135 + // Allow pasting an ID directly (published items only, like the search results).
1136 + if (is_numeric($query) && get_post_type((int) $query) === $post_type->name && 'publish' === get_post_status((int) $query)) {
1137 + array_unshift($ids, (int) $query);
1138 + }
1139 +
1140 + $options = array();
1141 + foreach (array_unique(array_map('intval', $ids)) as $id) {
1142 + $option = self::get_ctc_specific_option('post-' . $id);
1143 + if ($option) {
1144 + $options[] = $option;
1145 + }
1146 + }
1147 + if (! empty($options)) {
1148 + $groups[] = array('label' => $post_type->labels->name, 'options' => $options);
1149 + }
1150 + }
1151 +
1152 + foreach (get_taxonomies(array('public' => true), 'objects') as $taxonomy) {
1153 + if (! self::is_ctc_rule_taxonomy($taxonomy)) {
1154 + continue;
1155 + }
1156 + $args = array(
1157 + 'taxonomy' => $taxonomy->name,
1158 + 'hide_empty' => false,
1159 + 'number' => 10,
1160 + 'orderby' => 'name',
1161 + );
1162 + if ('' !== $query) {
1163 + $args['search'] = $query;
1164 + }
1165 + $terms = get_terms($args);
1166 + if (is_wp_error($terms) || empty($terms)) {
1167 + continue;
1168 + }
1169 +
1170 + $options = array();
1171 + foreach ($terms as $term) {
1172 + foreach (array('tax-' . $term->term_id, 'tax-' . $term->term_id . '-posts') as $key) {
1173 + $option = self::get_ctc_specific_option($key);
1174 + if ($option) {
1175 + $options[] = $option;
1176 + }
1177 + }
1178 + }
1179 + $groups[] = array('label' => $taxonomy->labels->name, 'options' => $options);
1180 + }
1181 +
1182 + return new WP_REST_Response($groups, 200);
1183 + }
1184 +
1185 + public static function get_ctc_device_options() {
1186 + return array(
1187 + 'desktop' => 'Desktop',
1188 + 'tablet' => 'Tablet',
1189 + 'mobile' => 'Mobile',
816 1190 );
817 1191 }
818 1192
819 1193 // ========================================