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 +372 -0 3.0.4 → 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;
@@ -784,12 +805,24 @@
784 805 'ctc_whatsapp_number' => get_option( 'notifier_ctc_whatsapp_number', '' ),
785 806 'ctc_greeting_text' => get_option( 'notifier_ctc_greeting_text', '' ),
786 807 'ctc_button_style' => get_option( 'notifier_ctc_button_style', 'default' ),
787 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() ) ),
788 818 'enable_activity_log' => get_option( 'notifier_enable_activity_log', 'no' ),
789 819 'wp_enabled_merge_tags' => get_option( 'notifier_wp_enabled_merge_tags', array() ),
790 820 'wp_enabled_recipient_fields' => get_option( 'notifier_wp_enabled_recipient_fields', array() ),
791 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(),
792 825 );
793 826 }
794 827
795 828 private static function get_settings_field_definitions() {
@@ -800,8 +833,17 @@
800 833 'ctc_whatsapp_number' => array( 'type' => 'text' ),
801 834 'ctc_greeting_text' => array( 'type' => 'text' ),
802 835 'ctc_button_style' => array( 'type' => 'select', 'options' => self::get_button_styles(), 'default' => 'default' ),
803 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() ),
804 846 'enable_activity_log' => array( 'type' => 'checkbox' ),
805 847 'wp_enabled_merge_tags' => array( 'type' => 'array' ),
806 848 'wp_enabled_recipient_fields' => array( 'type' => 'array' ),
807 849 );
@@ -814,8 +856,338 @@
814 856 'btn-style-2' => 'Style 2',
815 857 'btn-style-3' => 'Style 3',
816 858 'btn-style-4' => 'Style 4',
817 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',
818 1190 );
819 1191 }
820 1192
821 1193 // ========================================