PluginProbe
Post Grid Gutenberg Blocks – PostX / 5.1.1
Post Grid Gutenberg Blocks – PostX v5.1.1
5.1.1 5.1.0 5.0.41 5.0.39 5.0.38 5.0.37 5.0.36 5.0.35 5.0.34 5.0.33 5.0.32 5.0.31 5.0.30 5.0.29 5.0.28 5.0.27 5.0.26 5.0.25 5.0.23 5.0.24 5.0.22 5.0.21 5.0.20 5.0.19 5.0.18 All 240 releases
← All changes | includes/notice/class-notice.php +193 -842 5.0.39 → 5.1.1 View file →
@@ -3,10 +3,18 @@
3 3
4 4 defined( 'ABSPATH' ) || exit;
5 5
6 6 use ULTP\Includes\Durbin\Xpo;
7 -use ULTP\Includes\Durbin\DurbinClient;
8 7
8 +require_once __DIR__ . '/interface-notice-type.php';
9 +require_once __DIR__ . '/class-notice-assets.php';
10 +require_once __DIR__ . '/class-abstract-campaign-notice.php';
11 +require_once __DIR__ . '/types/class-banner-notice.php';
12 +require_once __DIR__ . '/types/class-content-notice.php';
13 +require_once __DIR__ . '/types/class-image-notice.php';
14 +require_once __DIR__ . '/types/class-consent-notice.php';
15 +require_once __DIR__ . '/class-promo-links.php';
16 +
9 17 /**
10 18 * Plugin Notice
11 19 */
12 20 class Notice {
@@ -12,20 +20,20 @@
12 20 class Notice {
13 21
14 22
15 23 /**
16 - * Notice version
24 + * Campaigns showing on this request. Null until first worked out.
17 25 *
18 - * @var string
26 + * @var Abstract_Campaign_Notice[]|null
19 27 */
20 - private $notice_version = 'v5132';
28 + private $active_campaigns = null;
21 29
22 30 /**
23 - * Notice JS/CSS applied
31 + * Data-collection consent notice. Null until first needed.
24 32 *
25 - * @var boolean
33 + * @var Consent_Notice|null
26 34 */
27 - private $notice_js_css_applied = false;
35 + private $consent_notice = null;
28 36
29 37 /**
30 38 * Notice Priority
31 39 *
@@ -45,15 +53,17 @@
45 53 */
46 54 public function __construct() {
47 55 add_action( 'admin_notices', array( $this, 'admin_notices_callback' ) );
48 56 add_action( 'admin_init', array( $this, 'set_dismiss_notice_callback' ) );
57 + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
49 58
50 59 // REST API routes.
51 60 add_action( 'rest_api_init', array( $this, 'register_rest_route' ) );
52 61
53 - // Woocommerce Install Action
54 - // add_action( 'wp_ajax_ultp_install', array( $this, 'install_activate_plugin' ) ); // this ajax not called anywhere in future need to removed, that is arise patchstack security issue
55 62 add_filter( 'xpo_active_notice_lists', array( $this, 'handle_xpo_active_notice_lists' ), 99, 1 );
63 +
64 + // Upgrade link on the Plugins page and upgrade item in the PostX menu.
65 + new Promo_Links();
56 66 }
57 67
58 68 /**
59 69 * Registers REST API endpoints.
@@ -69,8 +79,20 @@
69 79 'callback' => array( $this, 'hello_bar_callback' ),
70 80 'permission_callback' => function () {
71 81 return current_user_can( 'manage_options' );
72 82 },
83 + 'args' => array(
84 + 'type' => array(
85 + 'type' => 'string',
86 + 'required' => true,
87 + 'enum' => array( 'hello_bar' ),
88 + ),
89 + 'id' => array(
90 + 'type' => 'string',
91 + 'required' => true,
92 + 'enum' => self::get_hellobar_keys(),
93 + ),
94 + ),
73 95 ),
74 96 );
75 97
76 98 foreach ( $routes as $route ) {
@@ -81,8 +103,9 @@
81 103 array(
82 104 'methods' => $route['methods'],
83 105 'callback' => $route['callback'],
84 106 'permission_callback' => $route['permission_callback'],
107 + 'args' => isset( $route['args'] ) ? $route['args'] : array(),
85 108 ),
86 109 )
87 110 );
88 111 }
@@ -93,17 +116,34 @@
93 116 *
94 117 * @return array
95 118 */
96 119 public static function get_hellobar_config() {
97 - return array(
98 - // Final hours sale.
99 - 'ultp_helloBar_summer_sale_2026_v1' => Xpo::get_transient_without_cache( 'ultp_helloBar_summer_sale_2026_v1' ),
100 - 'ultp_helloBar_summer_sale_2026_v1_2' => Xpo::get_transient_without_cache( 'ultp_helloBar_summer_sale_2026_v1_2' ),
101 - 'ultp_helloBar_base_price_2026' => Xpo::get_transient_without_cache( 'ultp_helloBar_base_price_2026' ),
102 - );
120 + $config = array();
121 + foreach ( self::get_hellobar_keys() as $key ) {
122 + $config[ $key ] = Xpo::get_transient_without_cache( $key );
123 + }
124 + return $config;
103 125 }
104 126
105 127 /**
128 + * Hello bar ids the dashboard may dismiss, from Campaigns::hellobar_keys().
129 + *
130 + * class-campaigns.php is edited for every sale, so it is loaded inside a try: a mistake in it
131 + * empties this list (hello bars can't be closed until it's fixed) instead of breaking wp-admin.
132 + *
133 + * @return string[]
134 + */
135 + private static function get_hellobar_keys() {
136 + try {
137 + require_once __DIR__ . '/class-campaigns.php';
138 + $keys = Campaigns::hellobar_keys();
139 + } catch ( \Throwable $e ) {
140 + return array();
141 + }
142 + return is_array( $keys ) ? array_values( array_filter( $keys, 'is_string' ) ) : array();
143 + }
144 +
145 + /**
106 146 * Handles Hello Bar dismissal action via REST API .
107 147 *
108 148 * @param \WP_REST_Request $request REST request object .
109 149 * @return \WP_REST_Response
@@ -108,13 +148,13 @@
108 148 * @param \WP_REST_Request $request REST request object .
109 149 * @return \WP_REST_Response
110 150 */
111 151 public function hello_bar_callback( \WP_REST_Request $request ) {
112 - $request_params = $request->get_params();
113 - $type = isset( $request_params['type'] ) ? $request_params['type'] : '';
114 - $id = isset( $request_params['id'] ) ? $request_params['id'] : '';
152 + $type = $request->get_param( 'type' );
153 + $id = $request->get_param( 'id' );
115 154
116 - if ( 'hello_bar' === $type && ! empty( $id ) ) {
155 + // The route's args already reject unknown ids; check again in case this is called directly.
156 + if ( 'hello_bar' === $type && is_string( $id ) && in_array( $id, self::get_hellobar_keys(), true ) ) {
117 157 Xpo::set_transient_without_cache( $id, 'hide', 1296000 );
118 158 }
119 159
120 160 return new \WP_REST_Response(
@@ -136,19 +176,11 @@
136 176 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['wpnonce'] ?? '' ) ), 'ultp-nonce' ) ) {
137 177 return;
138 178 }
139 179
140 - $durbin_key = sanitize_text_field( wp_unslash( $_GET['ultp_durbin_key'] ?? '' ) );
141 -
142 180 // Durbin notice dismiss.
143 - if ( ! empty( $durbin_key ) ) {
144 - Xpo::set_transient_without_cache( 'ultp_durbin_notice_' . $durbin_key, 'off' );
181 + Consent_Notice::handle_dismiss_request();
145 182
146 - if ( 'get' === sanitize_text_field( wp_unslash( $_GET['ultp_get_durbin'] ?? '' ) ) ) {
147 - DurbinClient::send( DurbinClient::ACTIVATE_ACTION );
148 - }
149 - }
150 -
151 183 // Install notice dismiss.
152 184 $install_key = sanitize_text_field( wp_unslash( $_GET['ultp_install_key'] ?? '' ) );
153 185 if ( ! empty( $install_key ) ) {
154 186 Xpo::set_transient_without_cache( 'ultp_install_notice_' . $install_key, 'off' );
@@ -190,372 +222,45 @@
190 222
191 223 /**
192 224 * Dashboard Banner Notice
193 225 *
194 - * @return void
226 + * @param bool $return_bool True to only report whether a banner would show.
227 + * @return bool|void
195 228 */
196 229 public function ultp_dashboard_banner_notice( $return_bool = false ) {
197 - $ultp_db_nonce = wp_create_nonce( 'ultp-nonce' );
198 - $banner_notices = array(
199 - // FLash sale.
200 - array(
201 - 'key' => 'ultp_banner_flash_sale_2026_1',
202 - 'start' => '2026-05-18 00:00 Asia/Dhaka',
203 - 'end' => '2026-05-21 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka.
230 + return $this->render_campaigns( Banner_Notice::class, $return_bool );
231 + }
204 232
205 - 'brand_color' => '#0322ff',
206 -
207 - 'left_image' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale/offer.png',
208 - 'right_image' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale/btn.png',
209 - 'bg_image' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale/bg.png',
210 - 'text' => 'Deal ending soon',
211 - 'countdown_duration' => 259200, // Duration in seconds.
212 - // 'countdown_color' => '#0322ff',
213 - 'countdown_color' => '#3CF357',
214 - 'url' => Xpo::generate_utm_link(
215 - array(
216 - 'utmKey' => 'flash_sale_content',
217 - )
218 - ),
219 -
220 - 'visibility' => ! Xpo::is_lc_active(),
221 - ),
222 - // surprise sale
223 - array(
224 - 'key' => 'ultp_banner_surprise_sale_2026_1',
225 - 'start' => '2026-05-29 00:00 Asia/Dhaka',
226 - 'end' => '2026-06-01 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka.
227 -
228 - 'brand_color' => '#0322ff',
229 -
230 - 'left_image' => ULTP_URL . 'assets/img/dashboard_banner/surprise_sale/offer.png',
231 - 'right_image' => ULTP_URL . 'assets/img/dashboard_banner/surprise_sale/btn.png',
232 - 'bg_image' => ULTP_URL . 'assets/img/dashboard_banner/surprise_sale/bg.png',
233 - 'text' => 'Limited Time Offer',
234 - 'countdown_duration' => 259200, // Duration in seconds.
235 - // 'countdown_color' => '#0322ff',
236 - 'countdown_color' => '#3CF357',
237 - 'url' => Xpo::generate_utm_link(
238 - array(
239 - 'utmKey' => 'surprise_sale_content',
240 - )
241 - ),
242 -
243 - 'visibility' => ! Xpo::is_lc_active(),
244 - ),
245 - // massive sale
246 - array(
247 - 'key' => 'ultp_banner_massive_sale_2026_1',
248 - 'start' => '2026-06-17 00:00 Asia/Dhaka',
249 - 'end' => '2026-06-20 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka.
250 -
251 - 'brand_color' => '#0322ff',
252 -
253 - 'left_image' => ULTP_URL . 'assets/img/dashboard_banner/massive_sale/offer.png',
254 - 'right_image' => ULTP_URL . 'assets/img/dashboard_banner/massive_sale/btn.png',
255 - 'bg_image' => ULTP_URL . 'assets/img/dashboard_banner/massive_sale/bg.png',
256 - 'text' => 'Deal ending soon',
257 - 'countdown_duration' => 259200, // Duration in seconds.
258 - // 'countdown_color' => '#0322ff',
259 - 'countdown_color' => '#3CF357',
260 - 'url' => Xpo::generate_utm_link(
261 - array(
262 - 'utmKey' => 'massive_sale_content',
263 - )
264 - ),
265 -
266 - 'visibility' => ! Xpo::is_lc_active(),
267 - ),
268 - // Final hours sale
269 - array(
270 - 'key' => 'ultp_banner_final_hours_sale_2026_1',
271 - 'start' => '2026-06-28 00:00 Asia/Dhaka',
272 - 'end' => '2026-06-30 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka.
273 -
274 - 'brand_color' => '#0322ff',
275 -
276 - 'left_image' => ULTP_URL . 'assets/img/dashboard_banner/final_hours_sale/offer.png',
277 - 'right_image' => ULTP_URL . 'assets/img/dashboard_banner/final_hours_sale/btn.png',
278 - 'bg_image' => ULTP_URL . 'assets/img/dashboard_banner/final_hours_sale/bg.png',
279 - 'text' => 'Hurry Before It Ends!',
280 - 'countdown_duration' => 259200, // Duration in seconds.
281 - // 'countdown_color' => '#0322ff',
282 - 'countdown_color' => '#3CF357',
283 - 'url' => Xpo::generate_utm_link(
284 - array(
285 - 'utmKey' => 'final_hours_content',
286 - )
287 - ),
288 - 'visibility' => ! Xpo::is_lc_active(),
289 - ),
290 - );
291 - foreach ( $banner_notices as $notice ) {
292 - $notice_key = isset( $notice['key'] ) ? $notice['key'] : $this->notice_version;
293 -
294 - if ( isset( $_GET['ultp_notice'] ) && $notice_key === sanitize_text_field(wp_unslash($_GET['ultp_notice'])) ) { // phpcs:ignore
295 - continue;
233 + /**
234 + * Queue the stylesheets and scripts for the notices that will show on this page.
235 + *
236 + * Runs on admin_enqueue_scripts, before admin_notices, so the styles load in <head>.
237 + *
238 + * @return void
239 + */
240 + public function enqueue_assets() {
241 + if ( $this->is_available_for_notice() ) {
242 + foreach ( $this->get_active_campaigns() as $campaign ) {
243 + $campaign->enqueue_assets();
296 244 }
297 -
298 - $current_time = gmdate( 'U' );
299 - $notice_start = gmdate( 'U', strtotime( $notice['start'] ) );
300 - $notice_end = gmdate( 'U', strtotime( $notice['end'] ) );
301 - if ( $current_time >= $notice_start && $current_time <= $notice_end && $notice['visibility'] ) {
302 -
303 - $notice_transient = Xpo::get_transient_without_cache( 'ultp_get_pro_notice_' . $notice_key );
304 -
305 - if ( 'off' === $notice_transient ) {
306 - continue;
307 - }
308 - if ( $return_bool ) { // Early return for Other plugin notice.
309 - return true;
310 - }
311 - if ( ! $this->notice_js_css_applied ) {
312 - $this->ultp_banner_notice_js();
313 - $this->notice_js_css_applied = true;
314 - }
315 - $query_args = array(
316 - 'ultp_notice' => $notice_key,
317 - 'wpnonce' => $ultp_db_nonce,
318 - );
319 - if ( isset( $notice['repeat_interval'] ) && $notice['repeat_interval'] ) {
320 - $query_args['ultp_interval'] = $notice['repeat_interval'];
321 - }
322 - ?>
323 - <style type="text/css">
324 - .ultp-notice-wrapper.ultp-banner-notice {
325 - height: auto !important;
326 - padding: 0 !important;
327 - position: relative;
328 - box-sizing: border-box;
329 - background-repeat: no-repeat;
330 - background-size: cover;
331 - background-position: center;
332 - }
333 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-link {
334 - width: 100%;
335 - text-decoration: none;
336 - display: block;
337 - }
338 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content {
339 - display: flex;
340 - justify-content: space-between;
341 - align-items: center;
342 - max-width: 700px;
343 - margin: 0 auto;
344 - padding: 10px 16px;
345 - gap: 16px;
346 - }
347 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-side-image {
348 - display: block;
349 - max-width: 100%;
350 - height: auto;
351 - max-height: 32px;
352 - }
353 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-main .ultp-banner-main-text {
354 - color: #ffffff;
355 - }
356 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-main {
357 - display: flex;
358 - flex-direction: column;
359 - gap: 4px;
360 - align-items: center;
361 - justify-content: center;
362 - font-weight: 700;
363 - font-size: 18px;
364 - color: #333333;
365 - line-height: 1.2;
366 - text-align: center;
367 - }
368 -
369 - @media screen and (max-width: 1100px) {
370 - /* .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content {
371 - flex-direction: column;
372 - } */
373 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content .ultp-banner-main-text {
374 - display: none;
375 - }
376 - }
377 - @media screen and (max-width: 490px) {
378 - .ultp-notice-wrapper.ultp-banner-notice {
379 - display: none;
380 - }
381 - }
382 - @media screen and (max-width: 782px) {
383 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content {
384 - justify-content: center;
385 - padding: 12px 32px 12px 12px;
386 - }
387 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-main {
388 - font-size: 22px;
389 - line-height: 28px;
390 - }
391 - }
392 - @media screen and (max-width: 480px) {
393 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content {
394 - padding: 10px 32px 10px 10px;
395 - }
396 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-main {
397 - font-size: 18px;
398 - line-height: 24px;
399 - }
400 - }
401 - </style>
402 - <div
403 - class="ultp-notice-wrapper ultp-banner-notice notice"
404 - style="
405 - border-left: 3px solid <?php echo esc_attr( $notice['brand_color'] ); ?>;
406 - background-image: url('<?php echo esc_attr( $notice['bg_image'] ); ?>');
407 - ">
408 - <a
409 - class="wc-dismiss-notice dashicons dashicons-no-alt"
410 - style="
411 - position: absolute;
412 - top: 1px;
413 - right: 1px;
414 - border-radius: 50%;
415 - background-color: black;
416 - color: white;
417 - font-size: 14px;
418 - display: flex;
419 - align-items: center;
420 - justify-content: center;
421 - "
422 - aria-label="<?php esc_html_e( 'Close Banner', 'ultimate-post' ); ?>"
423 - href="<?php echo esc_url( add_query_arg( $query_args ) ); ?>">
424 - </a>
425 -
426 - <a class="ultp-banner-link" target="_blank" href="<?php echo esc_url( $notice['url'] ); ?>">
427 - <div class="ultp-banner-content">
428 - <img class="ultp-banner-side-image" loading="lazy" src="<?php echo esc_url( $notice['left_image'] ); ?>" />
429 - <div class="ultp-banner-main">
430 - <span class="ultp-banner-main-text">
431 - <?php echo esc_html( $notice['text'] ); ?>
432 - </span>
433 - <div
434 - class="ultp-notice-countdown"
435 - style="
436 - color: <?php echo esc_attr( $notice['countdown_color'] ); ?>;
437 - "
438 - data-notice-key="<?php echo esc_attr( $notice_key . '-countdown' ); ?>"
439 - data-duration="<?php echo esc_attr( $notice['countdown_duration'] ); ?>">
440 - 00:00:00:00
441 - </div>
442 - </div>
443 - <img class="ultp-banner-side-image" loading="lazy" src="<?php echo esc_url( $notice['right_image'] ); ?>" />
444 - </div>
445 - </a>
446 - </div>
447 - <?php
448 - }
449 245 }
246 + $consent = $this->get_consent_notice();
247 + if ( $consent->should_display() ) {
248 + $consent->enqueue_assets();
249 + }
450 250 }
451 251
452 252 /**
453 253 * Banner JS
454 254 *
255 + * @deprecated The countdown script is now the file assets/js/ultp-notice-countdown.js. This queues it.
256 + *
455 257 * @return void
456 258 */
457 259 public function ultp_banner_notice_js() {
458 - ?>
459 - <script type="text/javascript">
460 - jQuery(function($) {
461 - 'use strict';
462 -
463 - const storagePrefix = 'ultp_notice_countdown_';
464 -
465 - const formatCountdown = function(seconds) {
466 - const days = Math.floor(seconds / 86400);
467 - const hours = Math.floor((seconds % 86400) / 3600);
468 - const minutes = Math.floor((seconds % 3600) / 60);
469 - const secs = seconds % 60;
470 -
471 - return String(days).padStart(2, '0') + ':' + String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
472 - };
473 -
474 - const parseDurationToSeconds = function(duration) {
475 - if (typeof duration === 'number' && Number.isFinite(duration) && duration > 0) {
476 - return Math.floor(duration);
477 - }
478 -
479 - const durationString = String(duration || '').trim();
480 - if (/^\d+$/.test(durationString)) {
481 - return parseInt(durationString, 10);
482 - }
483 -
484 - return 0;
485 - };
486 -
487 - const nowInSeconds = function() {
488 - return Math.floor(Date.now() / 1000);
489 - };
490 -
491 - $('.ultp-notice-countdown').each(function() {
492 - const countdownElement = $(this);
493 - const noticeKey = String(countdownElement.data('noticeKey') || '');
494 - const duration = parseDurationToSeconds(countdownElement.data('duration'));
495 -
496 - if (!noticeKey || duration <= 0) {
497 - return;
498 - }
499 -
500 - const storageKey = storagePrefix + noticeKey;
501 - let endAt = 0;
502 -
503 - try {
504 - const storedDataRaw = window.localStorage.getItem(storageKey);
505 - if (storedDataRaw) {
506 - const storedData = JSON.parse(storedDataRaw);
507 - if (storedData && parseInt(storedData.duration, 10) === duration) {
508 - endAt = parseInt(storedData.endAt, 10) || 0;
509 - }
510 - }
511 - } catch (error) {
512 - endAt = 0;
513 - }
514 -
515 - const saveTimerState = function(nextEndAt) {
516 - try {
517 - window.localStorage.setItem(
518 - storageKey,
519 - JSON.stringify({
520 - endAt: nextEndAt,
521 - duration: duration,
522 - })
523 - );
524 - } catch (error) {
525 - // No-op.
526 - }
527 - };
528 -
529 - const resetTimer = function(currentTime) {
530 - endAt = currentTime + duration;
531 - saveTimerState(endAt);
532 - };
533 -
534 - const tick = function() {
535 - const currentTime = nowInSeconds();
536 -
537 - if (endAt <= currentTime) {
538 - resetTimer(currentTime);
539 - }
540 -
541 - const remaining = Math.max(endAt - currentTime, 0);
542 - countdownElement.text(formatCountdown(remaining));
543 - };
544 -
545 - if (endAt <= nowInSeconds()) {
546 - resetTimer(nowInSeconds());
547 - }
548 -
549 - tick();
550 - window.setInterval(tick, 1000);
551 - });
552 - });
553 - </script>
554 - <?php
260 + Notice_Assets::enqueue_countdown_script();
555 261 }
556 262
557 -
558 263 /**
559 264 * The Durbin Html
560 265 *
561 266 * @return void
@@ -560,354 +265,33 @@
560 265 *
561 266 * @return void
562 267 */
563 268 public function ultp_dashboard_durbin_notice_callback() {
564 - $durbin_key = 'ultp_durbin_dc1';
565 -
566 - if (
567 - isset( $_GET['ultp_durbin_key'] ) || // phpcs:ignore
568 - 'off' === Xpo::get_transient_without_cache( 'ultp_durbin_notice_' . $durbin_key )
569 - ) {
570 - return;
269 + $consent = $this->get_consent_notice();
270 + if ( $consent->should_display() ) {
271 + $this->render_safely( $consent );
571 272 }
273 + }
572 274
573 - if ( ! $this->notice_js_css_applied ) {
574 - $this->notice_js_css_applied = true;
275 + /**
276 + * The data-collection consent notice, created once per request.
277 + *
278 + * @return Consent_Notice
279 + */
280 + private function get_consent_notice() {
281 + if ( null === $this->consent_notice ) {
282 + $this->consent_notice = new Consent_Notice();
575 283 }
576 -
577 - $ultp_db_nonce = wp_create_nonce( 'ultp-nonce' );
578 -
579 - ?>
580 - <style>
581 - .ultp-consent-box {
582 - width: 656px;
583 - padding: 16px !important;
584 - border: 1px solid #070707;
585 - border-left-width: 4px;
586 - border-radius: 4px;
587 - background-color: #fff;
588 - position: relative;
589 - width: 100%;
590 - box-sizing: border-box;
591 - }
592 - .ultp-consent-content {
593 - display: flex;
594 - justify-content: flex-start;
595 - align-items: flex-end;
596 - gap: 26px;
597 - }
598 -
599 - .ultp-consent-text-first {
600 - font-size: 14px;
601 - font-weight: 600;
602 - color: #070707;
603 - }
604 - .ultp-consent-text-last {
605 - margin: 4px 0 0;
606 - font-size: 14px;
607 - color: #070707;
608 - }
609 -
610 - .ultp-consent-accept {
611 - background-color: #070707;
612 - color: #fff !important;
613 - border: none;
614 - padding: 6px 10px;
615 - border-radius: 4px;
616 - cursor: pointer;
617 - font-size: 12px;
618 - font-weight: 600;
619 - text-decoration: none;
620 - }
621 - .ultp-consent-accept:hover {
622 - background-color:rgb(38, 38, 38);
623 - color: #fff;
624 - }
625 - </style>
626 - <div class="ultp-consent-box ultp-notice-wrapper notice data_collection_notice">
627 - <div class="ultp-consent-content">
628 - <div class="ultp-consent-text">
629 - <div class="ultp-consent-text-first"><?php esc_html_e( 'Want to help make PostX even more awesome?', 'ultimate-post' ); ?></div>
630 - <div class="ultp-consent-text-last">
631 - <?php esc_html_e( 'Allow us to collect diagnostic data and usage information. see ', 'ultimate-post' ); ?>
632 - <a href="https://www.wpxpo.com/data-collection-policy/" target="_blank" ><?php esc_html_e( 'what we collect.', 'ultimate-post' ); ?></a>
633 - </div>
634 - </div>
635 - <a
636 - class="ultp-consent-accept"
637 - href=
638 - <?php
639 - echo esc_url(
640 - add_query_arg(
641 - array(
642 - 'ultp_durbin_key' => $durbin_key,
643 - 'ultp_get_durbin' => 'get',
644 - 'wpnonce' => $ultp_db_nonce,
645 - )
646 - )
647 - );
648 - ?>
649 - class="ultp-notice-close"
650 - ><?php esc_html_e( 'Accept & Close', 'ultimate-post' ); ?></a>
651 - </div>
652 - <a href=
653 - <?php
654 - echo esc_url(
655 - add_query_arg(
656 - array(
657 - 'ultp_durbin_key' => $durbin_key,
658 - 'wpnonce' => $ultp_db_nonce,
659 - )
660 - )
661 - );
662 - ?>
663 - class="ultp-notice-close"
664 - style="
665 - position: absolute;
666 - right: 2px;
667 - top: 5px;
668 - text-decoration: unset;
669 - color: #b6b6b6;
670 - font-family: dashicons;
671 - font-size: 16px;
672 - font-style: normal;
673 - font-weight: 400;
674 - line-height: 20px;
675 - "
676 - >
677 - <span
678 - style="font-size: 14px;"
679 - class="ultp-notice-close-icon dashicons dashicons-dismiss"> </span></a>
680 - </div>
681 - <?php
284 + return $this->consent_notice;
682 285 }
683 286 /**
684 287 * Dashboard Content Notice
685 288 *
686 - * @return void
289 + * @param bool $return_bool True to only report whether a content notice would show.
290 + * @return bool|void
687 291 */
688 292 public function ultp_dashboard_content_notice( $return_bool = false ) {
689 - $content_notices = array(
690 - array(
691 - 'key' => 'ultp_dashboard_content_notice_summer_sale_2026_vv1',
692 - 'start' => '2026-07-06 00:00 Asia/Dhaka',
693 - 'end' => '2026-07-12 23:59 Asia/Dhaka',
694 - 'url' => Xpo::generate_utm_link(
695 - array(
696 - 'utmKey' => 'summer_db',
697 - )
698 - ),
699 - 'visibility' => ! Xpo::is_lc_active(),
700 - 'content_heading' => __( 'Summer Sale:', 'ultimate-post' ),
701 - /* translators: %s: discount percentage */
702 - 'content_subheading' => __( 'Enjoy up to %s on PostX Pro.', 'ultimate-post' ),
703 - 'discount_content' => ' 50% OFF',
704 - 'border_color' => '#0322ff',
705 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/brand_logo.svg',
706 - 'button_text' => __( 'Upgrade Now!', 'ultimate-post' ),
707 - 'is_discount_logo' => true,
708 - ),
709 - array(
710 - 'key' => 'ultp_dashboard_content_notice_summer_sale_2026_vv3',
711 - 'start' => '2026-08-02 00:00 Asia/Dhaka',
712 - 'end' => '2026-08-08 23:59 Asia/Dhaka',
713 - 'url' => Xpo::generate_utm_link(
714 - array(
715 - 'utmKey' => 'summer_db',
716 - )
717 - ),
718 - 'visibility' => ! Xpo::is_lc_active(),
719 - 'content_heading' => __( 'Summer Sale:', 'ultimate-post' ),
720 - /* translators: %s: discount percentage */
721 - 'content_subheading' => __( 'Enjoy up to %s on PostX Pro.', 'ultimate-post' ),
722 - 'discount_content' => ' 50% OFF',
723 - 'border_color' => '#0322ff',
724 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale_50_discount_logo.png',
725 - 'button_text' => __( 'Upgrade Now!', 'ultimate-post' ),
726 - 'is_discount_logo' => true,
727 - ),
728 -
729 - );
730 -
731 - $ultp_db_nonce = wp_create_nonce( 'ultp-nonce' );
732 -
733 - foreach ( $content_notices as $key => $notice ) {
734 - $notice_key = isset( $notice['key'] ) ? $notice['key'] : $this->notice_version;
735 - if ( isset( $_GET['ultp_notice'] ) && $notice_key === $_GET['ultp_notice'] ) {
736 - continue;
737 - } else {
738 - $border_color = $notice['border_color'];
739 -
740 - $current_time = gmdate( 'U' );
741 - $notice_start = gmdate( 'U', strtotime( $notice['start'] ) );
742 - $notice_end = gmdate( 'U', strtotime( $notice['end'] ) );
743 - if ( $current_time >= $notice_start && $current_time <= $notice_end && $notice['visibility'] ) {
744 - $notice_transient = Xpo::get_transient_without_cache( 'ultp_get_pro_notice_' . $notice_key );
745 -
746 - if ( 'off' !== $notice_transient ) {
747 - if ( $return_bool ) { // Early return for Other plugin notice.
748 - return true;
749 - }
750 - $query_args = array(
751 - 'ultp_notice' => $notice_key,
752 - 'wpnonce' => $ultp_db_nonce,
753 - );
754 - if ( isset( $notice['repeat_interval'] ) && $notice['repeat_interval'] ) {
755 - $query_args['ultp_interval'] = $notice['repeat_interval'];
756 - }
757 -
758 - $url = isset( $notice['url'] ) ? $notice['url'] : Xpo::generate_utm_link(
759 - array(
760 - 'utmKey' => 'content_notice',
761 - )
762 - );
763 -
764 - ?>
765 -
766 - <style id="ultp-notice-css" type="text/css">
767 - .ultp-content-notice-wrapper {
768 - border: 1px solid #c3c4c7;
769 - border-left: 3px solid #037fff;
770 - margin: 15px 0 !important;
771 - display: flex;
772 - align-items: center;
773 - background-color: #eef0f4;
774 - width: 100%;
775 - padding: 10px 0;
776 - position: relative;
777 - box-sizing: border-box;
778 - }
779 -
780 - .ultp-content-notice-wrapper.notice {
781 - margin: 10px 0;
782 - width: calc(100% - 20px);
783 - }
784 -
785 - .wrap .ultp-content-notice-wrapper.notice {
786 - width: 100%;
787 - }
788 -
789 - .ultp-content-notice-icon {
790 - margin-left: 15px;
791 - }
792 -
793 - .ultp-content-notice-discout-icon {
794 - margin-left: 10px;
795 - }
796 -
797 - .ultp-content-notice-icon img {
798 - max-width: 42px;
799 - height: 70px;
800 - }
801 -
802 - .ultp-content-notice-discout-icon img {
803 - height: 70px;
804 - width: 70px;
805 - object-fit: contain;
806 - }
807 -
808 - .ultp-notice-content-wrapper {
809 - display: flex;
810 - flex-direction: column;
811 - gap: 8px;
812 - font-size: 14px;
813 - line-height: 20px;
814 - margin-left: 15px;
815 - }
816 -
817 - .ultp-content-notice-buttons {
818 - display: flex;
819 - align-items: center;
820 - gap: 15px;
821 - }
822 -
823 - .ultp-content-notice-btn {
824 - font-weight: 600;
825 - text-transform: uppercase !important;
826 - padding: 2px 10px !important;
827 - background-color: #0322ff;
828 - border: none !important;
829 - }
830 -
831 - .ultp-content-discount_btn {
832 - background-color: #ffffff;
833 - text-decoration: none;
834 - border: 1px solid #0322ff;
835 - padding: 5px 10px;
836 - border-radius: 5px;
837 - font-weight: 500;
838 - text-transform: uppercase;
839 - color: #0322ff !important;
840 - }
841 -
842 - .ultp-content-notice-close {
843 - position: absolute;
844 - right: 2px;
845 - top: 5px;
846 - text-decoration: none;
847 - color: #b6b6b6;
848 - font-family: dashicons;
849 - font-size: 16px;
850 - line-height: 20px;
851 - }
852 -
853 - .ultp-content-notice-close-icon {
854 - font-size: 14px;
855 - }
856 - </style>
857 - <div class="ultp-content-notice-wrapper notice data_collection_notice"
858 - style="border-left: 3px solid <?php echo esc_attr( $border_color ); ?>;"
859 - >
860 - <?php
861 - if ( $notice['is_discount_logo'] ) {
862 - ?>
863 - <div class="ultp-content-notice-discout-icon"> <img src="<?php echo esc_url( $notice['icon'] ); ?>"/> </div>
864 - <?php
865 - } else {
866 - ?>
867 - <div class="ultp-content-notice-icon"> <img src="<?php echo esc_url( $notice['icon'] ); ?>"/> </div>
868 - <?php
869 - }
870 - ?>
871 -
872 - <div class="ultp-notice-content-wrapper">
873 - <div class="">
874 - <strong><?php printf( esc_html( $notice['content_heading'] ) ); ?> </strong>
875 - <?php
876 - printf(
877 - wp_kses_post( $notice['content_subheading'] ),
878 - '<strong>' . esc_html( $notice['discount_content'] ) . '</strong>'
879 - );
880 - ?>
881 - </div>
882 - <div class="ultp-content-notice-buttons">
883 - <?php if ( isset( $notice['is_discount_logo'] ) && $notice['is_discount_logo'] ) : ?>
884 - <a class="ultp-content-discount_btn" href="<?php echo esc_url( $url ); ?>" target="_blank">
885 - <?php echo esc_html( $notice['button_text'] ); ?>
886 - </a>
887 - <?php else : ?>
888 - <a class="ultp-content-notice-btn button button-primary" href="<?php echo esc_url( $url ); ?>" target="_blank" style="background-color: <?php echo ! empty( $notice['background_color'] ) ? esc_attr( $notice['background_color'] ) : '#86a62c'; ?>;">
889 - <?php echo esc_html( $notice['button_text'] ); ?>
890 -
891 - </a>
892 - <?php endif; ?>
893 - </div>
894 - </div>
895 - <a href=
896 - <?php
897 - echo esc_url(
898 - add_query_arg(
899 - $query_args
900 - )
901 - );
902 - ?>
903 - class="ultp-content-notice-close"><span class="ultp-content-notice-close-icon dashicons dashicons-dismiss"> </span></a>
904 - </div>
905 - <?php
906 - }
907 - }
908 - }
909 - }
293 + return $this->render_campaigns( Content_Notice::class, $return_bool );
910 294 }
911 295
912 296 /**
913 297 * Handle Plugin Notice for all plugins
@@ -915,9 +299,9 @@
915 299 * @param array $active_lists Lists of all active plugin notice.
916 300 * @return array
917 301 */
918 302 public function handle_xpo_active_notice_lists( $active_lists ) {
919 - if ( $this->ultp_dashboard_banner_notice( true ) || $this->ultp_dashboard_content_notice( true ) || $this->ultp_dashboard_image_banner_notice( true ) ) {
303 + if ( ! empty( $this->get_active_campaigns() ) ) {
920 304 $active_lists[ $this->plugin_notice_priority_key ] = $this->plugin_notice_priority;
921 305 }
922 306 return $active_lists;
923 307 }
@@ -935,142 +319,109 @@
935 319 asort( $active_notices );
936 320 return array_key_first( $active_notices ) === $this->plugin_notice_priority_key;
937 321 }
938 322
323 + /**
324 + * Dashboard Image Banner Notice
325 + *
326 + * @param bool $return_bool True to only report whether an image banner would show.
327 + * @return bool|void
328 + */
939 329 public function ultp_dashboard_image_banner_notice( $return_bool = false ) {
940 - $ultp_db_nonce = wp_create_nonce( 'ultp-nonce' );
941 - $banner_notices = array(
942 - array(
943 - 'key' => 'ultp_summer_sale_2612',
944 - 'start' => '2026-07-20 00:00 Asia/Dhaka', // format YY-MM-DD always set time 00:00 and zone Asia/Dhaka
945 - 'end' => '2026-08-01 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka
946 - 'banner_src' => ULTP_URL . 'assets/img/dashboard_banner/dashboard_banner_summer_sale.png',
947 - 'url' => Xpo::generate_utm_link(
948 - array(
949 - 'utmKey' => 'sub_menu_offer',
950 - )
951 - ),
952 - 'close_color' => '#000000',
953 - 'visibility' => ! Xpo::is_lc_active(),
954 - ),
955 - array(
956 - 'key' => 'ultp_summer_sale_2613',
957 - 'start' => '2026-08-09 00:00 Asia/Dhaka', // format YY-MM-DD always set time 00:00 and zone Asia/Dhaka
958 - 'end' => '2026-08-16 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka
959 - 'banner_src' => ULTP_URL . 'assets/img/dashboard_banner/dashboard_banner_summer_sale.png',
960 - 'url' => Xpo::generate_utm_link(
961 - array(
962 - 'utmKey' => 'sub_menu_offer',
963 - )
964 - ),
965 - 'close_color' => '#000000',
966 - 'visibility' => ! Xpo::is_lc_active(),
967 - ),
968 - array(
969 - 'key' => 'ultp_preco_sale_campaign_262_1',
970 - 'start' => '2026-08-23 00:00 Asia/Dhaka', // format YY-MM-DD always set time 00:00 and zone Asia/Dhaka
971 - 'end' => '2026-08-29 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka
972 - 'banner_src' => ULTP_URL . 'assets/img/dashboard_banner/preco_banner_insider_deal.png',
973 - 'url' => 'https://www.wpxpo.com/product/wowrecommend/?utm_source=db-postx-notice&utm_medium=insider-deal&utm_campaign=postx-dashboard/#pricing',
974 - 'close_color' => '#ffffff',
975 - 'visibility' => ! Xpo::is_lc_active(),
976 - ),
977 - array(
978 - 'key' => 'ultp_preco_sale_campaign_262_2',
979 - 'start' => '2026-08-30 00:00 Asia/Dhaka', // format YY-MM-DD always set time 00:00 and zone Asia/Dhaka.
980 - 'end' => '2026-09-19 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka.
981 - 'banner_src' => ULTP_URL . 'assets/img/dashboard_banner/preco_banner_early_bird.png',
982 - 'url' => 'https://www.wpxpo.com/product/wowrecommend/?utm_source=db-postx-notice&utm_medium=early-bird&utm_campaign=postx-dashboard/#pricing',
983 - 'close_color' => '#ffffff',
984 - 'visibility' => ! Xpo::is_lc_active(),
985 - ),
986 - );
330 + return $this->render_campaigns( Image_Notice::class, $return_bool );
331 + }
987 332
988 - foreach ( $banner_notices as $notice ) {
989 - $notice_key = isset( $notice['key'] ) ? $notice['key'] : $this->notice_version;
990 - if ( isset( $_GET['ultp_notice'] ) && $notice_key === sanitize_text_field(wp_unslash($_GET['ultp_notice'])) ) { // phpcs:ignore
991 - continue;
333 + /**
334 + * Campaigns that should show on this request, in display order.
335 + *
336 + * Worked out once per request: the xpo_active_notice_lists filter and the render pass both need it,
337 + * and each campaign's dismissal check is an uncached database read.
338 + *
339 + * @param string $type Optional class name to keep only one kind of campaign.
340 + * @return Abstract_Campaign_Notice[]
341 + */
342 + private function get_active_campaigns( $type = '' ) {
343 + if ( null === $this->active_campaigns ) {
344 + $this->active_campaigns = array();
345 +
346 + // The registry is the file edited for every sale. Load it here, not at the top, so a
347 + // mistake in it (even a syntax error) hides the campaigns instead of breaking wp-admin.
348 + try {
349 + require_once __DIR__ . '/class-campaigns.php';
350 + $campaigns = Campaigns::all();
351 + } catch ( \Throwable $e ) {
352 + self::log_error( $e );
353 + $campaigns = array();
992 354 }
993 355
994 - $current_time = gmdate( 'U' );
995 - $notice_start = gmdate( 'U', strtotime( $notice['start'] ) );
996 - $notice_end = gmdate( 'U', strtotime( $notice['end'] ) );
997 - if ( $current_time >= $notice_start && $current_time <= $notice_end && $notice['visibility'] ) {
356 + foreach ( $campaigns as $campaign ) {
357 + try {
358 + if ( $campaign instanceof Abstract_Campaign_Notice && $campaign->should_display() ) {
359 + $this->active_campaigns[] = $campaign;
360 + }
361 + } catch ( \Throwable $e ) {
362 + self::log_error( $e );
363 + }
364 + }
365 + }
998 366
999 - $notice_transient = Xpo::get_transient_without_cache( 'ultp_get_pro_notice_' . $notice_key );
367 + if ( '' === $type ) {
368 + return $this->active_campaigns;
369 + }
370 + return array_values(
371 + array_filter(
372 + $this->active_campaigns,
373 + function ( $campaign ) use ( $type ) {
374 + return $campaign instanceof $type;
375 + }
376 + )
377 + );
378 + }
1000 379
1001 - if ( 'off' === $notice_transient ) {
1002 - continue;
1003 - }
380 + /**
381 + * Print every live campaign of one kind, or report whether there is one.
382 + *
383 + * @param string $type Campaign class name.
384 + * @param bool $return_bool True to only report whether one would show.
385 + * @return bool|void
386 + */
387 + private function render_campaigns( $type, $return_bool ) {
388 + $campaigns = $this->get_active_campaigns( $type );
1004 389
1005 - if ( $return_bool ) { // Early return for Other plugin notice.
1006 - return true;
1007 - }
390 + if ( $return_bool ) { // Early return for Other plugin notice.
391 + return ! empty( $campaigns );
392 + }
1008 393
1009 - if ( ! $this->notice_js_css_applied ) {
1010 - $this->ultp_banner_notice_js();
1011 - $this->notice_js_css_applied = true;
1012 - }
1013 - $query_args = array(
1014 - 'ultp_notice' => $notice_key,
1015 - 'wpnonce' => $ultp_db_nonce,
1016 - );
1017 - if ( isset( $notice['repeat_interval'] ) && $notice['repeat_interval'] ) {
1018 - $query_args['ultp_interval'] = $notice['repeat_interval'];
1019 - }
1020 - ?>
1021 - <style type="text/css">
1022 - .ultp-notice-wrapper.ultp-image-notice-wrapper {
1023 - padding: 0 !important;
1024 - position: relative;
1025 - box-sizing: border-box;
1026 - overflow: hidden;
1027 - border-radius: 0px;
1028 - border: none !important;
1029 - }
1030 - .ultp-notice-wrapper.ultp-image-notice-wrapper .ultp-image-banner {
1031 - position: relative;
1032 - line-height: 0;
1033 - }
1034 - .ultp-notice-wrapper.ultp-image-notice-wrapper .ultp-btn-image {
1035 - display: block;
1036 - }
1037 - .ultp-notice-wrapper.ultp-image-notice-wrapper .ultp-btn-image img {
1038 - display: block;
1039 - width: 100%;
1040 - height: auto;
1041 - border-radius: 0;
1042 - }
1043 - .ultp-notice-wrapper.ultp-image-notice-wrapper .ultp-content-notice-close {
1044 - top: 4px;
1045 - right: 4px;
1046 - position: absolute;
1047 - z-index: 999;
1048 - }
1049 - @media screen and (max-width: 650px) {
1050 - .ultp-image-notice-wrapper {
1051 - display: none;
1052 - }
1053 - }
1054 - </style>
1055 - <div class="ultp-notice-wrapper ultp-image-notice-wrapper notice wc-install ultp-free-notice">
1056 - <div class="wc-install-body ultp-image-banner">
1057 - <a class="wc-dismiss-notice ultp-content-notice-close" href="
1058 - <?php
1059 - echo esc_url(
1060 - add_query_arg(
1061 - $query_args
1062 - )
1063 - );
1064 - ?>
1065 - "><span class="ultp-content-notice-close-icon dashicons dashicons-dismiss" style="color: <?php echo esc_attr( $notice['close_color'] ); ?>;"> </span></a>
1066 - <a class="ultp-btn-image" target="_blank" href="<?php echo esc_url( $notice['url'] ); ?>">
1067 - <img loading="lazy" src="<?php echo esc_url( $notice['banner_src'] ); ?>" alt="Discount Banner"/>
1068 - </a>
1069 - </div>
1070 - </div>
1071 -
1072 - <?php
1073 - }
394 + foreach ( $campaigns as $campaign ) {
395 + $this->render_safely( $campaign );
396 + }
397 + }
398 +
399 + /**
400 + * Print one notice. It's buffered, so a notice that fails halfway leaves no broken markup behind.
401 + *
402 + * @param Notice_Type $notice Notice to print.
403 + * @return void
404 + */
405 + private function render_safely( Notice_Type $notice ) {
406 + ob_start();
407 + try {
408 + $notice->render();
409 + ob_end_flush();
410 + } catch ( \Throwable $e ) {
411 + ob_end_clean();
412 + self::log_error( $e );
413 + }
414 + }
415 +
416 + /**
417 + * Record a notice failure when debugging. Notices are never worth breaking wp-admin for.
418 + *
419 + * @param \Throwable $e What went wrong.
420 + * @return void
421 + */
422 + private static function log_error( \Throwable $e ) {
423 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
424 + error_log( 'PostX notice: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only.
1074 425 }
1075 426 }
1076 427 }