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