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/FrontEnd/FrontEnd.php +317 -8 3.2.7 → 3.3.3 View file →
@@ -41,13 +41,23 @@
41 41 const ASSET_PATH = NOTIFICATIONX_ASSETS_PATH . 'public/';
42 42 protected $notificationXArr = [];
43 43
44 44 /**
45 + * Block-editor bars whose block assets are already on this page, keyed by
46 + * the bar's `gutenberg_id` (see enqueue_gutenberg_bar_assets()).
47 + *
48 + * @var array<int, true>
49 + */
50 + protected $gutenberg_bar_assets = [];
51 +
52 + /**
45 53 * Initially Invoked
46 54 * when its initialized.
47 55 */
48 56 public function __construct() {
49 57 Analytics::get_instance();
58 + BarSpace::get_instance();
59 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reviewed for the NotificationX codebase: acceptable in this context.
50 60 if (!is_admin() || !empty($_GET['frontend'])) {
51 61 add_action('init', [$this, 'init'], 10);
52 62 }
53 63 add_filter('nx_frontend_localize_data', [$this, 'get_localize_data']);
@@ -65,13 +75,134 @@
65 75 add_filter('nx_fallback_data', [$this, 'fallback_data'], 10, 3);
66 76 add_filter('nx_filtered_data', [$this, 'filtered_data'], 9999, 3);
67 77 add_filter('nx_filtered_post', [$this, 'filtered_post'], 9999, 2);
68 78 add_action('wp_print_footer_scripts', [$this, 'footer_scripts']);
79 + // After wp_enqueue_scripts (wp_head priority 1) has collected the page's bars.
80 + add_action('wp_head', [$this, 'print_bar_reserve'], 3);
69 81 add_filter('body_class', [ $this, 'nx_add_body_class' ] );
82 + add_filter('style_loader_tag', [$this, 'non_blocking_style_tag'], 10, 2);
70 83
71 84 }
72 85
73 86 /**
87 + * Third-party stylesheets used by the notification themes, keyed by style
88 + * handle.
89 + *
90 + * They used to be CSS `@import`s at the top of frontend.css, which the
91 + * browser only discovers once frontend.css has downloaded: a serial,
92 + * render-blocking chain to two more origins on every page. They are now
93 + * enqueued next to `notificationx-public` and loaded without blocking the
94 + * first paint (see non_blocking_style_tag()).
95 + *
96 + * @since 3.3.3
97 + * @return array<string, string> Style handle => URL.
98 + */
99 + public function get_external_styles() {
100 + $styles = [];
101 + /**
102 + * Filters whether NotificationX loads Open Sans from Google Fonts.
103 + *
104 + * Return false if the site already loads Open Sans or must not
105 + * contact Google Fonts; the themes then fall back to sans-serif.
106 + *
107 + * @since 3.3.3
108 + * @param bool $load Default true.
109 + */
110 + if (apply_filters('notificationx_load_open_sans', true)) {
111 + $styles['notificationx-open-sans'] = 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap';
112 + }
113 + /**
114 + * Filters whether NotificationX loads FontAwesome 4.7 from cdnjs.
115 + *
116 + * The icons are drawn in pseudo-elements of a few themes only. Return
117 + * false if the site already loads FontAwesome 4.
118 + *
119 + * @since 3.3.3
120 + * @param bool $load Default true.
121 + */
122 + if (apply_filters('notificationx_load_fontawesome', true)) {
123 + $styles['notificationx-fontawesome-4'] = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css';
124 + }
125 + return $styles;
126 + }
127 +
128 + /**
129 + * Enqueue the third-party font and icon stylesheets (get_external_styles()).
130 + *
131 + * Call this wherever `notificationx-public` (or a stylesheet that bundles
132 + * the frontend themes) is enqueued.
133 + *
134 + * @since 3.3.3
135 + * @return void
136 + */
137 + public function enqueue_external_styles() {
138 + foreach ($this->get_external_styles() as $handle => $src) {
139 + // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Third-party URL; a ?ver= query would only split the CDN cache.
140 + wp_enqueue_style($handle, $src, [], null);
141 + }
142 + }
143 +
144 + /**
145 + * Load some stylesheets without blocking the first paint.
146 + *
147 + * - `notificationx-public` styles only what the runtime renders — bars,
148 + * popups and shortcodes all mount from REST data after the page has
149 + * loaded (the bar's reserved space is a separate inline style), and the
150 + * runtime waits for this sheet before its first render (whenStyled()).
151 + * At ~600 KB it was the largest render-blocking stylesheet on every page.
152 + * - `notificationx-gdpr-modal` styles only the cookie-preferences modal,
153 + * which opens on a click.
154 + * - The font and icon stylesheets from get_external_styles(): the fonts
155 + * swap in, and the FontAwesome glyphs are drawn only in pseudo-elements
156 + * of notifications the runtime renders after the page has loaded.
157 + *
158 + * The stylesheet is requested as `print` and switched to `all` once it has
159 + * loaded; the <noscript> copy keeps it for visitors without JavaScript.
160 + * `data-nx-style` marks it for the runtime, which also switches it (see
161 + * applyDeferredStyles() in external-styles.ts) in case the inline handler
162 + * was stripped by an optimizer or blocked by a Content Security Policy.
163 + *
164 + * @param string $tag The link tag.
165 + * @param string $handle Style handle.
166 + * @return string
167 + */
168 + public function non_blocking_style_tag($tag, $handle) {
169 + $handles = ['notificationx-public', 'notificationx-gdpr-modal', 'notificationx-open-sans', 'notificationx-fontawesome-4'];
170 + if (!in_array($handle, $handles, true) || false !== strpos($tag, 'onload=')) {
171 + return $tag;
172 + }
173 + $deferred = preg_replace('/\smedia=([\'"])all\1/', ' media=$1print$1 onload="this.media=\'all\'" data-nx-style=$1print$1', $tag, 1, $count);
174 + if (!$count) {
175 + return $tag;
176 + }
177 + return $deferred . '<noscript>' . trim($tag) . "</noscript>\n";
178 + }
179 +
180 + /**
181 + * Script dependencies of the `notificationx-public` runtime.
182 + *
183 + * `wp-hooks` provides `window.wp.hooks`, the registry the runtime reads its
184 + * frontend filters from (see nxdev/notificationx/frontend/core/hooks.ts and
185 + * docs/api/frontend-js-hooks.md). It is always kept, even if a filter drops
186 + * it, because without it every add-on filter silently stops firing.
187 + *
188 + * @since 3.3.3
189 + * @return string[]
190 + */
191 + public function get_script_dependencies() {
192 + /**
193 + * Filters the script dependencies of the `notificationx-public` runtime.
194 + *
195 + * @since 3.3.3
196 + * @param string[] $deps Script handles.
197 + */
198 + $deps = apply_filters( 'nx_frontend_script_deps', [ 'wp-hooks' ] );
199 + $deps = is_array( $deps ) ? array_filter( $deps, 'is_string' ) : [];
200 + array_unshift( $deps, 'wp-hooks' );
201 + return array_values( array_unique( $deps ) );
202 + }
203 +
204 + /**
74 205 * This method is responsible for enqueueing scripts for public use.
75 206 *
76 207 * @return void
77 208 */
@@ -76,10 +207,16 @@
76 207 * @return void
77 208 */
78 209 public function enqueue_scripts() {
79 210 $custom_css = $this->generate_custom_css();
80 - wp_register_script('notificationx-public', Helper::file('public/js/frontend.js', true), [], apply_filters('nx_frontend_js_version', NOTIFICATIONX_VERSION ), true);
211 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
212 + wp_register_script('notificationx-public', Helper::file('public/js/frontend.js', true), $this->get_script_dependencies(), apply_filters('nx_frontend_js_version', NOTIFICATIONX_VERSION ), true);
213 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
81 214 wp_register_style('notificationx-public', Helper::file('public/css/frontend.css', true), [], apply_filters('nx_frontend_css_version', NOTIFICATIONX_VERSION ), 'all');
215 + // GDPR cookie-customisation modal styles, split out of frontend.css
216 + // (~305 KB) so only pages with an active GDPR notice load them.
217 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
218 + wp_register_style('notificationx-gdpr-modal', Helper::file('public/css/gdpr-modal.css', true), ['notificationx-public'], apply_filters('nx_frontend_css_version', NOTIFICATIONX_VERSION ), 'all');
82 219 // wp_register_style('notificationx-icon-pack', Helper::file('public/icon/style.css', true), [], NOTIFICATIONX_VERSION, 'all');
83 220 // Localize scripts for frontend
84 221 wp_localize_script(
85 222 'notificationx-public',
@@ -113,12 +250,14 @@
113 250 )
114 251 );
115 252
116 253 $exit = false;
117 - if(isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'wp-admin/widgets.php') !== false){
254 + $referer = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
255 + if($referer && strpos($referer, 'wp-admin/widgets.php') !== false){
118 256 $exit = ['total' => 0];
119 257 }
120 258
259 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
121 260 $exit = apply_filters('nx_before_enqueue_scripts', $exit);
122 261 if(!empty($exit)){
123 262 $this->notificationXArr = $exit;
124 263 return;
@@ -123,8 +262,9 @@
123 262 $this->notificationXArr = $exit;
124 263 return;
125 264 }
126 265
266 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reviewed for the NotificationX codebase: acceptable in this context.
127 267 if (!$exit && empty($_GET['elementor-preview'])) {
128 268 $this->notificationXArr = $this->get_notifications_ids();
129 269 if ($this->notificationXArr['total'] > 0) {
130 270 $lang = get_locale();
@@ -144,8 +284,12 @@
144 284 }
145 285 }
146 286
147 287 wp_enqueue_style('notificationx-public');
288 + $this->enqueue_external_styles();
289 + if ( ! empty( $this->notificationXArr['gdpr'] ) ) {
290 + wp_enqueue_style('notificationx-gdpr-modal');
291 + }
148 292 wp_enqueue_script('notificationx-public');
149 293 wp_enqueue_style('dashicons');
150 294 do_action('notificationx_scripts', $this->notificationXArr);
151 295 wp_add_inline_style( 'notificationx-public', $custom_css );
@@ -155,13 +299,76 @@
155 299 // LATER
156 300 }
157 301 }
158 302
303 + /**
304 + * Reserve the top bar's measured height before it mounts (see BarSpace).
305 + *
306 + * @return void
307 + */
308 + public function print_bar_reserve() {
309 + // The builder preview passes preview data here, not bar IDs.
310 + if (!empty($this->notificationXArr['nxPreview']) || empty($this->notificationXArr['pressbar']) || !is_array($this->notificationXArr['pressbar'])) {
311 + return;
312 + }
313 + $ids = array_filter($this->notificationXArr['pressbar'], 'is_numeric');
314 + if ($ids) {
315 + BarSpace::get_instance()->print_reserve($ids);
316 + }
317 + }
318 +
159 319 public function nx_add_body_class($classes) {
160 320 $classes[] = 'has-notificationx';
321 + // Tells the runtime it need not fetch these bars' pages for their assets.
322 + foreach (array_keys($this->gutenberg_bar_assets) as $gutenberg_id) {
323 + $classes[] = 'nx-bar-assets-' . $gutenberg_id;
324 + }
161 325 return $classes;
162 326 }
163 327
328 + /**
329 + * Put a block-editor bar's block assets on the current page.
330 + *
331 + * The bar's HTML arrives over REST after load, where anything its blocks
332 + * enqueue is discarded. The runtime used to recover those assets by
333 + * fetching the bar's own permalink — a full themed page (~300 KB on
334 + * essential-blocks.com) — and loading each stylesheet and script it had
335 + * that this page did not, one after another, before the bar could settle.
336 + * Rendering the blocks here, while this page's assets are still being
337 + * collected, lets each block enqueue what it needs (e.g. a countdown's
338 + * frontend script) exactly as it would in post content; the output is
339 + * discarded. Mirrors the Elementor branch above.
340 + *
341 + * Runs for page loads only: REST and admin requests have no page to add
342 + * assets to.
343 + *
344 + * @param int|string $gutenberg_id The bar's block post ID.
345 + * @return void
346 + */
347 + protected function enqueue_gutenberg_bar_assets($gutenberg_id) {
348 + $gutenberg_id = absint($gutenberg_id);
349 + if (!$gutenberg_id || isset($this->gutenberg_bar_assets[$gutenberg_id]) || !doing_action('wp_enqueue_scripts')) {
350 + return;
351 + }
352 + // Same lookup as PressBar::print_bar_notice(), which renders the HTML the runtime shows.
353 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
354 + $post_id = apply_filters('wpml_object_id', $gutenberg_id, 'wp_block', true);
355 + $post = $post_id ? get_post($post_id) : null;
356 + if (!$post || '' === trim((string) $post->post_content)) {
357 + return;
358 + }
359 + // This runs inside <head> (wp_enqueue_scripts). A block whose render
360 + // callback echoes instead of returning would print into <head> and push
361 + // every later head tag into <body>; buffer and discard all output.
362 + ob_start();
363 + try {
364 + do_blocks($post->post_content);
365 + } finally {
366 + ob_end_clean();
367 + }
368 + $this->gutenberg_bar_assets[$gutenberg_id] = true;
369 + }
370 +
164 371 private function separate_css($css) {
165 372 $media_css = '';
166 373 $normal_css = '';
167 374
@@ -188,9 +395,10 @@
188 395 public function generate_custom_css() {
189 396 $posts = Database::get_instance()->get_posts(Database::$table_posts, '*', ['enabled' => true] );
190 397 $combine_css = "";
191 398 foreach ($posts as $post) {
192 - if( !empty( $post['data']['add_custom_css'] ) && !empty( $post['nx_id'] ) ) {
399 + // Raw rows (not normalize_post()), so check the type here.
400 + if( !empty( $post['data']['add_custom_css'] ) && is_string( $post['data']['add_custom_css'] ) && !empty( $post['nx_id'] ) ) {
193 401 $separatedCss = $this->separate_css($post['data']['add_custom_css']);
194 402 if( !empty( $post['data']['source'] ) && $post['data']['source'] == 'press_bar' ) {
195 403 $combine_css .= "{$separatedCss['normal_css']} {$separatedCss['media_css']} ";
196 404 } else if( !empty( $post['data']['source'] ) && $post['data']['source'] == 'gdpr_notification' ) {
@@ -205,8 +413,9 @@
205 413 }
206 414
207 415 public function footer_scripts() {
208 416 if (!empty($this->notificationXArr['total']) && $this->notificationXArr['total'] > 0) {
417 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
209 418 $this->notificationXArr = apply_filters('nx_frontend_localize_data', $this->notificationXArr);
210 419 ?>
211 420 <script data-no-optimize="1">
212 421 (function() {
@@ -222,11 +431,48 @@
222 431 public function localizeScripts() {
223 432 return [];
224 433 }
225 434
435 + /**
436 + * The URL WordPress prints for a registered stylesheet.
437 + *
438 + * @param string $handle Style handle.
439 + * @return string Empty when the handle has no source.
440 + */
441 + protected function style_url($handle) {
442 + $style = wp_styles()->query($handle, 'registered');
443 + if (!$style || empty($style->src) || !is_string($style->src)) {
444 + return '';
445 + }
446 + $src = $style->src;
447 + // Same version rule as WP_Styles::_css_href().
448 + $ver = null === $style->ver ? '' : ($style->ver ? $style->ver : get_bloginfo('version'));
449 + if ('' !== $ver) {
450 + $src = add_query_arg('ver', $ver, $src);
451 + }
452 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core filter, so CDN rewrites apply.
453 + return (string) apply_filters('style_loader_src', $src, $handle);
454 + }
455 +
226 456 public function get_localize_data($data) {
227 457 $data['rest'] = REST::get_instance()->rest_data(false);
228 458 $data['assets'] = self::ASSET_URL;
459 + if (empty($data['cross'])) {
460 + // The runtime re-adds these if an optimizer combined them into a
461 + // bundle that does not apply (e.g. a print-only one).
462 + $styles = [];
463 + foreach (['notificationx-public', 'notificationx-gdpr-modal'] as $handle) {
464 + if (wp_style_is($handle, 'enqueued') || wp_style_is($handle, 'done')) {
465 + $url = $this->style_url($handle);
466 + if ($url) {
467 + $styles[$handle] = $url;
468 + }
469 + }
470 + }
471 + if ($styles) {
472 + $data['styles'] = $styles;
473 + }
474 + }
229 475 $data['is_pro'] = false;
230 476 $data['gmt_offset'] = get_option('gmt_offset');
231 477 $data['lang'] = get_locale();
232 478 $data['common_assets'] = NOTIFICATIONX_COMMON_URL;
@@ -236,8 +482,12 @@
236 482 'queried_id' => get_queried_object_id(),
237 483 'pid' => !empty($GLOBALS['post']->ID) ? $GLOBALS['post']->ID : 0,
238 484 ];
239 485 $data['localeData'] = load_script_textdomain('notificationx-public', 'notificationx');
486 + if (!empty($data['cross'])) {
487 + // Cross-domain embeds have no WordPress enqueue: the runtime adds these.
488 + $data['external_styles'] = $this->get_external_styles();
489 + }
240 490 return $data;
241 491 }
242 492
243 493 public function get_notifications_data($params) {
@@ -266,8 +516,13 @@
266 516 'shortcode' => [],
267 517 'inline_shortcode' => false,
268 518 ]
269 519 );
520 + // These come straight from the public REST `notice` request, where
521 + // `?global=1` arrives as a string; the code below needs ID lists.
522 + foreach (['global', 'active', 'pressbar', 'gdpr', 'popup', 'exit_intent', 'shortcode'] as $list_key) {
523 + $params[$list_key] = NotificationX::get_instance()->normalize_multiple_value($params[$list_key]);
524 + }
270 525 $global = $params['global'];
271 526 $active = $params['active'];
272 527 $pressbar = $params['pressbar'];
273 528 $gdpr = $params['gdpr'];
@@ -306,8 +561,9 @@
306 561
307 562 $type = $settings['type'];
308 563 $source = $settings['source'];
309 564
565 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
310 566 $should_continue = apply_filters("nx_entry_show_on_frontend_$source", false, $entry, $settings);
311 567 if ( $should_continue ) {
312 568 continue;
313 569 }
@@ -319,8 +575,9 @@
319 575 if (!is_numeric($timestamp)) {
320 576 $entry['timestamp'] = $timestamp = is_string($timestamp) ? strtotime($timestamp) : false;
321 577 }
322 578 if ($timestamp && $display_from > $timestamp) {
579 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
323 580 if (apply_filters("nx_entry_display_$source", true, $entry, $settings)) {
324 581 continue;
325 582 }
326 583 }
@@ -325,19 +582,24 @@
325 582 }
326 583 }
327 584 }
328 585
586 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
329 587 $defaults = apply_filters("nx_fallback_data_$source", $_defaults, $entry, $settings);
588 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
330 589 $defaults = apply_filters('nx_fallback_data', $defaults, $entry, $settings);
331 590
332 591 $entry = $this->apply_defaults($entry, $defaults);
333 592 $entry['image_data'] = $this->get_image_url($entry, $settings);
334 593 if (!empty($entry['title'])) {
335 - $entry['title'] = strip_tags(html_entity_decode($entry['title']));
594 + $entry['title'] = wp_strip_all_tags(html_entity_decode($entry['title']));
336 595 }
337 596
597 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
338 598 $entry = apply_filters("nx_filtered_entry_$type", $entry, $settings);
599 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
339 600 $entry = apply_filters("nx_filtered_entry_$source", $entry, $settings);
601 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
340 602 $entry = apply_filters('nx_filtered_entry', $entry, $settings);
341 603 $entry = $this->link_url($entry, $settings, $params);
342 604
343 605 // @todo shortcode
@@ -372,14 +634,19 @@
372 634 }
373 635
374 636 foreach ($result as &$group) {
375 637 foreach ($group as &$value) {
638 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
376 639 $value['entries'] = apply_filters("nx_filtered_data_{$value['post']['type']}", $value['entries'], $value['post'], $params);
640 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
377 641 $value['entries'] = apply_filters("nx_filtered_data_{$value['post']['source']}", $value['entries'], $value['post'], $params);
642 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
378 643 $value['entries'] = apply_filters('nx_filtered_data', $value['entries'], $value['post'], $params);
644 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
379 645 $value['post'] = apply_filters('nx_filtered_post', $value['post'], $params);
380 646 }
381 647 }
648 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
382 649 $result = apply_filters('nx_filtered_notice', $result, $params);
383 650 }
384 651
385 652 if (!empty($pressbar)) {
@@ -398,8 +665,9 @@
398 665 continue;
399 666 }
400 667
401 668 // $settings['button_url'] = apply_filters("nx_notification_link_{$settings['source']}", $settings['button_url'], $settings);
669 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
402 670 $settings['button_url'] = apply_filters('nx_notification_link', $settings['button_url'], $settings);
403 671 if (!empty($settings['button_url']) && strpos($settings['button_url'], '//') === false && strpos($settings['button_url'], './') === false) {
404 672 $settings['button_url'] = "//{$settings['button_url']}";
405 673 }
@@ -404,8 +672,9 @@
404 672 $settings['button_url'] = "//{$settings['button_url']}";
405 673 }
406 674 $bar_content = $this->get_bar_content($settings, false, $params);
407 675 if ($bar_content !== '&nbsp;' || !empty($settings['enable_countdown'])) {
676 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
408 677 $settings = apply_filters('nx_filtered_post', $settings, $params);
409 678 $result['pressbar'][$_nx_id]['post'] = $settings;
410 679 $result['pressbar'][$_nx_id]['content'] = $bar_content;
411 680 }
@@ -421,8 +690,9 @@
421 690 if (!empty($_params['all_active'])) {
422 691 continue;
423 692 }
424 693
694 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
425 695 $settings = apply_filters('nx_filtered_post', $settings, $params);
426 696
427 697 $result['gdpr'][$_nx_id]['post'] = $settings;
428 698 $result['gdpr'][$_nx_id]['content'] = "";
@@ -438,8 +708,9 @@
438 708 if ( !$settings['enabled'] ) {
439 709 continue;
440 710 }
441 711
712 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
442 713 $settings = apply_filters('nx_filtered_post', $settings, $params);
443 714
444 715 $result['popup'][$_nx_id]['post'] = $settings;
445 716 $result['popup'][$_nx_id]['content'] = "";
@@ -454,8 +725,9 @@
454 725 $_nx_id = $settings['nx_id'];
455 726 if ( !$settings['enabled'] ) {
456 727 continue;
457 728 }
729 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
458 730 $settings = apply_filters('nx_filtered_post', $settings, $params);
459 731 $result['exit_intent'][$_nx_id]['post'] = $settings;
460 732 $result['exit_intent'][$_nx_id]['content'] = "";
461 733 unset($_nx_id);
@@ -467,8 +739,9 @@
467 739 }
468 740
469 741 public function get_settings(){
470 742
743 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
471 744 $branding_url = apply_filters('nx_branding_url', NOTIFICATIONX_PLUGIN_URL . '?utm_source=' . esc_url(home_url()) . '&utm_medium=notificationx');
472 745 $settings = [
473 746 'disable_powered_by' => Settings::get_instance()->get('settings.disable_powered_by'),
474 747 'affiliate_link' => $branding_url,
@@ -540,8 +813,9 @@
540 813 // if ($settings['hide_on_mobile'] && wp_is_mobile()) {
541 814 // continue;
542 815 // }
543 816
817 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
544 818 $show_on_exclude = apply_filters('nx_show_on_exclude', false, $settings);
545 819 if ($show_on_exclude) {
546 820 continue;
547 821 }
@@ -568,8 +842,10 @@
568 842 $bar_notifications[] = $return_posts ? $settings : $settings['nx_id'];
569 843 if (!empty($settings['elementor_id']) && class_exists('\Elementor\Plugin')) {
570 844 // @todo Find a function to only load css instead of building content.
571 845 \Elementor\Plugin::$instance->frontend->get_builder_content($settings['elementor_id'], false);
846 + } elseif (!empty($settings['gutenberg_id'])) {
847 + $this->enqueue_gutenberg_bar_assets($settings['gutenberg_id']);
572 848 }
573 849 } elseif($settings['source'] == 'gdpr_notification') {
574 850 $gdpr_notification[] = $return_posts ? $settings : $settings['nx_id'];
575 851 } elseif($settings['source'] == 'popup_notification') {
@@ -575,8 +851,17 @@
575 851 } elseif($settings['source'] == 'popup_notification') {
576 852 $popup_notifications[] = $settings['nx_id'];
577 853 } elseif($settings['source'] == 'exit_intent_custom') {
578 854 $exit_intent_notifications[] = $settings['nx_id'];
855 + // Force Elementor's frontend runtime + per-widget assets onto the
856 + // page (elementor-frontend.js, the document CSS, and each widget's
857 + // get_style_depends()/get_script_depends() — e.g. nx-countdown).
858 + // The popup HTML itself is rendered later via REST, where these
859 + // enqueues would be discarded, so the countdown widget would lose
860 + // its layout CSS and timer JS. Mirrors the press_bar branch above.
861 + if (!empty($settings['elementor_id']) && class_exists('\Elementor\Plugin')) {
862 + \Elementor\Plugin::$instance->frontend->get_builder_content($settings['elementor_id'], false);
863 + }
579 864 } elseif ($active_global_queue && NotificationX::is_pro()) {
580 865 $global_notifications[] = $return_posts ? $settings : $settings['nx_id'];
581 866 } else {
582 867 $active_notifications[] = $return_posts ? $settings : $settings['nx_id'];
@@ -588,8 +873,9 @@
588 873
589 874 // @todo maybe combine two hooks.
590 875
591 876 return apply_filters(
877 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
592 878 'get_notifications_ids',
593 879 [
594 880 'global' => $global_notifications,
595 881 'active' => $active_notifications,
@@ -625,8 +911,9 @@
625 911 // @todo need to pass url.
626 912 $check_location = Locations::get_instance()->check_location($locations, $custom_ids, $taxonomy_ids);
627 913 }
628 914
915 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
629 916 $check_location = apply_filters('nx_check_location', $check_location, $custom_ids, $show_on);
630 917
631 918 if ($show_on == 'on_selected') {
632 919 // show if the page is on selected
@@ -692,8 +979,9 @@
692 979 foreach ($ids as $id) {
693 980 if (!empty($notifications[$id])) {
694 981 $post = $notifications[$id];
695 982 $global_query = " nx_id = " . absint($id) . " AND source = '" . esc_sql($post['source']) . "'";
983 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
696 984 $_q = apply_filters("nx_get_entries_query_part_{$notifications[$id]['source']}",$global_query, $notifications[$id], $params );
697 985 $query[$id] = " (" . $_q . ")";
698 986 }
699 987 }
@@ -709,8 +997,9 @@
709 997 }
710 998 if (!is_array($entries)) {
711 999 $entries = [];
712 1000 }
1001 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
713 1002 $entries = apply_filters('nx_frontend_get_entries', $entries, $ids, $notifications,$params);
714 1003 return $entries;
715 1004 }
716 1005
@@ -729,9 +1018,11 @@
729 1018 if (empty($post['link_type']) || $post['link_type'] === 'none') {
730 1019 $link = '';
731 1020 }
732 1021
1022 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
733 1023 $link = apply_filters("nx_notification_link_{$post['source']}", $link, $post, $entry, $params);
1024 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
734 1025 $entry['link'] = apply_filters('nx_notification_link', $link, $post, $entry, $params);
735 1026 return $entry;
736 1027 }
737 1028
@@ -780,9 +1071,11 @@
780 1071 }
781 1072 }
782 1073
783 1074 $image_data['classes'] = $image_type;
1075 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
784 1076 $image_data = apply_filters("nx_notification_image_$source", $image_data, $data, $settings);
1077 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
785 1078 $image_data = apply_filters('nx_notification_image', $image_data, $data, $settings);
786 1079
787 1080 if (!empty($image_data['url'])) {
788 1081 return $image_data;
@@ -826,8 +1119,9 @@
826 1119 if (!empty($post['display_last']) && !in_array($post['source'], ['google', 'woo_inline', 'edd_inline', 'tutor_inline', 'learndash_inline', 'google_reviews', 'youtube','woocommerce_sales_inline','fluentcart_inline'])) {
827 1120 $entries = array_slice($entries, 0, $post['display_last']);
828 1121 }
829 1122 foreach ($entries as $index => $entry) {
1123 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
830 1124 $_entry = apply_filters("nx_frontend_keep_entry_{$post['source']}", [
831 1125 'nx_id' => $entry['nx_id'],
832 1126 'timestamp' => isset($entry['timestamp']) ? $entry['timestamp'] : Helper::current_timestamp($entry['updated_at']),
833 1127 'updated_at' => isset( $entry['updated_at'] ) ? $entry['updated_at'] : '',
@@ -832,16 +1126,27 @@
832 1126 'timestamp' => isset($entry['timestamp']) ? $entry['timestamp'] : Helper::current_timestamp($entry['updated_at']),
833 1127 'updated_at' => isset( $entry['updated_at'] ) ? $entry['updated_at'] : '',
834 1128 'image_data' => $entry['image_data'],
835 1129 'link' => $entry['link'],
1130 + // Location is rendered by themes (e.g. conv-theme-fifteen) via the
1131 + // hardcoded {{city_country}} tag, not through a notification-template
1132 + // param, so the template loop below never whitelists it. Keep the
1133 + // raw parts here so the frontend can compose "City, Country".
1134 + 'city' => $entry['city'] ?? '',
1135 + 'country' => $entry['country'] ?? '',
836 1136 ], $entry, $post, $params);
837 1137 if (!empty($params['inline_shortcode']) && isset($entry['product_id'])) {
838 1138 $_entry['product_id'] = $entry['product_id'];
839 1139 }
840 1140
841 - $template_arr = array_values($post['notification-template']);
842 - if ($post['template_adv']) {
843 - $adv_template = $post['advanced_template'];
1141 + // `notification-template` can be absent for some posts (e.g. advanced-
1142 + // template notifications or imported/migrated posts), so guard against
1143 + // passing null to array_values() — that is a fatal TypeError on PHP 8.
1144 + $template_arr = ( ! empty( $post['notification-template'] ) && is_array( $post['notification-template'] ) )
1145 + ? array_values( $post['notification-template'] )
1146 + : [];
1147 + if ( ! empty( $post['template_adv'] ) ) {
1148 + $adv_template = isset( $post['advanced_template'] ) ? $post['advanced_template'] : '';
844 1149 $pattern = "/{{(.+?)}}/i";
845 1150 if (preg_match_all($pattern, $adv_template, $matches)) {
846 1151 $template_arr = $matches[1];
847 1152 }
@@ -884,8 +1189,9 @@
884 1189 if (is_array($post) && empty($params['inline_shortcode']) && (!defined('NX_DEBUG') || !NX_DEBUG)) {
885 1190 $ignore_props = [
886 1191 'all_locations',
887 1192 'category_list',
1193 + 'combine_multiorder_display',
888 1194 'combine_multiorder_text',
889 1195 'content_trim_length',
890 1196 'convertkit_form',
891 1197 'currentTab',
@@ -908,8 +1214,10 @@
908 1214 'is_elementor',
909 1215 'is_inline',
910 1216 'ld_course_list',
911 1217 'ld_product_control',
1218 + 'activecampaign_form',
1219 + 'brevo_list',
912 1220 'mailchimp_list',
913 1221 'max_stock',
914 1222 'nx-bar_with_elementor',
915 1223 'nx-bar_with_elementor-remove',
@@ -956,14 +1264,15 @@
956 1264
957 1265 public function get_bar_content($settings, $suppress_filters = false, $params = []){
958 1266 $bar_content = PressBar::get_instance()->print_bar_notice($settings);
959 1267 if(!$suppress_filters){
1268 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
960 1269 $bar_content = apply_filters("nx_filtered_data_{$settings['source']}", $bar_content, $settings, $params);
961 1270 }
962 1271
963 1272 // checking if content is empty
964 1273 $_bar_content = str_replace(array("\r\n", "\n", "\r"), '', $bar_content);
965 - $_bar_content = trim(strip_tags($_bar_content));
1274 + $_bar_content = trim(wp_strip_all_tags($_bar_content));
966 1275 if (empty($_bar_content) && !empty($settings['enable_countdown'])) {
967 1276 $bar_content = '&nbsp;';
968 1277 }
969 1278 return $bar_content;