PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / 3.3.3
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar v3.3.3
3.3.3 3.3.2 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 All 158 releases
notificationx / includes / Core / Helper.php

Helper.php in NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar 3.3.3, at includes/Core/Helper.php

1,425 lines 53.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace NotificationX\Core;
4
5 use NotificationX\Extensions\GlobalFields;
6 use NotificationX\Types\TypeFactory;
7 use NotificationX\Admin\Settings;
8
9 /**
10 * This class will provide all kind of helper methods.
11 */
12 class Helper {
13 /**
14 * Get all post types
15 *
16 * @param array $exclude
17 * @return array
18 */
19 public static function post_types($exclude = array()) {
20 $post_types = get_post_types(array(
21 'public' => true,
22 'show_ui' => true
23 ), 'objects');
24
25 unset($post_types['attachment']);
26
27 if (count($exclude)) {
28 foreach ($exclude as $type) {
29 if (isset($post_types[$type])) {
30 unset($post_types[$type]);
31 }
32 }
33 }
34
35 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
36 return apply_filters('nx_post_types', $post_types);
37 }
38
39 /**
40 * Get all taxonomies
41 *
42 * @param string $post_type
43 * @param array $exclude
44 * @return array
45 */
46 public static function taxonomies($post_type = '', $exclude = array()) {
47 if (empty($post_type)) {
48 $taxonomies = get_taxonomies(
49 array(
50 'public' => true,
51 '_builtin' => false
52 ),
53 'objects'
54 );
55 } else {
56 $taxonomies = get_object_taxonomies($post_type, 'objects');
57 }
58
59 $data = array();
60 if (is_array($taxonomies)) {
61 foreach ($taxonomies as $tax_slug => $tax) {
62 if (!$tax->public || !$tax->show_ui) {
63 continue;
64 }
65 if (in_array($tax_slug, $exclude)) {
66 continue;
67 }
68 $data[$tax_slug] = $tax;
69 }
70 }
71 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
72 return apply_filters('nx_loop_taxonomies', $data, $taxonomies, $post_type);
73 }
74
75 /**
76 * This function is responsible for the data sanitization
77 *
78 * @param array $field
79 * @param string|array $value
80 * @return string|array
81 */
82 public static function sanitize_field($field, $value) {
83 if (isset($field['sanitize']) && !empty($field['sanitize'])) {
84 if (function_exists($field['sanitize'])) {
85 $value = call_user_func($field['sanitize'], $value);
86 }
87 return $value;
88 }
89
90 if (is_array($field) && isset($field['type'])) {
91 switch ($field['type']) {
92 case 'text':
93 $value = sanitize_text_field($value);
94 break;
95 case 'textarea':
96 $value = sanitize_textarea_field($value);
97 break;
98 case 'email':
99 $value = sanitize_email($value);
100 break;
101 default:
102 return $value;
103 break;
104 }
105 } else {
106 $value = sanitize_text_field($value);
107 }
108
109 return $value;
110 }
111
112
113 /**
114 * Sorting Data
115 * by their type
116 *
117 * @param array $value
118 * @param string $key
119 * @return void
120 */
121 public static function sortBy(&$value, $key = 'comments') {
122 switch ($key) {
123 case 'comments':
124 return self::sorter($value, 'key', 'DESC');
125 break;
126 default:
127 return self::sorter($value, 'timestamp', 'DESC');
128 break;
129 }
130 }
131
132 /**
133 * This function is responsible for making an array sort by their key
134 * @param array $data
135 * @param string $using
136 * @param string $way
137 * @return array
138 */
139 public static function sorter($data, $using = 'time_date', $way = 'DESC') {
140 if (!is_array($data)) {
141 return $data;
142 }
143 $new_array = [];
144 if ($using === 'key') {
145 if ($way !== 'ASC') {
146 krsort($data);
147 } else {
148 ksort($data);
149 }
150 } else {
151 foreach ($data as $key => $value) {
152 if (!is_array($value)) continue;
153 foreach ($value as $inner_key => $single) {
154 if ($inner_key == $using) {
155 $value['tempid'] = $key;
156 $single = self::numeric_key_gen($new_array, $single);
157 $new_array[$single] = $value;
158 }
159 }
160 }
161
162 if ($way !== 'ASC') {
163 krsort($new_array);
164 } else {
165 ksort($new_array);
166 }
167
168 if (!empty($new_array)) {
169 foreach ($new_array as $array) {
170 $index = $array['tempid'];
171 unset($array['tempid']);
172 $new_data[$index] = $array;
173 }
174 $data = $new_data;
175 }
176 }
177
178 return $data;
179 }
180
181 /**
182 * This function is responsible for generate unique numeric key for a given array.
183 *
184 * @param array $data
185 * @param integer $index
186 * @return integer
187 */
188 protected static function numeric_key_gen($data, $index = 0) {
189 if (isset($data[$index])) {
190 $index += 1;
191 return self::numeric_key_gen($data, $index);
192 }
193 return $index;
194 }
195
196
197
198
199 /**
200 * Contact Forms Key Name filter for Name Selectbox
201 * @since 1.4.*
202 * @param string
203 * @return boolean
204 */
205 public static function filter_contactform_key_names($name) {
206 $validKey = true;
207 $filterWords = array(
208 "checkbox",
209 "color",
210 "date",
211 "datetime-local",
212 "file",
213 "image",
214 "month",
215 "number",
216 "password",
217 "radio",
218 "range",
219 "reset",
220 "submit",
221 "tel",
222 "time",
223 "week",
224 "Comment",
225 "message",
226 "address",
227 "phone",
228 );
229 foreach ($filterWords as $word) {
230 if (!empty($name) && stripos($name, $word) === false) {
231 $validKey = true;
232 } else {
233 $validKey = false;
234 break;
235 }
236 }
237 return $validKey;
238 }
239
240 /**
241 * Contact Forms Key Name remove special characters and meaningless words for Name Selectbox
242 * @since 1.4.*
243 * @param string
244 * @return string
245 */
246 public static function rename_contactform_key_names($name) {
247 $result = preg_split("/[_,\-]+/", $name);
248 $returnName = ucfirst($result[0]);
249 return $returnName;
250 }
251
252
253 /**
254 * Formating Number in a Nice way
255 * @since 1.2.1
256 * @param int|string $n
257 * @return string
258 */
259 public static function nice_number($n) {
260 $temp_number = !empty( $n ) ? str_replace(",", "", $n) : '';
261 if (!empty($temp_number)) {
262 $n = (0 + (int) $temp_number);
263 } else {
264 $n = (int) $n;
265 }
266 if (!is_numeric($n)) return 0;
267 $is_neg = false;
268 if ($n < 0) {
269 $is_neg = true;
270 $n = abs($n);
271 }
272 $number = 0;
273 $suffix = '';
274 switch (true) {
275 case $n >= 1000000000000:
276 $number = ($n / 1000000000000);
277 $suffix = $n > 1000000000000 ? 'T+' : 'T';
278 break;
279 case $n >= 1000000000:
280 $number = ($n / 1000000000);
281 $suffix = $n > 1000000000 ? 'B+' : 'B';
282 break;
283 case $n >= 1000000:
284 $number = ($n / 1000000);
285 $suffix = $n > 1000000 ? 'M+' : 'M';
286 break;
287 case $n >= 1000:
288 $number = ($n / 1000);
289 $suffix = $n > 1000 ? 'K+' : 'K';
290 break;
291 default:
292 $number = $n;
293 break;
294 }
295 if (strpos($number, '.') !== false && strpos($number, '.') >= 0) {
296 $number = number_format($number, 1);
297 }
298 return ($is_neg ? '-' : '') . $number . $suffix;
299 }
300
301 /**
302 * Developer log helper. Writes only when WP_DEBUG is on, and is used by the
303 * Pro plugin's Google/YouTube integrations to report API failures.
304 */
305 public static function write_log($log) {
306 if (true === WP_DEBUG) {
307 if (is_array($log) || is_object($log)) {
308 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r, WordPress.PHP.DevelopmentFunctions.error_log_error_log
309 error_log(print_r($log, true));
310 } else {
311 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
312 error_log($log);
313 }
314 }
315 }
316
317 public static function get_theme_or_plugin_list($api_data = null) {
318 $data = array();
319 $new_data = array();
320
321 $needed_key = array('slug', 'title', 'installs_count', 'active_installs_count', 'free_releases_count', 'premium_releases_count', 'total_purchases', 'total_subscriptions', 'total_renewals', 'accepted_payments', 'id', 'created', 'icon');
322
323 if (!empty($api_data->plugins)) {
324 foreach ($api_data->plugins as $single_data) {
325 $type = $single_data->type;
326 foreach ($needed_key as $key) {
327 if ($key == 'created') {
328 if (isset($single_data->$key)) {
329 $new_data['timestamp'] = strtotime($single_data->$key);
330 }
331 continue;
332 }
333 if (isset($single_data->$key)) {
334 $new_data[$key] = $single_data->$key;
335 }
336 }
337 $data[$type . 's'][$new_data['id']] = $new_data;
338 $new_data = array();
339 }
340 }
341
342 return $data;
343 }
344
345 public static function today_to_last_week($data) {
346 if (empty($data)) {
347 return array();
348 }
349 $new_data = array();
350 $timestamp = current_time('timestamp');
351 $date = gmdate('Y-m-d', $timestamp);
352 $date_7_days_back = gmdate('Y-m-d', strtotime($date . ' -8 days'));
353 $counter_7days = 0;
354 $counter_todays = 0;
355 foreach ($data as $single_install) {
356 gmdate('Y-m-d', strtotime($single_install->created)) > $date_7_days_back ? $counter_7days++ : $counter_7days;
357 gmdate('Y-m-d', strtotime($single_install->created)) == $date ? $counter_todays++ : $counter_todays;
358 }
359 return array(
360 'last_week' => $counter_7days,
361 'today' => $counter_todays,
362 );
363 }
364
365 public static function current_timestamp( $date = null, $timezone = 'UTC' ){
366 $timezone = new \DateTimeZone( $timezone );
367 $datetime = new \DateTime($date, $timezone);
368 return $datetime->getTimestamp();
369 }
370
371 public static function current_time($timestamp = null) {
372 $type = 'Y-m-d H:i:s';
373 if (empty($timestamp)) {
374 $timestamp = time();
375 }
376
377 $timezone = new \DateTimeZone('UTC');
378 if (is_numeric($timestamp)) {
379 $datetime = new \DateTime();
380 $datetime->setTimezone($timezone);
381 $datetime->setTimestamp($timestamp);
382 }
383 else{
384 $datetime = new \DateTime($timestamp);
385 $datetime->setTimezone($timezone);
386 }
387 return $datetime->format($type);
388 }
389
390 public static function get_utc_time($timestamp = null) {
391 $type = 'Y-m-d H:i:s';
392 if (empty($timestamp)) {
393 $timestamp = time();
394 }
395
396 // Get the WP timezone as a DateTimeZone object
397 $wp_timezone = wp_timezone();
398 $timezone = new \DateTimeZone('UTC');
399
400 if (is_numeric($timestamp)) {
401 $datetime = new \DateTime(null, $wp_timezone);
402 $datetime->setTimezone($timezone);
403 $datetime->setTimestamp($timestamp);
404 }
405 else{
406 $datetime = new \DateTime($timestamp, $wp_timezone);
407 $datetime->setTimezone($timezone);
408 }
409 return $datetime->format($type);
410 }
411
412 public static function mysql_time($timestamp = null) {
413 $type = 'Y-m-d H:i:s';
414 if (empty($timestamp)) {
415 $timestamp = time();
416 }
417
418 if (is_numeric($timestamp)) {
419 $datetime = new \DateTime();
420 $datetime->setTimestamp($timestamp);
421 }
422 else{
423 $datetime = new \DateTime($timestamp);
424 }
425 return $datetime->format($type);
426 }
427
428 /**
429 * Generating Full Name with one letter from last name
430 * @since 1.3.9
431 * @param string $first_name
432 * @param string $last_name
433 * @return string
434 */
435 public static function name($first_name = '', $last_name = '') {
436 $name = $first_name;
437 $name .= !empty($last_name) ? ' ' . mb_substr($last_name, 0, 1) : '';
438 return $name;
439 }
440
441 public static function get_type_title( $type ){
442 $_type = TypeFactory::get_instance()->get($type);
443 return ! empty( $_type->title ) ? $_type->title : $type;
444 }
445
446 public static function is_plugin_installed( $plugin ){
447 if ( ! function_exists( 'get_plugins' ) ) {
448 require_once ABSPATH . 'wp-admin/includes/plugin.php';
449 }
450 $plugins = get_plugins();
451 return isset( $plugins[ $plugin ] );
452 }
453
454 public static function is_plugin_active( $plugin ) {
455 return in_array( $plugin, (array) get_option( 'active_plugins', array() ), true ) || self::is_plugin_active_for_network( $plugin );
456 }
457
458 public static function is_plugin_active_for_network( $plugin ) {
459 if ( ! is_multisite() ) {
460 return false;
461 }
462
463 $plugins = get_site_option( 'active_sitewide_plugins' );
464 if ( isset( $plugins[ $plugin ] ) ) {
465 return true;
466 }
467
468 return false;
469 }
470 public static function remote_get($url, $args = array(), $raw = false, $assoc = null) {
471 $defaults = array(
472 'timeout' => 20,
473 'redirection' => 5,
474 'httpversion' => '1.1',
475 'user-agent' => 'NotificationX/' . NOTIFICATIONX_VERSION . '; ' . home_url(),
476 'body' => null,
477 'sslverify' => false,
478 'stream' => false,
479 'filename' => null
480 );
481 $args = wp_parse_args($args, $defaults);
482 $response = wp_remote_get($url, $args);
483
484 if (is_wp_error($response)) {
485 return false;
486 }
487 if($raw){
488 return $response;
489 }
490
491 $body = wp_remote_retrieve_body( $response );
492 $response = json_decode($body, $assoc);
493 $_response = (array) $response;
494 if (isset($_response['status']) && $_response['status'] == 'fail') {
495 return false;
496 }
497 return $response;
498 }
499
500 /**
501 * Get File Modification Time or URL
502 *
503 * @param string $file File relative path for Admin
504 * @param boolean $url true for URL return
505 * @return void|string|integer
506 */
507 public static function file( $file, $url = false ){
508 $base = '';
509 if(defined('NX_DEBUG') && NX_DEBUG){
510 if( $url ) {
511 $base = NOTIFICATIONX_DEV_ASSETS;
512 }
513 else{
514 $base = NOTIFICATIONX_DEV_ASSETS_PATH;
515 }
516 if(!file_exists(path_join(NOTIFICATIONX_DEV_ASSETS_PATH, $file))){
517 $base = '';
518 }
519 }
520 if(empty($base)){
521 if( $url ) {
522 $base = NOTIFICATIONX_ASSETS;
523 }
524 else{
525 $base = NOTIFICATIONX_ASSETS_PATH;
526 }
527 }
528 return path_join($base, $file);
529 }
530
531
532 /**
533 * This function returns an array of post titles by searching the post type and the input value
534 *
535 * @param string $post_type The post type to search
536 * @param string|array $inputValue The input value to search by title or ID
537 * @param integer $numberposts The number of posts to return
538 * @return array An associative array of post IDs and titles
539 */
540 public static function get_post_titles_by_search($post_type, $inputValue = '', $numberposts = 10, $args = []) {
541 global $wpdb;
542 $product_list = [];
543 $numberposts = intval( $numberposts );
544 $args = wp_parse_args( $args, [
545 'prefix' => '',
546 ] );
547
548 // Generate a unique cache key based on the input parameters
549 $cache_key = 'get_post_titles_by_search_' . md5( $post_type . serialize( $inputValue ) . $numberposts );
550
551 // Try to get the cached data from the object cache
552 $cached_data = wp_cache_get( $cache_key );
553
554 // If the cached data exists and is not expired, return it
555 if ( false !== $cached_data ) {
556 return $cached_data;
557 }
558
559 // Otherwise, run the original query
560 // Start with the common part of the query
561 $sql = "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish'";
562 $query_args = array( $post_type );
563
564 if ( is_array( $inputValue ) && count( $inputValue ) ) {
565 // If the input value is an array of post IDs, use IN clause with placeholders
566 // Generate a string of placeholders like %d,%d,%d
567 $placeholders = implode( ',', array_fill( 0, count( $inputValue ), '%d' ) );
568
569 // Add the IN clause to the query
570 $sql .= " AND ID IN ($placeholders)";
571
572 // Merge the input values to the query arguments
573 $query_args = array_merge( $query_args, array_map( 'intval', $inputValue ) );
574 } else {
575 // If the input value is a string, use LIKE clause with placeholder
576 if ( ! empty( $inputValue ) ) {
577 // Add the LIKE clause to the query
578 $sql .= " AND post_title LIKE %s";
579
580 // Add the input value to the query arguments with wildcards
581 $query_args[] = '%' . $wpdb->esc_like( $inputValue ) . '%';
582 }
583 }
584
585 // Add order and limit clauses
586 $sql .= " ORDER BY post_date DESC LIMIT %d";
587
588 // Add the number of posts to the query arguments
589 $query_args[] = $numberposts;
590
591 // Prepare and execute the query using wpdb methods
592 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16.
593 $sql = $wpdb->prepare( $sql, $query_args );
594 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16.
595 $products = $wpdb->get_results( $sql );
596
597 if ( ! empty( $products ) ) {
598 // Loop through the results and build the output array
599 foreach ( $products as $product ) {
600 $key = $args['prefix'] . $product->ID;
601 $product_list[ $key ] = $product->post_title;
602 }
603 }
604
605 // Store the query result in the object cache with an expiration time of one hour
606 wp_cache_set( $cache_key, $product_list, '', MINUTE_IN_SECONDS );
607
608 // Return the query result
609 return $product_list;
610 }
611
612 /**
613 * Checks if WPML (WordPress Multilingual Plugin) is properly set up and loaded.
614 *
615 * @return bool True if WPML is set up, false otherwise.
616 */
617 public static function is_wpml_setup()
618 {
619 $wpml_has_run = get_option('WPML(TM-has-run)');
620 if (!empty($wpml_has_run['WPML\TM\ATE\Sitekey\Sync']) && did_action('wpml_loaded') && function_exists('load_wpml_st_basics')) {
621 return true;
622 }
623 return false;
624 }
625
626 /**
627 * Returns a list of common fields for GDPR cookie configuration settings.
628 *
629 * @return array An associative array of common GDPR cookie fields.
630 */
631 public static function gdpr_common_fields()
632 {
633 return [
634 'enabled' => array(
635 'type' => 'toggle',
636 'name' => 'enabled',
637 'label' => __('Enabled', 'notificationx'),
638 'priority' => 5,
639 ),
640 'discovered' => array(
641 'type' => 'toggle',
642 'name' => 'discovered',
643 'label' => __('Discovered', 'notificationx'),
644 'priority' => 5,
645 ),
646 'cookies_id' => array(
647 'type' => 'text',
648 'name' => 'cookies_id',
649 'label' => __('Cookie ID', 'notificationx'),
650 'priority' => 10,
651 ),
652 'domain' => array(
653 'type' => 'text',
654 'name' => 'domain',
655 'label' => __('Domain', 'notificationx'),
656 'priority' => 15,
657 ),
658 'duration' => array(
659 'type' => 'number',
660 'name' => 'duration',
661 'label' => __('Duration', 'notificationx'),
662 'min' => 1,
663 'priority' => 20,
664 'suggestions' => [
665 [
666 'value' => 30,
667 'unit' => 'days',
668 ],
669 [
670 'value' => 90,
671 'unit' => 'days',
672 ],
673 [
674 'value' => 180,
675 'unit' => 'days',
676 ],
677 [
678 'value' => 365,
679 'unit' => 'days',
680 ],
681 ],
682 ),
683 'description' => array(
684 'type' => 'textarea',
685 'name' => 'description',
686 'label' => __('Description', 'notificationx'),
687 'priority' => 30,
688 ),
689 'is_add_script' => array(
690 'type' => 'toggle',
691 'name' => 'is_add_script',
692 'label' => __('Add Script', 'notificationx'),
693 'priority' => 35,
694 ),
695 'load_inside' => array(
696 'label' => __('Add Script on', 'notificationx'),
697 'name' => 'product_control',
698 'type' => 'select',
699 'priority' => 40,
700 'default' => 'head',
701 'options' => GlobalFields::get_instance()->normalize_fields([
702 'head' => __('Header', 'notificationx'),
703 'body' => __('Body', 'notificationx'),
704 'footer' => __('Footer', 'notificationx'),
705 ]),
706 ),
707 'script_url_pattern' => array(
708 'type' => 'codeviewer',
709 'name' => 'script_url_pattern',
710 'label' => __('Script', 'notificationx'),
711 'priority' => 45,
712 ),
713 ];
714 }
715
716 /**
717 * Specifies the fields to be shown in the GDPR cookie list.
718 *
719 * @return array An array of field names visible in the GDPR cookie list.
720 */
721 public static function gdpr_cookie_list_visible_fields()
722 {
723 return ['cookies_id', 'domain', 'script_url_pattern', 'duration', 'load_inside','description'];
724 }
725
726 /**
727 * Deletes specific cookies on the server and returns a list of removed cookies.
728 *
729 * @return void Outputs a JSON-encoded list of removed cookies.
730 */
731 public static function delete_server_cookies()
732 {
733 $urlparts = wp_parse_url(site_url('/'));
734 $domain = preg_replace('/www\./i', '', $urlparts['host']);
735 $cookies_removed = array();
736 $d_domains = array('_ga', '_fbp', '_gid', '_gat', '__utma', '__utmb', '__utmc', '__utmt', '__utmz');
737 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
738 $d_domains = apply_filters('gdpr_d_domains_filter', $d_domains);
739
740 // Iterate over all cookies and remove them if they match specific conditions.
741 if (isset($_COOKIE) && is_array($_COOKIE) && $domain) :
742 foreach ($_COOKIE as $key => $value) {
743 if ($key !== 'moove_gdpr_popup' && strpos($key, 'woocommerce') === false && strpos($key, 'wc_') === false && strpos($key, 'wordpress') === false) :
744 if ('language' === $key || 'currency' === $key) {
745 setcookie($key, null, -1, '/', 'www.' . $domain);
746 $cookies_removed[$key] = $domain;
747 } elseif (in_array($key, $d_domains) || strpos($key, '_ga') !== false || strpos($key, '_fbp') !== false) {
748 setcookie($key, null, -1, '/', '.' . $domain);
749 $cookies_removed[$key] = $domain;
750 }
751 endif;
752 }
753 endif;
754
755 // Parse and remove cookies from the HTTP header.
756 $cookies = isset($_SERVER['HTTP_COOKIE']) ? explode(';', sanitize_text_field(wp_unslash($_SERVER['HTTP_COOKIE']))) : false;
757 if (is_array($cookies)) :
758 foreach ($cookies as $cookie) {
759 $parts = explode('=', $cookie);
760 $name = trim($parts[0]);
761 if ($name && $name !== 'moove_gdpr_popup' && strpos($name, 'woocommerce') === false && strpos($name, 'wc_') === false && strpos($name, 'wordpress') === false) :
762 setcookie($name, '', time() - 1000);
763 setcookie($name, '', time() - 1000, '/');
764 if ('language' === $name || 'currency' === $name) {
765 setcookie($name, null, -1, '/', 'www.' . $domain);
766 $cookies_removed[$name] = $domain;
767 } elseif (in_array($key, $d_domains) || strpos($name, '_ga') !== false || strpos($name, '_fbp') !== false) {
768 setcookie($name, null, -1, '/', '.' . $domain);
769 $cookies_removed[$name] = '.' . $domain;
770 } else {
771 setcookie($name, null, -1, '/');
772 $cookies_removed[$name] = $domain;
773 }
774 endif;
775 }
776 endif;
777
778 // Output the list of removed cookies as a JSON response.
779 echo json_encode($cookies_removed);
780 }
781
782 public static function tab_info_title($name, $title_default, $modal = false)
783 {
784 return [
785 'type' => 'text',
786 'name' => "{$name}_tab_title",
787 'default' => $title_default,
788 'label' => __('Name', 'notificationx'),
789 'autoFocus' => true,
790 ];
791 }
792
793 public static function tab_info_desc($name, $desc_default, $modal = false)
794 {
795 return [
796 'type' => 'textarea',
797 'row' => 3,
798 'name' => "{$name}_tab_desc",
799 'default' => $desc_default,
800 'label' => __('Description', 'notificationx'),
801 ];
802 }
803
804
805 public static function default_cookie_list()
806 {
807 return [
808 [
809 'enabled' => true,
810 'default' => true,
811 'cookies_id' => 'wordpress_logged_in',
812 'load_inside' => 'head',
813 'script_url_pattern' => '',
814 'description' => __('Indicates when a user is logged in and who they are, for most interface use.','notificationx'),
815 'index' => wp_generate_uuid4(),
816 ],
817 [
818 'enabled' => true,
819 'default' => true,
820 'cookies_id' => 'wordpress_sec',
821 'load_inside' => 'head',
822 'script_url_pattern' => '',
823 'description' => __('Used for security purposes for logged-in users.', 'notificationx'),
824 'index' => wp_generate_uuid4(),
825 ],
826 [
827 'enabled' => true,
828 'default' => true,
829 'cookies_id' => 'wp-settings-{user_id}',
830 'load_inside' => 'head',
831 'script_url_pattern' => '',
832 'description' => __('Used to persist a user\'s WordPress admin settings.','notificationx'),
833 'index' => wp_generate_uuid4(),
834 ],
835 [
836 'enabled' => true,
837 'default' => true,
838 'cookies_id' => 'wp-settings-time-{user_id}',
839 'load_inside' => 'head',
840 'script_url_pattern' => '',
841 'description' => __('Records the time that wp-settings-{user_id} was set.', 'notificationx'),
842 'index' => wp_generate_uuid4(),
843 ],
844 [
845 'enabled' => true,
846 'default' => true,
847 'cookies_id' => 'wp-settings-time-{user_id}',
848 'load_inside' => 'head',
849 'script_url_pattern' => '',
850 'description' => __('Records the time that wp-settings-{user_id} was set.', 'notificationx'),
851 'index' => wp_generate_uuid4(),
852 ],
853 [
854 'enabled' => true,
855 'default' => true,
856 'cookies_id' => 'nx_cookie_manager',
857 'script_url_pattern' => '',
858 'description' => __('Manages the cookies on the site, ensuring user consent for GDPR compliance.', 'notificationx'),
859 'index' => wp_generate_uuid4(),
860 ],
861 ];
862
863 }
864
865 // Helper function to get the image ID from data
866 public static function get_image_id_from_settings($data) {
867 return isset($data['image']['id']) ? $data['image']['id'] : null;
868 }
869
870 // Helper function to get custom image size
871 public static function get_custom_image_size() {
872 $default_size = '100_100'; // Default size
873 $image_size = (string) Settings::get_instance()->get('settings.notification_image_size', $default_size);
874 $image_size_parts = explode('_', $image_size);
875
876 if (!empty($image_size_parts[0]) && is_numeric($image_size_parts[0]) && !empty($image_size_parts[1]) && is_numeric($image_size_parts[1])) {
877 return [
878 'width' => (int) $image_size_parts[0],
879 'height' => (int) $image_size_parts[1],
880 ];
881 }
882
883 return [
884 'width' => 100, // Default width
885 'height' => 100, // Default height
886 ];
887 }
888
889 // Helper function to get resized image URL
890 public static function get_resized_image_url($image_id, $custom_size) {
891 if (!$image_id || empty($custom_size['width']) || empty($custom_size['height'])) {
892 return null;
893 }
894
895 $image = wp_get_attachment_image_src(
896 $image_id,
897 [$custom_size['width'], $custom_size['height']],
898 true // Crop the image to exact dimensions
899 );
900
901 return $image && isset($image[0]) ? $image[0] : null;
902 }
903
904 public static function nx_allowed_html()
905 {
906 return [
907 'a' => [
908 'href' => [],
909 'title' => [],
910 'target' => [],
911 'rel' => [],
912 'class' => [],
913 'id' => [],
914 ],
915 'abbr' => [
916 'title' => [],
917 'class' => [],
918 ],
919 'style' => [],
920 'b' => [
921 'class' => [],
922 ],
923 'blockquote' => [
924 'cite' => [],
925 'class' => [],
926 ],
927 'br' => [],
928 'cite' => [
929 'class' => [],
930 ],
931 'code' => [
932 'class' => [],
933 ],
934 'del' => [
935 'datetime' => [],
936 'class' => [],
937 ],
938 'div' => [
939 'class' => [],
940 'id' => [],
941 'style' => [],
942 ],
943 'em' => [
944 'class' => [],
945 ],
946 'h1' => [
947 'class' => [],
948 'id' => [],
949 ],
950 'h2' => [
951 'class' => [],
952 'id' => [],
953 ],
954 'h3' => [
955 'class' => [],
956 'id' => [],
957 ],
958 'h4' => [
959 'class' => [],
960 'id' => [],
961 ],
962 'h5' => [
963 'class' => [],
964 'id' => [],
965 ],
966 'h6' => [
967 'class' => [],
968 'id' => [],
969 ],
970 'hr' => [
971 'class' => [],
972 ],
973 'i' => [
974 'class' => [],
975 ],
976 'img' => [
977 'src' => [],
978 'alt' => [],
979 'title' => [],
980 'width' => [],
981 'height' => [],
982 'class' => [],
983 'id' => [],
984 ],
985 'li' => [
986 'class' => [],
987 ],
988 'ol' => [
989 'class' => [],
990 ],
991 'p' => [
992 'class' => [],
993 'style' => [],
994 ],
995 'pre' => [
996 'class' => [],
997 ],
998 'q' => [
999 'cite' => [],
1000 'class' => [],
1001 ],
1002 'span' => [
1003 'class' => [],
1004 'style' => [],
1005 ],
1006 'strong' => [
1007 'class' => [],
1008 ],
1009 'table' => [
1010 'class' => [],
1011 'style' => [],
1012 ],
1013 'tbody' => [
1014 'class' => [],
1015 ],
1016 'td' => [
1017 'colspan' => [],
1018 'rowspan' => [],
1019 'class' => [],
1020 'style' => [],
1021 ],
1022 'tfoot' => [
1023 'class' => [],
1024 ],
1025 'th' => [
1026 'colspan' => [],
1027 'rowspan' => [],
1028 'scope' => [],
1029 'class' => [],
1030 'style' => [],
1031 ],
1032 'thead' => [
1033 'class' => [],
1034 ],
1035 'tr' => [
1036 'class' => [],
1037 ],
1038 'ul' => [
1039 'class' => [],
1040 ],
1041 ];
1042 }
1043 public static function generate_time_string($data) {
1044 $timeString = '';
1045 if (isset($data['display_from']) && intval($data['display_from']) > 0) {
1046 $timeString .= intval($data['display_from']) . ' days ';
1047 }
1048
1049 if (isset($data['display_from_hour']) && intval($data['display_from_hour']) > 0) {
1050 $timeString .= intval($data['display_from_hour']) . ' hours ';
1051 }
1052
1053 if (isset($data['display_from_minute']) && intval($data['display_from_minute']) > 0) {
1054 $timeString .= intval($data['display_from_minute']) . ' minutes ';
1055 }
1056
1057 if (!empty($timeString)) {
1058 $time = strtotime($timeString . ' ago');
1059 } else {
1060 $time = time(); // Default to current time if no valid inputs
1061 }
1062 return $time;
1063 }
1064
1065 /**
1066 * Get the current datetime based on the WordPress site's timezone.
1067 *
1068 * @return string Formatted datetime in 'Y-m-d H:i:s' format.
1069 */
1070 public static function nx_get_current_datetime() {
1071 // Get the WordPress timezone setting
1072 $timezone = get_option('timezone_string');
1073
1074 if (!$timezone) {
1075 // If timezone_string is empty, fallback to gmt_offset
1076 $gmt_offset = get_option('gmt_offset');
1077
1078 if ($gmt_offset !== false) {
1079 $timezone = timezone_name_from_abbr("", (int) $gmt_offset * 3600, false);
1080 }
1081
1082 // If timezone_name_from_abbr fails, manually handle GMT offsets
1083 if (!$timezone) {
1084 $timezone = sprintf('Etc/GMT%+d', -$gmt_offset); // Example: GMT+6 → Etc/GMT-6
1085 }
1086 }
1087
1088 try {
1089 $date = new \DateTime('now', new \DateTimeZone($timezone));
1090 return $date->format('Y-m-d H:i:s'); // Format as MySQL datetime
1091 } catch (\Exception $e) {
1092 // If an error occurs, return UTC time as a fallback
1093 return gmdate('Y-m-d H:i:s');
1094 }
1095 }
1096
1097 public static function nx_get_visitor_country_code() {
1098 // Country targeting now applies to ALL notification types, so a single page
1099 // load can evaluate several country-targeted notifications. Resolve the
1100 // visitor's country once per request and reuse it. We key the memo on the IP
1101 // and read it with array_key_exists (not isset) so an empty/failed result is
1102 // remembered too and never re-triggers the header/API lookups below within the
1103 // same request. An unknown country is always represented as an empty string.
1104 static $memo = [];
1105
1106 $ip = '';
1107 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
1108 $ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_CLIENT_IP']));
1109 } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1110 $ip = explode(',', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR'])))[0];
1111 } else {
1112 $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
1113 }
1114 $ip = trim($ip);
1115
1116 // No usable IP (CLI/cron) or localhost: return an empty code. Callers treat an
1117 // unknown country as "don't filter" (fail-open), so country-targeted
1118 // notifications still show during local testing instead of silently
1119 // disappearing. Bailing here also avoids requesting
1120 // http://ip-api.com/json/?fields=countryCode with an empty IP, which would
1121 // resolve to the SERVER's country and cache nothing.
1122 if ($ip === '' || $ip === '127.0.0.1' || $ip === '::1') {
1123 return apply_filters('nx_visitor_country_code', '', $ip);
1124 }
1125
1126 if (array_key_exists($ip, $memo)) {
1127 return apply_filters('nx_visitor_country_code', $memo[$ip], $ip);
1128 }
1129
1130 // Let a host/CDN or custom resolver short-circuit the external lookup entirely.
1131 // e.g. Cloudflare sends CF-IPCountry; many hosts expose GEOIP_COUNTRY_CODE.
1132 $header_country = '';
1133 foreach (['HTTP_CF_IPCOUNTRY', 'GEOIP_COUNTRY_CODE', 'HTTP_X_COUNTRY_CODE'] as $h) {
1134 if (!empty($_SERVER[$h])) {
1135 $header_country = strtoupper(sanitize_text_field(wp_unslash($_SERVER[$h])));
1136 break;
1137 }
1138 }
1139 // 'XX'/'T1' are Cloudflare's "unknown"/Tor placeholders — ignore them.
1140 if ($header_country !== '' && !in_array($header_country, ['XX', 'T1'], true)) {
1141 $memo[$ip] = $header_country;
1142 return apply_filters('nx_visitor_country_code', $header_country, $ip);
1143 }
1144
1145 // Per-IP cache so we don't hit the external API on every page load. A
1146 // transient is used deliberately: WordPress serves transients from a
1147 // persistent object cache (Redis/Memcached) when one is available — so those
1148 // sites add zero wp_options rows — and transparently falls back to the options
1149 // table otherwise, so the country stays cached across requests on plain sites
1150 // too (which is what keeps us under ip-api's 45 req/min limit). A cached empty
1151 // string is a remembered "lookup failed" marker, distinct from a miss (false).
1152 $cache_key = 'nx_geo_' . md5($ip);
1153 $cached = get_transient($cache_key);
1154 if (false !== $cached) {
1155 $memo[$ip] = $cached;
1156 return apply_filters('nx_visitor_country_code', $cached, $ip);
1157 }
1158
1159 $country = '';
1160 $api_endpoint = apply_filters('nx_visitor_country_api', "http://ip-api.com/json/{$ip}?fields=countryCode", $ip);
1161 $response = wp_remote_get($api_endpoint, ['timeout' => 3]);
1162
1163 if (!is_wp_error($response) && (int) wp_remote_retrieve_response_code($response) === 200) {
1164 $data = json_decode(wp_remote_retrieve_body($response), true);
1165 if (isset($data['countryCode']) && $data['countryCode'] !== '') {
1166 $country = $data['countryCode'];
1167 }
1168 }
1169
1170 // Cache successes for the full TTL; negative-cache failures for a short window
1171 // so a rate-limit/timeout (ip-api's free tier is ~45 req/min keyed by the
1172 // SERVER IP, shared across all visitors) doesn't re-hit the API on every
1173 // subsequent page load while it recovers.
1174 $ttl = '' !== $country
1175 ? (int) apply_filters('nx_visitor_country_cache_ttl', 12 * HOUR_IN_SECONDS)
1176 : (int) apply_filters('nx_visitor_country_failed_cache_ttl', 5 * MINUTE_IN_SECONDS);
1177 if ($ttl > 0) {
1178 set_transient($cache_key, $country, $ttl);
1179 }
1180
1181 $memo[$ip] = $country;
1182 return apply_filters('nx_visitor_country_code', $country, $ip);
1183 }
1184
1185 public static function nx_get_all_country($search = '') {
1186 $countries = [
1187 'all' => __('All Countries', 'notificationx'),
1188 'AF' => __('Afghanistan', 'notificationx'),
1189 'AL' => __('Albania', 'notificationx'),
1190 'DZ' => __('Algeria', 'notificationx'),
1191 'AS' => __('American Samoa', 'notificationx'),
1192 'AD' => __('Andorra', 'notificationx'),
1193 'AO' => __('Angola', 'notificationx'),
1194 'AI' => __('Anguilla', 'notificationx'),
1195 'AQ' => __('Antarctica', 'notificationx'),
1196 'AG' => __('Antigua and Barbuda', 'notificationx'),
1197 'AR' => __('Argentina', 'notificationx'),
1198 'AM' => __('Armenia', 'notificationx'),
1199 'AW' => __('Aruba', 'notificationx'),
1200 'AU' => __('Australia', 'notificationx'),
1201 'AT' => __('Austria', 'notificationx'),
1202 'AZ' => __('Azerbaijan', 'notificationx'),
1203 'BS' => __('Bahamas', 'notificationx'),
1204 'BH' => __('Bahrain', 'notificationx'),
1205 'BD' => __('Bangladesh', 'notificationx'),
1206 'BB' => __('Barbados', 'notificationx'),
1207 'BY' => __('Belarus', 'notificationx'),
1208 'BE' => __('Belgium', 'notificationx'),
1209 'BZ' => __('Belize', 'notificationx'),
1210 'BJ' => __('Benin', 'notificationx'),
1211 'BM' => __('Bermuda', 'notificationx'),
1212 'BT' => __('Bhutan', 'notificationx'),
1213 'BO' => __('Bolivia', 'notificationx'),
1214 'BA' => __('Bosnia and Herzegovina', 'notificationx'),
1215 'BW' => __('Botswana', 'notificationx'),
1216 'BR' => __('Brazil', 'notificationx'),
1217 'BN' => __('Brunei', 'notificationx'),
1218 'BG' => __('Bulgaria', 'notificationx'),
1219 'BF' => __('Burkina Faso', 'notificationx'),
1220 'BI' => __('Burundi', 'notificationx'),
1221 'KH' => __('Cambodia', 'notificationx'),
1222 'CM' => __('Cameroon', 'notificationx'),
1223 'CA' => __('Canada', 'notificationx'),
1224 'CV' => __('Cape Verde', 'notificationx'),
1225 'CF' => __('Central African Republic', 'notificationx'),
1226 'TD' => __('Chad', 'notificationx'),
1227 'CL' => __('Chile', 'notificationx'),
1228 'CN' => __('China', 'notificationx'),
1229 'CO' => __('Colombia', 'notificationx'),
1230 'KM' => __('Comoros', 'notificationx'),
1231 'CG' => __('Congo (Brazzaville)', 'notificationx'),
1232 'CD' => __('Congo (Kinshasa)', 'notificationx'),
1233 'CR' => __('Costa Rica', 'notificationx'),
1234 'HR' => __('Croatia', 'notificationx'),
1235 'CU' => __('Cuba', 'notificationx'),
1236 'CY' => __('Cyprus', 'notificationx'),
1237 'CZ' => __('Czech Republic', 'notificationx'),
1238 'DK' => __('Denmark', 'notificationx'),
1239 'DJ' => __('Djibouti', 'notificationx'),
1240 'DM' => __('Dominica', 'notificationx'),
1241 'DO' => __('Dominican Republic', 'notificationx'),
1242 'EC' => __('Ecuador', 'notificationx'),
1243 'EG' => __('Egypt', 'notificationx'),
1244 'SV' => __('El Salvador', 'notificationx'),
1245 'GQ' => __('Equatorial Guinea', 'notificationx'),
1246 'ER' => __('Eritrea', 'notificationx'),
1247 'EE' => __('Estonia', 'notificationx'),
1248 'ET' => __('Ethiopia', 'notificationx'),
1249 'FJ' => __('Fiji', 'notificationx'),
1250 'FI' => __('Finland', 'notificationx'),
1251 'FR' => __('France', 'notificationx'),
1252 'GA' => __('Gabon', 'notificationx'),
1253 'GM' => __('Gambia', 'notificationx'),
1254 'GE' => __('Georgia', 'notificationx'),
1255 'DE' => __('Germany', 'notificationx'),
1256 'GH' => __('Ghana', 'notificationx'),
1257 'GR' => __('Greece', 'notificationx'),
1258 'GD' => __('Grenada', 'notificationx'),
1259 'GT' => __('Guatemala', 'notificationx'),
1260 'GN' => __('Guinea', 'notificationx'),
1261 'GW' => __('Guinea-Bissau', 'notificationx'),
1262 'GY' => __('Guyana', 'notificationx'),
1263 'HT' => __('Haiti', 'notificationx'),
1264 'HN' => __('Honduras', 'notificationx'),
1265 'HK' => __('Hong Kong', 'notificationx'),
1266 'HU' => __('Hungary', 'notificationx'),
1267 'IS' => __('Iceland', 'notificationx'),
1268 'IN' => __('India', 'notificationx'),
1269 'ID' => __('Indonesia', 'notificationx'),
1270 'IR' => __('Iran', 'notificationx'),
1271 'IQ' => __('Iraq', 'notificationx'),
1272 'IE' => __('Ireland', 'notificationx'),
1273 'IL' => __('Israel', 'notificationx'),
1274 'IT' => __('Italy', 'notificationx'),
1275 'JM' => __('Jamaica', 'notificationx'),
1276 'JP' => __('Japan', 'notificationx'),
1277 'JO' => __('Jordan', 'notificationx'),
1278 'KZ' => __('Kazakhstan', 'notificationx'),
1279 'KE' => __('Kenya', 'notificationx'),
1280 'KI' => __('Kiribati', 'notificationx'),
1281 'KR' => __('Korea, South', 'notificationx'),
1282 'KW' => __('Kuwait', 'notificationx'),
1283 'KG' => __('Kyrgyzstan', 'notificationx'),
1284 'LA' => __('Laos', 'notificationx'),
1285 'LV' => __('Latvia', 'notificationx'),
1286 'LB' => __('Lebanon', 'notificationx'),
1287 'LS' => __('Lesotho', 'notificationx'),
1288 'LR' => __('Liberia', 'notificationx'),
1289 'LY' => __('Libya', 'notificationx'),
1290 'LI' => __('Liechtenstein', 'notificationx'),
1291 'LT' => __('Lithuania', 'notificationx'),
1292 'LU' => __('Luxembourg', 'notificationx'),
1293 'MG' => __('Madagascar', 'notificationx'),
1294 'MW' => __('Malawi', 'notificationx'),
1295 'MY' => __('Malaysia', 'notificationx'),
1296 'MV' => __('Maldives', 'notificationx'),
1297 'ML' => __('Mali', 'notificationx'),
1298 'MT' => __('Malta', 'notificationx'),
1299 'MH' => __('Marshall Islands', 'notificationx'),
1300 'MR' => __('Mauritania', 'notificationx'),
1301 'MU' => __('Mauritius', 'notificationx'),
1302 'MX' => __('Mexico', 'notificationx'),
1303 'FM' => __('Micronesia', 'notificationx'),
1304 'MD' => __('Moldova', 'notificationx'),
1305 'MC' => __('Monaco', 'notificationx'),
1306 'MN' => __('Mongolia', 'notificationx'),
1307 'ME' => __('Montenegro', 'notificationx'),
1308 'MA' => __('Morocco', 'notificationx'),
1309 'MZ' => __('Mozambique', 'notificationx'),
1310 'MM' => __('Myanmar (Burma)', 'notificationx'),
1311 'NA' => __('Namibia', 'notificationx'),
1312 'NR' => __('Nauru', 'notificationx'),
1313 'NP' => __('Nepal', 'notificationx'),
1314 'NL' => __('Netherlands', 'notificationx'),
1315 'NZ' => __('New Zealand', 'notificationx'),
1316 'NI' => __('Nicaragua', 'notificationx'),
1317 'NE' => __('Niger', 'notificationx'),
1318 'NG' => __('Nigeria', 'notificationx'),
1319 'MK' => __('North Macedonia', 'notificationx'),
1320 'NO' => __('Norway', 'notificationx'),
1321 'OM' => __('Oman', 'notificationx'),
1322 'PK' => __('Pakistan', 'notificationx'),
1323 'PW' => __('Palau', 'notificationx'),
1324 'PA' => __('Panama', 'notificationx'),
1325 'PG' => __('Papua New Guinea', 'notificationx'),
1326 'PY' => __('Paraguay', 'notificationx'),
1327 'PE' => __('Peru', 'notificationx'),
1328 'PH' => __('Philippines', 'notificationx'),
1329 'PL' => __('Poland', 'notificationx'),
1330 'PT' => __('Portugal', 'notificationx'),
1331 'QA' => __('Qatar', 'notificationx'),
1332 'RO' => __('Romania', 'notificationx'),
1333 'RU' => __('Russia', 'notificationx'),
1334 'RW' => __('Rwanda', 'notificationx'),
1335 'SA' => __('Saudi Arabia', 'notificationx'),
1336 'SN' => __('Senegal', 'notificationx'),
1337 'RS' => __('Serbia', 'notificationx'),
1338 'SC' => __('Seychelles', 'notificationx'),
1339 'SL' => __('Sierra Leone', 'notificationx'),
1340 'SG' => __('Singapore', 'notificationx'),
1341 'SK' => __('Slovakia', 'notificationx'),
1342 'SI' => __('Slovenia', 'notificationx'),
1343 'SB' => __('Solomon Islands', 'notificationx'),
1344 'SO' => __('Somalia', 'notificationx'),
1345 'ZA' => __('South Africa', 'notificationx'),
1346 'ES' => __('Spain', 'notificationx'),
1347 'LK' => __('Sri Lanka', 'notificationx'),
1348 'SD' => __('Sudan', 'notificationx'),
1349 'SR' => __('Suriname', 'notificationx'),
1350 'SE' => __('Sweden', 'notificationx'),
1351 'CH' => __('Switzerland', 'notificationx'),
1352 'SY' => __('Syria', 'notificationx'),
1353 'TW' => __('Taiwan', 'notificationx'),
1354 'TJ' => __('Tajikistan', 'notificationx'),
1355 'TZ' => __('Tanzania', 'notificationx'),
1356 'TH' => __('Thailand', 'notificationx'),
1357 'TG' => __('Togo', 'notificationx'),
1358 'TO' => __('Tonga', 'notificationx'),
1359 'TT' => __('Trinidad and Tobago', 'notificationx'),
1360 'TN' => __('Tunisia', 'notificationx'),
1361 'TR' => __('Turkey', 'notificationx'),
1362 'TM' => __('Turkmenistan', 'notificationx'),
1363 'UG' => __('Uganda', 'notificationx'),
1364 'UA' => __('Ukraine', 'notificationx'),
1365 'AE' => __('United Arab Emirates', 'notificationx'),
1366 'GB' => __('United Kingdom', 'notificationx'),
1367 'US' => __('United States', 'notificationx'),
1368 'UY' => __('Uruguay', 'notificationx'),
1369 'UZ' => __('Uzbekistan', 'notificationx'),
1370 'VU' => __('Vanuatu', 'notificationx'),
1371 'VE' => __('Venezuela', 'notificationx'),
1372 'VN' => __('Vietnam', 'notificationx'),
1373 'YE' => __('Yemen', 'notificationx'),
1374 'ZM' => __('Zambia', 'notificationx'),
1375 'ZW' => __('Zimbabwe', 'notificationx'),
1376 ];
1377 if (!empty($search)) {
1378 $search = strtolower($search);
1379 $countries = array_filter($countries, function($name) use ($search) {
1380 return strpos(strtolower($name), $search) !== false;
1381 });
1382 }
1383 return $countries;
1384 }
1385
1386
1387 /**
1388 * Delete a design document that NotificationX itself owns.
1389 *
1390 * The ID reaching the callers of this method arrives in a REST payload, so
1391 * it is attacker-controlled. Without a post-type check, any user holding
1392 * `edit_notificationx` could pass an arbitrary ID and force-delete any post
1393 * on the site -- pages, products, orders -- with no trash to recover from.
1394 * Only documents of a post type NotificationX creates may be removed here.
1395 *
1396 * A `current_user_can( 'delete_post' )` check is deliberately NOT applied.
1397 * These post types register with `capability_type => 'post'`, so that meta
1398 * cap resolves to the primitive `delete_posts`. A custom role delegated only
1399 * "Who Can Create Notification?" does not hold `delete_posts`, and gating on
1400 * it would stop that role from removing its own designs -- breaking exactly
1401 * the delegated workflow this boundary exists to support. Actor authority is
1402 * already established by the route's `edit_notificationx` permission
1403 * callback; what was missing, and what this restores, is object authority.
1404 *
1405 * @param int|string $post_id Candidate post ID, untrusted.
1406 * @param string $expected_type Post type NotificationX owns.
1407 * @return bool True when a post was deleted.
1408 */
1409 public static function delete_owned_post( $post_id, $expected_type ) {
1410 $post_id = absint( $post_id );
1411 if ( ! $post_id ) {
1412 return false;
1413 }
1414
1415 $post = get_post( $post_id );
1416 if ( ! $post || $expected_type !== $post->post_type ) {
1417 return false;
1418 }
1419
1420 return (bool) wp_delete_post( $post_id, true );
1421 }
1422
1423
1424 }
1425