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 +254 -2 3.2.14 → 3.3.3 View file →
@@ -41,13 +41,22 @@
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();
50 59 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reviewed for the NotificationX codebase: acceptable in this context.
51 60 if (!is_admin() || !empty($_GET['frontend'])) {
52 61 add_action('init', [$this, 'init'], 10);
53 62 }
@@ -66,13 +75,134 @@
66 75 add_filter('nx_fallback_data', [$this, 'fallback_data'], 10, 3);
67 76 add_filter('nx_filtered_data', [$this, 'filtered_data'], 9999, 3);
68 77 add_filter('nx_filtered_post', [$this, 'filtered_post'], 9999, 2);
69 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);
70 81 add_filter('body_class', [ $this, 'nx_add_body_class' ] );
82 + add_filter('style_loader_tag', [$this, 'non_blocking_style_tag'], 10, 2);
71 83
72 84 }
73 85
74 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 + /**
75 205 * This method is responsible for enqueueing scripts for public use.
76 206 *
77 207 * @return void
78 208 */
@@ -78,11 +208,15 @@
78 208 */
79 209 public function enqueue_scripts() {
80 210 $custom_css = $this->generate_custom_css();
81 211 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
82 - wp_register_script('notificationx-public', Helper::file('public/js/frontend.js', true), [], apply_filters('nx_frontend_js_version', NOTIFICATIONX_VERSION ), true);
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);
83 213 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Reviewed for the NotificationX codebase: acceptable in this context.
84 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');
85 219 // wp_register_style('notificationx-icon-pack', Helper::file('public/icon/style.css', true), [], NOTIFICATIONX_VERSION, 'all');
86 220 // Localize scripts for frontend
87 221 wp_localize_script(
88 222 'notificationx-public',
@@ -150,8 +284,12 @@
150 284 }
151 285 }
152 286
153 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 + }
154 292 wp_enqueue_script('notificationx-public');
155 293 wp_enqueue_style('dashicons');
156 294 do_action('notificationx_scripts', $this->notificationXArr);
157 295 wp_add_inline_style( 'notificationx-public', $custom_css );
@@ -161,13 +299,76 @@
161 299 // LATER
162 300 }
163 301 }
164 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 +
165 319 public function nx_add_body_class($classes) {
166 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 + }
167 325 return $classes;
168 326 }
169 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 +
170 371 private function separate_css($css) {
171 372 $media_css = '';
172 373 $normal_css = '';
173 374
@@ -194,9 +395,10 @@
194 395 public function generate_custom_css() {
195 396 $posts = Database::get_instance()->get_posts(Database::$table_posts, '*', ['enabled' => true] );
196 397 $combine_css = "";
197 398 foreach ($posts as $post) {
198 - 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'] ) ) {
199 401 $separatedCss = $this->separate_css($post['data']['add_custom_css']);
200 402 if( !empty( $post['data']['source'] ) && $post['data']['source'] == 'press_bar' ) {
201 403 $combine_css .= "{$separatedCss['normal_css']} {$separatedCss['media_css']} ";
202 404 } else if( !empty( $post['data']['source'] ) && $post['data']['source'] == 'gdpr_notification' ) {
@@ -229,11 +431,48 @@
229 431 public function localizeScripts() {
230 432 return [];
231 433 }
232 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 +
233 456 public function get_localize_data($data) {
234 457 $data['rest'] = REST::get_instance()->rest_data(false);
235 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 + }
236 475 $data['is_pro'] = false;
237 476 $data['gmt_offset'] = get_option('gmt_offset');
238 477 $data['lang'] = get_locale();
239 478 $data['common_assets'] = NOTIFICATIONX_COMMON_URL;
@@ -243,8 +482,12 @@
243 482 'queried_id' => get_queried_object_id(),
244 483 'pid' => !empty($GLOBALS['post']->ID) ? $GLOBALS['post']->ID : 0,
245 484 ];
246 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 + }
247 490 return $data;
248 491 }
249 492
250 493 public function get_notifications_data($params) {
@@ -273,8 +516,13 @@
273 516 'shortcode' => [],
274 517 'inline_shortcode' => false,
275 518 ]
276 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 + }
277 525 $global = $params['global'];
278 526 $active = $params['active'];
279 527 $pressbar = $params['pressbar'];
280 528 $gdpr = $params['gdpr'];
@@ -594,8 +842,10 @@
594 842 $bar_notifications[] = $return_posts ? $settings : $settings['nx_id'];
595 843 if (!empty($settings['elementor_id']) && class_exists('\Elementor\Plugin')) {
596 844 // @todo Find a function to only load css instead of building content.
597 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']);
598 848 }
599 849 } elseif($settings['source'] == 'gdpr_notification') {
600 850 $gdpr_notification[] = $return_posts ? $settings : $settings['nx_id'];
601 851 } elseif($settings['source'] == 'popup_notification') {
@@ -964,8 +1214,10 @@
964 1214 'is_elementor',
965 1215 'is_inline',
966 1216 'ld_course_list',
967 1217 'ld_product_control',
1218 + 'activecampaign_form',
1219 + 'brevo_list',
968 1220 'mailchimp_list',
969 1221 'max_stock',
970 1222 'nx-bar_with_elementor',
971 1223 'nx-bar_with_elementor-remove',