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.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 1.2.2 All 155 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 trunk, at includes/Core/Helper.php

1,437 lines 54.1 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 remove_old_notice(){
471 global $wp_filter;
472 if( isset( $wp_filter['admin_notices']->callbacks[10] ) && is_array( $wp_filter['admin_notices']->callbacks[10] ) ) {
473 foreach( $wp_filter['admin_notices']->callbacks[10] as $hash => $callbacks ) {
474 if( is_array( $callbacks['function'] ) && ! empty( $callbacks['function'][0] ) && is_object( $callbacks['function'][0] ) && $callbacks['function'][0] instanceof \NotificationX_Licensing ) {
475 remove_action( 'admin_notices', $hash );
476 break;
477 }
478 }
479 }
480 }
481
482 public static function remote_get($url, $args = array(), $raw = false, $assoc = null) {
483 $defaults = array(
484 'timeout' => 20,
485 'redirection' => 5,
486 'httpversion' => '1.1',
487 'user-agent' => 'NotificationX/' . NOTIFICATIONX_VERSION . '; ' . home_url(),
488 'body' => null,
489 'sslverify' => false,
490 'stream' => false,
491 'filename' => null
492 );
493 $args = wp_parse_args($args, $defaults);
494 $response = wp_remote_get($url, $args);
495
496 if (is_wp_error($response)) {
497 return false;
498 }
499 if($raw){
500 return $response;
501 }
502
503 $body = wp_remote_retrieve_body( $response );
504 $response = json_decode($body, $assoc);
505 $_response = (array) $response;
506 if (isset($_response['status']) && $_response['status'] == 'fail') {
507 return false;
508 }
509 return $response;
510 }
511
512 /**
513 * Get File Modification Time or URL
514 *
515 * @param string $file File relative path for Admin
516 * @param boolean $url true for URL return
517 * @return void|string|integer
518 */
519 public static function file( $file, $url = false ){
520 $base = '';
521 if(defined('NX_DEBUG') && NX_DEBUG){
522 if( $url ) {
523 $base = NOTIFICATIONX_DEV_ASSETS;
524 }
525 else{
526 $base = NOTIFICATIONX_DEV_ASSETS_PATH;
527 }
528 if(!file_exists(path_join(NOTIFICATIONX_DEV_ASSETS_PATH, $file))){
529 $base = '';
530 }
531 }
532 if(empty($base)){
533 if( $url ) {
534 $base = NOTIFICATIONX_ASSETS;
535 }
536 else{
537 $base = NOTIFICATIONX_ASSETS_PATH;
538 }
539 }
540 return path_join($base, $file);
541 }
542
543
544 /**
545 * This function returns an array of post titles by searching the post type and the input value
546 *
547 * @param string $post_type The post type to search
548 * @param string|array $inputValue The input value to search by title or ID
549 * @param integer $numberposts The number of posts to return
550 * @return array An associative array of post IDs and titles
551 */
552 public static function get_post_titles_by_search($post_type, $inputValue = '', $numberposts = 10, $args = []) {
553 global $wpdb;
554 $product_list = [];
555 $numberposts = intval( $numberposts );
556 $args = wp_parse_args( $args, [
557 'prefix' => '',
558 ] );
559
560 // Generate a unique cache key based on the input parameters
561 $cache_key = 'get_post_titles_by_search_' . md5( $post_type . serialize( $inputValue ) . $numberposts );
562
563 // Try to get the cached data from the object cache
564 $cached_data = wp_cache_get( $cache_key );
565
566 // If the cached data exists and is not expired, return it
567 if ( false !== $cached_data ) {
568 return $cached_data;
569 }
570
571 // Otherwise, run the original query
572 // Start with the common part of the query
573 $sql = "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish'";
574 $query_args = array( $post_type );
575
576 if ( is_array( $inputValue ) && count( $inputValue ) ) {
577 // If the input value is an array of post IDs, use IN clause with placeholders
578 // Generate a string of placeholders like %d,%d,%d
579 $placeholders = implode( ',', array_fill( 0, count( $inputValue ), '%d' ) );
580
581 // Add the IN clause to the query
582 $sql .= " AND ID IN ($placeholders)";
583
584 // Merge the input values to the query arguments
585 $query_args = array_merge( $query_args, array_map( 'intval', $inputValue ) );
586 } else {
587 // If the input value is a string, use LIKE clause with placeholder
588 if ( ! empty( $inputValue ) ) {
589 // Add the LIKE clause to the query
590 $sql .= " AND post_title LIKE %s";
591
592 // Add the input value to the query arguments with wildcards
593 $query_args[] = '%' . $wpdb->esc_like( $inputValue ) . '%';
594 }
595 }
596
597 // Add order and limit clauses
598 $sql .= " ORDER BY post_date DESC LIMIT %d";
599
600 // Add the number of posts to the query arguments
601 $query_args[] = $numberposts;
602
603 // Prepare and execute the query using wpdb methods
604 // 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.
605 $sql = $wpdb->prepare( $sql, $query_args );
606 // 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.
607 $products = $wpdb->get_results( $sql );
608
609 if ( ! empty( $products ) ) {
610 // Loop through the results and build the output array
611 foreach ( $products as $product ) {
612 $key = $args['prefix'] . $product->ID;
613 $product_list[ $key ] = $product->post_title;
614 }
615 }
616
617 // Store the query result in the object cache with an expiration time of one hour
618 wp_cache_set( $cache_key, $product_list, '', MINUTE_IN_SECONDS );
619
620 // Return the query result
621 return $product_list;
622 }
623
624 /**
625 * Checks if WPML (WordPress Multilingual Plugin) is properly set up and loaded.
626 *
627 * @return bool True if WPML is set up, false otherwise.
628 */
629 public static function is_wpml_setup()
630 {
631 $wpml_has_run = get_option('WPML(TM-has-run)');
632 if (!empty($wpml_has_run['WPML\TM\ATE\Sitekey\Sync']) && did_action('wpml_loaded') && function_exists('load_wpml_st_basics')) {
633 return true;
634 }
635 return false;
636 }
637
638 /**
639 * Returns a list of common fields for GDPR cookie configuration settings.
640 *
641 * @return array An associative array of common GDPR cookie fields.
642 */
643 public static function gdpr_common_fields()
644 {
645 return [
646 'enabled' => array(
647 'type' => 'toggle',
648 'name' => 'enabled',
649 'label' => __('Enabled', 'notificationx'),
650 'priority' => 5,
651 ),
652 'discovered' => array(
653 'type' => 'toggle',
654 'name' => 'discovered',
655 'label' => __('Discovered', 'notificationx'),
656 'priority' => 5,
657 ),
658 'cookies_id' => array(
659 'type' => 'text',
660 'name' => 'cookies_id',
661 'label' => __('Cookie ID', 'notificationx'),
662 'priority' => 10,
663 ),
664 'domain' => array(
665 'type' => 'text',
666 'name' => 'domain',
667 'label' => __('Domain', 'notificationx'),
668 'priority' => 15,
669 ),
670 'duration' => array(
671 'type' => 'number',
672 'name' => 'duration',
673 'label' => __('Duration', 'notificationx'),
674 'min' => 1,
675 'priority' => 20,
676 'suggestions' => [
677 [
678 'value' => 30,
679 'unit' => 'days',
680 ],
681 [
682 'value' => 90,
683 'unit' => 'days',
684 ],
685 [
686 'value' => 180,
687 'unit' => 'days',
688 ],
689 [
690 'value' => 365,
691 'unit' => 'days',
692 ],
693 ],
694 ),
695 'description' => array(
696 'type' => 'textarea',
697 'name' => 'description',
698 'label' => __('Description', 'notificationx'),
699 'priority' => 30,
700 ),
701 'is_add_script' => array(
702 'type' => 'toggle',
703 'name' => 'is_add_script',
704 'label' => __('Add Script', 'notificationx'),
705 'priority' => 35,
706 ),
707 'load_inside' => array(
708 'label' => __('Add Script on', 'notificationx'),
709 'name' => 'product_control',
710 'type' => 'select',
711 'priority' => 40,
712 'default' => 'head',
713 'options' => GlobalFields::get_instance()->normalize_fields([
714 'head' => __('Header', 'notificationx'),
715 'body' => __('Body', 'notificationx'),
716 'footer' => __('Footer', 'notificationx'),
717 ]),
718 ),
719 'script_url_pattern' => array(
720 'type' => 'codeviewer',
721 'name' => 'script_url_pattern',
722 'label' => __('Script', 'notificationx'),
723 'priority' => 45,
724 ),
725 ];
726 }
727
728 /**
729 * Specifies the fields to be shown in the GDPR cookie list.
730 *
731 * @return array An array of field names visible in the GDPR cookie list.
732 */
733 public static function gdpr_cookie_list_visible_fields()
734 {
735 return ['cookies_id', 'domain', 'script_url_pattern', 'duration', 'load_inside','description'];
736 }
737
738 /**
739 * Deletes specific cookies on the server and returns a list of removed cookies.
740 *
741 * @return void Outputs a JSON-encoded list of removed cookies.
742 */
743 public static function delete_server_cookies()
744 {
745 $urlparts = wp_parse_url(site_url('/'));
746 $domain = preg_replace('/www\./i', '', $urlparts['host']);
747 $cookies_removed = array();
748 $d_domains = array('_ga', '_fbp', '_gid', '_gat', '__utma', '__utmb', '__utmc', '__utmt', '__utmz');
749 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
750 $d_domains = apply_filters('gdpr_d_domains_filter', $d_domains);
751
752 // Iterate over all cookies and remove them if they match specific conditions.
753 if (isset($_COOKIE) && is_array($_COOKIE) && $domain) :
754 foreach ($_COOKIE as $key => $value) {
755 if ($key !== 'moove_gdpr_popup' && strpos($key, 'woocommerce') === false && strpos($key, 'wc_') === false && strpos($key, 'wordpress') === false) :
756 if ('language' === $key || 'currency' === $key) {
757 setcookie($key, null, -1, '/', 'www.' . $domain);
758 $cookies_removed[$key] = $domain;
759 } elseif (in_array($key, $d_domains) || strpos($key, '_ga') !== false || strpos($key, '_fbp') !== false) {
760 setcookie($key, null, -1, '/', '.' . $domain);
761 $cookies_removed[$key] = $domain;
762 }
763 endif;
764 }
765 endif;
766
767 // Parse and remove cookies from the HTTP header.
768 $cookies = isset($_SERVER['HTTP_COOKIE']) ? explode(';', sanitize_text_field(wp_unslash($_SERVER['HTTP_COOKIE']))) : false;
769 if (is_array($cookies)) :
770 foreach ($cookies as $cookie) {
771 $parts = explode('=', $cookie);
772 $name = trim($parts[0]);
773 if ($name && $name !== 'moove_gdpr_popup' && strpos($name, 'woocommerce') === false && strpos($name, 'wc_') === false && strpos($name, 'wordpress') === false) :
774 setcookie($name, '', time() - 1000);
775 setcookie($name, '', time() - 1000, '/');
776 if ('language' === $name || 'currency' === $name) {
777 setcookie($name, null, -1, '/', 'www.' . $domain);
778 $cookies_removed[$name] = $domain;
779 } elseif (in_array($key, $d_domains) || strpos($name, '_ga') !== false || strpos($name, '_fbp') !== false) {
780 setcookie($name, null, -1, '/', '.' . $domain);
781 $cookies_removed[$name] = '.' . $domain;
782 } else {
783 setcookie($name, null, -1, '/');
784 $cookies_removed[$name] = $domain;
785 }
786 endif;
787 }
788 endif;
789
790 // Output the list of removed cookies as a JSON response.
791 echo json_encode($cookies_removed);
792 }
793
794 public static function tab_info_title($name, $title_default, $modal = false)
795 {
796 return [
797 'type' => 'text',
798 'name' => "{$name}_tab_title",
799 'default' => $title_default,
800 'label' => __('Name', 'notificationx'),
801 'autoFocus' => true,
802 ];
803 }
804
805 public static function tab_info_desc($name, $desc_default, $modal = false)
806 {
807 return [
808 'type' => 'textarea',
809 'row' => 3,
810 'name' => "{$name}_tab_desc",
811 'default' => $desc_default,
812 'label' => __('Description', 'notificationx'),
813 ];
814 }
815
816
817 public static function default_cookie_list()
818 {
819 return [
820 [
821 'enabled' => true,
822 'default' => true,
823 'cookies_id' => 'wordpress_logged_in',
824 'load_inside' => 'head',
825 'script_url_pattern' => '',
826 'description' => __('Indicates when a user is logged in and who they are, for most interface use.','notificationx'),
827 'index' => wp_generate_uuid4(),
828 ],
829 [
830 'enabled' => true,
831 'default' => true,
832 'cookies_id' => 'wordpress_sec',
833 'load_inside' => 'head',
834 'script_url_pattern' => '',
835 'description' => __('Used for security purposes for logged-in users.', 'notificationx'),
836 'index' => wp_generate_uuid4(),
837 ],
838 [
839 'enabled' => true,
840 'default' => true,
841 'cookies_id' => 'wp-settings-{user_id}',
842 'load_inside' => 'head',
843 'script_url_pattern' => '',
844 'description' => __('Used to persist a user\'s WordPress admin settings.','notificationx'),
845 'index' => wp_generate_uuid4(),
846 ],
847 [
848 'enabled' => true,
849 'default' => true,
850 'cookies_id' => 'wp-settings-time-{user_id}',
851 'load_inside' => 'head',
852 'script_url_pattern' => '',
853 'description' => __('Records the time that wp-settings-{user_id} was set.', 'notificationx'),
854 'index' => wp_generate_uuid4(),
855 ],
856 [
857 'enabled' => true,
858 'default' => true,
859 'cookies_id' => 'wp-settings-time-{user_id}',
860 'load_inside' => 'head',
861 'script_url_pattern' => '',
862 'description' => __('Records the time that wp-settings-{user_id} was set.', 'notificationx'),
863 'index' => wp_generate_uuid4(),
864 ],
865 [
866 'enabled' => true,
867 'default' => true,
868 'cookies_id' => 'nx_cookie_manager',
869 'script_url_pattern' => '',
870 'description' => __('Manages the cookies on the site, ensuring user consent for GDPR compliance.', 'notificationx'),
871 'index' => wp_generate_uuid4(),
872 ],
873 ];
874
875 }
876
877 // Helper function to get the image ID from data
878 public static function get_image_id_from_settings($data) {
879 return isset($data['image']['id']) ? $data['image']['id'] : null;
880 }
881
882 // Helper function to get custom image size
883 public static function get_custom_image_size() {
884 $default_size = '100_100'; // Default size
885 $image_size = (string) Settings::get_instance()->get('settings.notification_image_size', $default_size);
886 $image_size_parts = explode('_', $image_size);
887
888 if (!empty($image_size_parts[0]) && is_numeric($image_size_parts[0]) && !empty($image_size_parts[1]) && is_numeric($image_size_parts[1])) {
889 return [
890 'width' => (int) $image_size_parts[0],
891 'height' => (int) $image_size_parts[1],
892 ];
893 }
894
895 return [
896 'width' => 100, // Default width
897 'height' => 100, // Default height
898 ];
899 }
900
901 // Helper function to get resized image URL
902 public static function get_resized_image_url($image_id, $custom_size) {
903 if (!$image_id || empty($custom_size['width']) || empty($custom_size['height'])) {
904 return null;
905 }
906
907 $image = wp_get_attachment_image_src(
908 $image_id,
909 [$custom_size['width'], $custom_size['height']],
910 true // Crop the image to exact dimensions
911 );
912
913 return $image && isset($image[0]) ? $image[0] : null;
914 }
915
916 public static function nx_allowed_html()
917 {
918 return [
919 'a' => [
920 'href' => [],
921 'title' => [],
922 'target' => [],
923 'rel' => [],
924 'class' => [],
925 'id' => [],
926 ],
927 'abbr' => [
928 'title' => [],
929 'class' => [],
930 ],
931 'style' => [],
932 'b' => [
933 'class' => [],
934 ],
935 'blockquote' => [
936 'cite' => [],
937 'class' => [],
938 ],
939 'br' => [],
940 'cite' => [
941 'class' => [],
942 ],
943 'code' => [
944 'class' => [],
945 ],
946 'del' => [
947 'datetime' => [],
948 'class' => [],
949 ],
950 'div' => [
951 'class' => [],
952 'id' => [],
953 'style' => [],
954 ],
955 'em' => [
956 'class' => [],
957 ],
958 'h1' => [
959 'class' => [],
960 'id' => [],
961 ],
962 'h2' => [
963 'class' => [],
964 'id' => [],
965 ],
966 'h3' => [
967 'class' => [],
968 'id' => [],
969 ],
970 'h4' => [
971 'class' => [],
972 'id' => [],
973 ],
974 'h5' => [
975 'class' => [],
976 'id' => [],
977 ],
978 'h6' => [
979 'class' => [],
980 'id' => [],
981 ],
982 'hr' => [
983 'class' => [],
984 ],
985 'i' => [
986 'class' => [],
987 ],
988 'img' => [
989 'src' => [],
990 'alt' => [],
991 'title' => [],
992 'width' => [],
993 'height' => [],
994 'class' => [],
995 'id' => [],
996 ],
997 'li' => [
998 'class' => [],
999 ],
1000 'ol' => [
1001 'class' => [],
1002 ],
1003 'p' => [
1004 'class' => [],
1005 'style' => [],
1006 ],
1007 'pre' => [
1008 'class' => [],
1009 ],
1010 'q' => [
1011 'cite' => [],
1012 'class' => [],
1013 ],
1014 'span' => [
1015 'class' => [],
1016 'style' => [],
1017 ],
1018 'strong' => [
1019 'class' => [],
1020 ],
1021 'table' => [
1022 'class' => [],
1023 'style' => [],
1024 ],
1025 'tbody' => [
1026 'class' => [],
1027 ],
1028 'td' => [
1029 'colspan' => [],
1030 'rowspan' => [],
1031 'class' => [],
1032 'style' => [],
1033 ],
1034 'tfoot' => [
1035 'class' => [],
1036 ],
1037 'th' => [
1038 'colspan' => [],
1039 'rowspan' => [],
1040 'scope' => [],
1041 'class' => [],
1042 'style' => [],
1043 ],
1044 'thead' => [
1045 'class' => [],
1046 ],
1047 'tr' => [
1048 'class' => [],
1049 ],
1050 'ul' => [
1051 'class' => [],
1052 ],
1053 ];
1054 }
1055 public static function generate_time_string($data) {
1056 $timeString = '';
1057 if (isset($data['display_from']) && intval($data['display_from']) > 0) {
1058 $timeString .= intval($data['display_from']) . ' days ';
1059 }
1060
1061 if (isset($data['display_from_hour']) && intval($data['display_from_hour']) > 0) {
1062 $timeString .= intval($data['display_from_hour']) . ' hours ';
1063 }
1064
1065 if (isset($data['display_from_minute']) && intval($data['display_from_minute']) > 0) {
1066 $timeString .= intval($data['display_from_minute']) . ' minutes ';
1067 }
1068
1069 if (!empty($timeString)) {
1070 $time = strtotime($timeString . ' ago');
1071 } else {
1072 $time = time(); // Default to current time if no valid inputs
1073 }
1074 return $time;
1075 }
1076
1077 /**
1078 * Get the current datetime based on the WordPress site's timezone.
1079 *
1080 * @return string Formatted datetime in 'Y-m-d H:i:s' format.
1081 */
1082 public static function nx_get_current_datetime() {
1083 // Get the WordPress timezone setting
1084 $timezone = get_option('timezone_string');
1085
1086 if (!$timezone) {
1087 // If timezone_string is empty, fallback to gmt_offset
1088 $gmt_offset = get_option('gmt_offset');
1089
1090 if ($gmt_offset !== false) {
1091 $timezone = timezone_name_from_abbr("", (int) $gmt_offset * 3600, false);
1092 }
1093
1094 // If timezone_name_from_abbr fails, manually handle GMT offsets
1095 if (!$timezone) {
1096 $timezone = sprintf('Etc/GMT%+d', -$gmt_offset); // Example: GMT+6 → Etc/GMT-6
1097 }
1098 }
1099
1100 try {
1101 $date = new \DateTime('now', new \DateTimeZone($timezone));
1102 return $date->format('Y-m-d H:i:s'); // Format as MySQL datetime
1103 } catch (\Exception $e) {
1104 // If an error occurs, return UTC time as a fallback
1105 return gmdate('Y-m-d H:i:s');
1106 }
1107 }
1108
1109 public static function nx_get_visitor_country_code() {
1110 // Country targeting now applies to ALL notification types, so a single page
1111 // load can evaluate several country-targeted notifications. Resolve the
1112 // visitor's country once per request and reuse it. We key the memo on the IP
1113 // and read it with array_key_exists (not isset) so an empty/failed result is
1114 // remembered too and never re-triggers the header/API lookups below within the
1115 // same request. An unknown country is always represented as an empty string.
1116 static $memo = [];
1117
1118 $ip = '';
1119 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
1120 $ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_CLIENT_IP']));
1121 } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1122 $ip = explode(',', sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR'])))[0];
1123 } else {
1124 $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
1125 }
1126 $ip = trim($ip);
1127
1128 // No usable IP (CLI/cron) or localhost: return an empty code. Callers treat an
1129 // unknown country as "don't filter" (fail-open), so country-targeted
1130 // notifications still show during local testing instead of silently
1131 // disappearing. Bailing here also avoids requesting
1132 // http://ip-api.com/json/?fields=countryCode with an empty IP, which would
1133 // resolve to the SERVER's country and cache nothing.
1134 if ($ip === '' || $ip === '127.0.0.1' || $ip === '::1') {
1135 return apply_filters('nx_visitor_country_code', '', $ip);
1136 }
1137
1138 if (array_key_exists($ip, $memo)) {
1139 return apply_filters('nx_visitor_country_code', $memo[$ip], $ip);
1140 }
1141
1142 // Let a host/CDN or custom resolver short-circuit the external lookup entirely.
1143 // e.g. Cloudflare sends CF-IPCountry; many hosts expose GEOIP_COUNTRY_CODE.
1144 $header_country = '';
1145 foreach (['HTTP_CF_IPCOUNTRY', 'GEOIP_COUNTRY_CODE', 'HTTP_X_COUNTRY_CODE'] as $h) {
1146 if (!empty($_SERVER[$h])) {
1147 $header_country = strtoupper(sanitize_text_field(wp_unslash($_SERVER[$h])));
1148 break;
1149 }
1150 }
1151 // 'XX'/'T1' are Cloudflare's "unknown"/Tor placeholders — ignore them.
1152 if ($header_country !== '' && !in_array($header_country, ['XX', 'T1'], true)) {
1153 $memo[$ip] = $header_country;
1154 return apply_filters('nx_visitor_country_code', $header_country, $ip);
1155 }
1156
1157 // Per-IP cache so we don't hit the external API on every page load. A
1158 // transient is used deliberately: WordPress serves transients from a
1159 // persistent object cache (Redis/Memcached) when one is available — so those
1160 // sites add zero wp_options rows — and transparently falls back to the options
1161 // table otherwise, so the country stays cached across requests on plain sites
1162 // too (which is what keeps us under ip-api's 45 req/min limit). A cached empty
1163 // string is a remembered "lookup failed" marker, distinct from a miss (false).
1164 $cache_key = 'nx_geo_' . md5($ip);
1165 $cached = get_transient($cache_key);
1166 if (false !== $cached) {
1167 $memo[$ip] = $cached;
1168 return apply_filters('nx_visitor_country_code', $cached, $ip);
1169 }
1170
1171 $country = '';
1172 $api_endpoint = apply_filters('nx_visitor_country_api', "http://ip-api.com/json/{$ip}?fields=countryCode", $ip);
1173 $response = wp_remote_get($api_endpoint, ['timeout' => 3]);
1174
1175 if (!is_wp_error($response) && (int) wp_remote_retrieve_response_code($response) === 200) {
1176 $data = json_decode(wp_remote_retrieve_body($response), true);
1177 if (isset($data['countryCode']) && $data['countryCode'] !== '') {
1178 $country = $data['countryCode'];
1179 }
1180 }
1181
1182 // Cache successes for the full TTL; negative-cache failures for a short window
1183 // so a rate-limit/timeout (ip-api's free tier is ~45 req/min keyed by the
1184 // SERVER IP, shared across all visitors) doesn't re-hit the API on every
1185 // subsequent page load while it recovers.
1186 $ttl = '' !== $country
1187 ? (int) apply_filters('nx_visitor_country_cache_ttl', 12 * HOUR_IN_SECONDS)
1188 : (int) apply_filters('nx_visitor_country_failed_cache_ttl', 5 * MINUTE_IN_SECONDS);
1189 if ($ttl > 0) {
1190 set_transient($cache_key, $country, $ttl);
1191 }
1192
1193 $memo[$ip] = $country;
1194 return apply_filters('nx_visitor_country_code', $country, $ip);
1195 }
1196
1197 public static function nx_get_all_country($search = '') {
1198 $countries = [
1199 'all' => __('All Countries', 'notificationx'),
1200 'AF' => __('Afghanistan', 'notificationx'),
1201 'AL' => __('Albania', 'notificationx'),
1202 'DZ' => __('Algeria', 'notificationx'),
1203 'AS' => __('American Samoa', 'notificationx'),
1204 'AD' => __('Andorra', 'notificationx'),
1205 'AO' => __('Angola', 'notificationx'),
1206 'AI' => __('Anguilla', 'notificationx'),
1207 'AQ' => __('Antarctica', 'notificationx'),
1208 'AG' => __('Antigua and Barbuda', 'notificationx'),
1209 'AR' => __('Argentina', 'notificationx'),
1210 'AM' => __('Armenia', 'notificationx'),
1211 'AW' => __('Aruba', 'notificationx'),
1212 'AU' => __('Australia', 'notificationx'),
1213 'AT' => __('Austria', 'notificationx'),
1214 'AZ' => __('Azerbaijan', 'notificationx'),
1215 'BS' => __('Bahamas', 'notificationx'),
1216 'BH' => __('Bahrain', 'notificationx'),
1217 'BD' => __('Bangladesh', 'notificationx'),
1218 'BB' => __('Barbados', 'notificationx'),
1219 'BY' => __('Belarus', 'notificationx'),
1220 'BE' => __('Belgium', 'notificationx'),
1221 'BZ' => __('Belize', 'notificationx'),
1222 'BJ' => __('Benin', 'notificationx'),
1223 'BM' => __('Bermuda', 'notificationx'),
1224 'BT' => __('Bhutan', 'notificationx'),
1225 'BO' => __('Bolivia', 'notificationx'),
1226 'BA' => __('Bosnia and Herzegovina', 'notificationx'),
1227 'BW' => __('Botswana', 'notificationx'),
1228 'BR' => __('Brazil', 'notificationx'),
1229 'BN' => __('Brunei', 'notificationx'),
1230 'BG' => __('Bulgaria', 'notificationx'),
1231 'BF' => __('Burkina Faso', 'notificationx'),
1232 'BI' => __('Burundi', 'notificationx'),
1233 'KH' => __('Cambodia', 'notificationx'),
1234 'CM' => __('Cameroon', 'notificationx'),
1235 'CA' => __('Canada', 'notificationx'),
1236 'CV' => __('Cape Verde', 'notificationx'),
1237 'CF' => __('Central African Republic', 'notificationx'),
1238 'TD' => __('Chad', 'notificationx'),
1239 'CL' => __('Chile', 'notificationx'),
1240 'CN' => __('China', 'notificationx'),
1241 'CO' => __('Colombia', 'notificationx'),
1242 'KM' => __('Comoros', 'notificationx'),
1243 'CG' => __('Congo (Brazzaville)', 'notificationx'),
1244 'CD' => __('Congo (Kinshasa)', 'notificationx'),
1245 'CR' => __('Costa Rica', 'notificationx'),
1246 'HR' => __('Croatia', 'notificationx'),
1247 'CU' => __('Cuba', 'notificationx'),
1248 'CY' => __('Cyprus', 'notificationx'),
1249 'CZ' => __('Czech Republic', 'notificationx'),
1250 'DK' => __('Denmark', 'notificationx'),
1251 'DJ' => __('Djibouti', 'notificationx'),
1252 'DM' => __('Dominica', 'notificationx'),
1253 'DO' => __('Dominican Republic', 'notificationx'),
1254 'EC' => __('Ecuador', 'notificationx'),
1255 'EG' => __('Egypt', 'notificationx'),
1256 'SV' => __('El Salvador', 'notificationx'),
1257 'GQ' => __('Equatorial Guinea', 'notificationx'),
1258 'ER' => __('Eritrea', 'notificationx'),
1259 'EE' => __('Estonia', 'notificationx'),
1260 'ET' => __('Ethiopia', 'notificationx'),
1261 'FJ' => __('Fiji', 'notificationx'),
1262 'FI' => __('Finland', 'notificationx'),
1263 'FR' => __('France', 'notificationx'),
1264 'GA' => __('Gabon', 'notificationx'),
1265 'GM' => __('Gambia', 'notificationx'),
1266 'GE' => __('Georgia', 'notificationx'),
1267 'DE' => __('Germany', 'notificationx'),
1268 'GH' => __('Ghana', 'notificationx'),
1269 'GR' => __('Greece', 'notificationx'),
1270 'GD' => __('Grenada', 'notificationx'),
1271 'GT' => __('Guatemala', 'notificationx'),
1272 'GN' => __('Guinea', 'notificationx'),
1273 'GW' => __('Guinea-Bissau', 'notificationx'),
1274 'GY' => __('Guyana', 'notificationx'),
1275 'HT' => __('Haiti', 'notificationx'),
1276 'HN' => __('Honduras', 'notificationx'),
1277 'HK' => __('Hong Kong', 'notificationx'),
1278 'HU' => __('Hungary', 'notificationx'),
1279 'IS' => __('Iceland', 'notificationx'),
1280 'IN' => __('India', 'notificationx'),
1281 'ID' => __('Indonesia', 'notificationx'),
1282 'IR' => __('Iran', 'notificationx'),
1283 'IQ' => __('Iraq', 'notificationx'),
1284 'IE' => __('Ireland', 'notificationx'),
1285 'IL' => __('Israel', 'notificationx'),
1286 'IT' => __('Italy', 'notificationx'),
1287 'JM' => __('Jamaica', 'notificationx'),
1288 'JP' => __('Japan', 'notificationx'),
1289 'JO' => __('Jordan', 'notificationx'),
1290 'KZ' => __('Kazakhstan', 'notificationx'),
1291 'KE' => __('Kenya', 'notificationx'),
1292 'KI' => __('Kiribati', 'notificationx'),
1293 'KR' => __('Korea, South', 'notificationx'),
1294 'KW' => __('Kuwait', 'notificationx'),
1295 'KG' => __('Kyrgyzstan', 'notificationx'),
1296 'LA' => __('Laos', 'notificationx'),
1297 'LV' => __('Latvia', 'notificationx'),
1298 'LB' => __('Lebanon', 'notificationx'),
1299 'LS' => __('Lesotho', 'notificationx'),
1300 'LR' => __('Liberia', 'notificationx'),
1301 'LY' => __('Libya', 'notificationx'),
1302 'LI' => __('Liechtenstein', 'notificationx'),
1303 'LT' => __('Lithuania', 'notificationx'),
1304 'LU' => __('Luxembourg', 'notificationx'),
1305 'MG' => __('Madagascar', 'notificationx'),
1306 'MW' => __('Malawi', 'notificationx'),
1307 'MY' => __('Malaysia', 'notificationx'),
1308 'MV' => __('Maldives', 'notificationx'),
1309 'ML' => __('Mali', 'notificationx'),
1310 'MT' => __('Malta', 'notificationx'),
1311 'MH' => __('Marshall Islands', 'notificationx'),
1312 'MR' => __('Mauritania', 'notificationx'),
1313 'MU' => __('Mauritius', 'notificationx'),
1314 'MX' => __('Mexico', 'notificationx'),
1315 'FM' => __('Micronesia', 'notificationx'),
1316 'MD' => __('Moldova', 'notificationx'),
1317 'MC' => __('Monaco', 'notificationx'),
1318 'MN' => __('Mongolia', 'notificationx'),
1319 'ME' => __('Montenegro', 'notificationx'),
1320 'MA' => __('Morocco', 'notificationx'),
1321 'MZ' => __('Mozambique', 'notificationx'),
1322 'MM' => __('Myanmar (Burma)', 'notificationx'),
1323 'NA' => __('Namibia', 'notificationx'),
1324 'NR' => __('Nauru', 'notificationx'),
1325 'NP' => __('Nepal', 'notificationx'),
1326 'NL' => __('Netherlands', 'notificationx'),
1327 'NZ' => __('New Zealand', 'notificationx'),
1328 'NI' => __('Nicaragua', 'notificationx'),
1329 'NE' => __('Niger', 'notificationx'),
1330 'NG' => __('Nigeria', 'notificationx'),
1331 'MK' => __('North Macedonia', 'notificationx'),
1332 'NO' => __('Norway', 'notificationx'),
1333 'OM' => __('Oman', 'notificationx'),
1334 'PK' => __('Pakistan', 'notificationx'),
1335 'PW' => __('Palau', 'notificationx'),
1336 'PA' => __('Panama', 'notificationx'),
1337 'PG' => __('Papua New Guinea', 'notificationx'),
1338 'PY' => __('Paraguay', 'notificationx'),
1339 'PE' => __('Peru', 'notificationx'),
1340 'PH' => __('Philippines', 'notificationx'),
1341 'PL' => __('Poland', 'notificationx'),
1342 'PT' => __('Portugal', 'notificationx'),
1343 'QA' => __('Qatar', 'notificationx'),
1344 'RO' => __('Romania', 'notificationx'),
1345 'RU' => __('Russia', 'notificationx'),
1346 'RW' => __('Rwanda', 'notificationx'),
1347 'SA' => __('Saudi Arabia', 'notificationx'),
1348 'SN' => __('Senegal', 'notificationx'),
1349 'RS' => __('Serbia', 'notificationx'),
1350 'SC' => __('Seychelles', 'notificationx'),
1351 'SL' => __('Sierra Leone', 'notificationx'),
1352 'SG' => __('Singapore', 'notificationx'),
1353 'SK' => __('Slovakia', 'notificationx'),
1354 'SI' => __('Slovenia', 'notificationx'),
1355 'SB' => __('Solomon Islands', 'notificationx'),
1356 'SO' => __('Somalia', 'notificationx'),
1357 'ZA' => __('South Africa', 'notificationx'),
1358 'ES' => __('Spain', 'notificationx'),
1359 'LK' => __('Sri Lanka', 'notificationx'),
1360 'SD' => __('Sudan', 'notificationx'),
1361 'SR' => __('Suriname', 'notificationx'),
1362 'SE' => __('Sweden', 'notificationx'),
1363 'CH' => __('Switzerland', 'notificationx'),
1364 'SY' => __('Syria', 'notificationx'),
1365 'TW' => __('Taiwan', 'notificationx'),
1366 'TJ' => __('Tajikistan', 'notificationx'),
1367 'TZ' => __('Tanzania', 'notificationx'),
1368 'TH' => __('Thailand', 'notificationx'),
1369 'TG' => __('Togo', 'notificationx'),
1370 'TO' => __('Tonga', 'notificationx'),
1371 'TT' => __('Trinidad and Tobago', 'notificationx'),
1372 'TN' => __('Tunisia', 'notificationx'),
1373 'TR' => __('Turkey', 'notificationx'),
1374 'TM' => __('Turkmenistan', 'notificationx'),
1375 'UG' => __('Uganda', 'notificationx'),
1376 'UA' => __('Ukraine', 'notificationx'),
1377 'AE' => __('United Arab Emirates', 'notificationx'),
1378 'GB' => __('United Kingdom', 'notificationx'),
1379 'US' => __('United States', 'notificationx'),
1380 'UY' => __('Uruguay', 'notificationx'),
1381 'UZ' => __('Uzbekistan', 'notificationx'),
1382 'VU' => __('Vanuatu', 'notificationx'),
1383 'VE' => __('Venezuela', 'notificationx'),
1384 'VN' => __('Vietnam', 'notificationx'),
1385 'YE' => __('Yemen', 'notificationx'),
1386 'ZM' => __('Zambia', 'notificationx'),
1387 'ZW' => __('Zimbabwe', 'notificationx'),
1388 ];
1389 if (!empty($search)) {
1390 $search = strtolower($search);
1391 $countries = array_filter($countries, function($name) use ($search) {
1392 return strpos(strtolower($name), $search) !== false;
1393 });
1394 }
1395 return $countries;
1396 }
1397
1398
1399 /**
1400 * Delete a design document that NotificationX itself owns.
1401 *
1402 * The ID reaching the callers of this method arrives in a REST payload, so
1403 * it is attacker-controlled. Without a post-type check, any user holding
1404 * `edit_notificationx` could pass an arbitrary ID and force-delete any post
1405 * on the site -- pages, products, orders -- with no trash to recover from.
1406 * Only documents of a post type NotificationX creates may be removed here.
1407 *
1408 * A `current_user_can( 'delete_post' )` check is deliberately NOT applied.
1409 * These post types register with `capability_type => 'post'`, so that meta
1410 * cap resolves to the primitive `delete_posts`. A custom role delegated only
1411 * "Who Can Create Notification?" does not hold `delete_posts`, and gating on
1412 * it would stop that role from removing its own designs -- breaking exactly
1413 * the delegated workflow this boundary exists to support. Actor authority is
1414 * already established by the route's `edit_notificationx` permission
1415 * callback; what was missing, and what this restores, is object authority.
1416 *
1417 * @param int|string $post_id Candidate post ID, untrusted.
1418 * @param string $expected_type Post type NotificationX owns.
1419 * @return bool True when a post was deleted.
1420 */
1421 public static function delete_owned_post( $post_id, $expected_type ) {
1422 $post_id = absint( $post_id );
1423 if ( ! $post_id ) {
1424 return false;
1425 }
1426
1427 $post = get_post( $post_id );
1428 if ( ! $post || $expected_type !== $post->post_type ) {
1429 return false;
1430 }
1431
1432 return (bool) wp_delete_post( $post_id, true );
1433 }
1434
1435
1436 }
1437