PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / trunk
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar vtrunk
3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 All 156 releases
← All changes | includes/Core/Helper.php +1436 -0 0.2.5.7trunk View file →
@@ -1,0 +1,1436 @@
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 +}