PluginProbe ʕ •ᴥ•ʔ
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress / 5.6.10
Appointment Booking Plugin – LatePoint | Calendar & Scheduling for WordPress v5.6.10
5.6.10 5.6.9 5.6.8 5.6.7 5.6.6 5.6.5 5.6.4 5.6.3 5.6.2 5.6.1 5.6.0 5.5.2 5.5.1 5.5.0 5.4.2 trunk 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.1.91 5.1.92 5.1.93 5.1.94 5.2.0 5.2.1 5.2.10 5.2.11 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.0 5.3.1 5.3.2 5.4.0 5.4.1
latepoint / lib / kit / astra-notices / class-bsf-admin-notices.php
latepoint / lib / kit / astra-notices Last commit date
class-bsf-admin-notices.php 1 month ago notices.css 5 months ago notices.js 1 month ago
class-bsf-admin-notices.php
464 lines
1 <?php
2 /**
3 * BSF Admin Notices
4 *
5 * An easy to use PHP Library to add dismissible admin notices in the WordPress admin.
6 *
7 * @package BSF Admin Notices
8 * @since 1.2.0
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit; // Exit if accessed directly.
13 }
14
15 if ( ! class_exists( 'BSF_Admin_Notices' ) ) :
16
17 /**
18 * BSF_Admin_Notices
19 *
20 * Renamed from Astra_Notices. All runtime strings (AJAX action, nonce,
21 * script handles, JS globals, CSS classes, option keys, ID prefixes) are
22 * intentionally frozen at their original values so old plugin JS/CSS that
23 * is already shipped continues to work without updates.
24 *
25 * @since 1.2.0
26 */
27 class BSF_Admin_Notices {
28
29 /**
30 * Library version.
31 *
32 * @access private
33 * @var string
34 * @since 1.2.0
35 */
36 private static $version = '1.2.3';
37
38 /**
39 * Registered notices.
40 *
41 * @access private
42 * @var array
43 * @since 1.2.0
44 */
45 private static $notices = array();
46
47 /**
48 * Instance.
49 *
50 * @access private
51 * @var object Class object.
52 * @since 1.2.0
53 */
54 private static $instance;
55
56 /**
57 * Initiator.
58 *
59 * @since 1.2.0
60 * @return object initialized object of class.
61 */
62 public static function get_instance() {
63 if ( ! isset( self::$instance ) ) {
64 self::$instance = new self();
65 }
66 return self::$instance;
67 }
68
69 /**
70 * Constructor.
71 *
72 * @since 1.2.0
73 */
74 public function __construct() {
75 $this->maybe_migrate_notices_option();
76 add_action( 'admin_notices', array( $this, 'show_notices' ), 30 );
77 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
78 add_action( 'wp_ajax_astra-notice-dismiss', array( $this, 'dismiss_notice' ) );
79 add_filter( 'wp_kses_allowed_html', array( $this, 'add_data_attributes' ), 10, 2 );
80 }
81
82 /**
83 * Migrate allowed_astra_notices → astra_notices_allowed (one-time, on first load).
84 *
85 * @since 1.2.2
86 * @return void
87 */
88 private function maybe_migrate_notices_option() {
89 $old = get_option( 'allowed_astra_notices', false );
90 if ( false !== $old ) {
91 update_option( 'astra_notices_allowed', $old );
92 delete_option( 'allowed_astra_notices' );
93 }
94 }
95
96 /**
97 * Filters and returns a list of allowed tags and attributes for a given context.
98 *
99 * @param array $allowedposttags array of allowed tags.
100 * @param string $context Context type (explicit).
101 * @since 1.2.0
102 * @return array
103 */
104 public function add_data_attributes( $allowedposttags, $context ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
105 $allowedposttags['a']['data-repeat-notice-after'] = true;
106
107 return $allowedposttags;
108 }
109
110 /**
111 * Add Notice.
112 *
113 * @since 1.2.0
114 * @param array $args Notice arguments.
115 * @return void
116 */
117 public static function add_notice( $args = array() ) {
118 self::$notices[] = $args;
119
120 if ( ! isset( $args['id'] ) ) {
121 return;
122 }
123
124 $notice_id = sanitize_key( $args['id'] ); // Notice ID.
125 $notices = get_option( 'astra_notices_allowed', array() );
126 if ( ! in_array( $notice_id, $notices, true ) ) {
127 $notices[] = $notice_id; // Add notice id to the array.
128 update_option( 'astra_notices_allowed', $notices ); // Update the option.
129 }
130 }
131
132 /**
133 * Dismiss Notice.
134 *
135 * @since 1.2.0
136 * @return void
137 */
138 public function dismiss_notice() {
139 check_ajax_referer( 'astra-notices', 'nonce' );
140
141 $notice_id = ( isset( $_POST['notice_id'] ) ) ? sanitize_key( wp_unslash( $_POST['notice_id'] ) ) : '';
142 $repeat_notice_after = ( isset( $_POST['repeat_notice_after'] ) ) ? absint( wp_unslash( $_POST['repeat_notice_after'] ) ) : 0;
143 $notice = $this->get_notice_by_id( $notice_id );
144 $capability = isset( $notice['capability'] ) ? $notice['capability'] : 'manage_options';
145
146 $has_cap = current_user_can( $capability );
147
148 /**
149 * Filters whether the current user passes the capability check for notice dismissal.
150 *
151 * Both the legacy and new filter names are fired for backward compatibility.
152 * Filters can only restrict access (return false), never grant it — if the
153 * underlying current_user_can() check fails, filters cannot override to true.
154 */
155 $cap_check = apply_filters( 'astra_notices_user_cap_check', $has_cap );
156 $cap_check = apply_filters( 'bsf_admin_notices_user_cap_check', $cap_check );
157
158 if ( ! $has_cap || ! $cap_check ) {
159 wp_send_json_error( esc_html__( 'Permission denied.', 'astra-notices' ) );
160 }
161
162 $allowed_notices = get_option( 'astra_notices_allowed', array() ); // Get allowed notices.
163
164 // Define restricted user meta keys using the dynamic table prefix.
165 global $wpdb;
166 $wp_default_meta_keys = array(
167 $wpdb->prefix . 'capabilities',
168 $wpdb->prefix . 'user_level',
169 $wpdb->prefix . 'user-settings',
170 'account_status',
171 'session_tokens',
172 );
173
174 // if $notice_id does not start with astra-notices-id and notice_id is not from the allowed notices, then return.
175 if ( 0 !== strpos( $notice_id, 'astra-notices-id-' ) && ( ! in_array( $notice_id, $allowed_notices, true ) ) ) {
176 wp_send_json_error( esc_html__( 'Invalid notice ID.', 'astra-notices' ) );
177 }
178
179 // Valid inputs?
180 if ( ! empty( $notice_id ) ) {
181
182 if ( in_array( $notice_id, $wp_default_meta_keys, true ) ) {
183 wp_send_json_error( esc_html__( 'Invalid notice ID.', 'astra-notices' ) );
184 }
185
186 if ( ! empty( $repeat_notice_after ) ) {
187 set_transient( $notice_id, true, $repeat_notice_after );
188 } else {
189 update_user_meta( get_current_user_id(), $notice_id, 'notice-dismissed' );
190 }
191
192 wp_send_json_success();
193 }
194
195 wp_send_json_error();
196 }
197
198 /**
199 * Enqueue Scripts.
200 *
201 * @since 1.2.0
202 * @return void
203 */
204 public function enqueue_scripts() {
205 wp_register_style( 'bsf-astra-notices', self::get_url() . 'notices.css', array(), self::$version );
206 wp_register_script( 'bsf-astra-notices', self::get_url() . 'notices.js', array( 'jquery' ), self::$version, true );
207 wp_localize_script(
208 'bsf-astra-notices',
209 'bsfAstraNotices',
210 array(
211 '_notice_nonce' => wp_create_nonce( 'astra-notices' ),
212 )
213 );
214 }
215
216 /**
217 * Sort the notices based on the given priority of the notice.
218 * This function is called from usort()
219 *
220 * @since 1.2.0
221 * @param array $notice_1 First notice.
222 * @param array $notice_2 Second Notice.
223 * @return array
224 */
225 public function sort_notices( $notice_1, $notice_2 ) {
226 if ( ! isset( $notice_1['priority'] ) ) {
227 $notice_1['priority'] = 10;
228 }
229 if ( ! isset( $notice_2['priority'] ) ) {
230 $notice_2['priority'] = 10;
231 }
232
233 return $notice_1['priority'] - $notice_2['priority'];
234 }
235
236 /**
237 * Get all registered notices.
238 *
239 * @return array|null
240 */
241 private function get_notices() {
242 usort( self::$notices, array( $this, 'sort_notices' ) );
243
244 return self::$notices;
245 }
246
247 /**
248 * Get notice by notice_id.
249 *
250 * @param string $notice_id Notice id.
251 *
252 * @return array notice based on the notice id.
253 */
254 private function get_notice_by_id( $notice_id ) {
255 if ( empty( $notice_id ) ) {
256 return array();
257 }
258
259 $notices = $this->get_notices();
260 $notice = wp_list_filter(
261 $notices,
262 array(
263 'id' => $notice_id,
264 )
265 );
266
267 return ( ! empty( $notice ) && isset( $notice[0] ) ) ? $notice[0] : array();
268 }
269
270 /**
271 * Display the notices in the WordPress admin.
272 *
273 * @since 1.2.0
274 * @return void
275 */
276 public function show_notices() {
277 $defaults = array(
278 'id' => '', // Optional, Notice ID. If empty it set `astra-notices-id-<$array-index>`.
279 'type' => 'info', // Optional, Notice type. Default `info`. Expected [info, warning, notice, error].
280 'message' => '', // Optional, Message.
281 'show_if' => true, // Optional, Show notice on custom condition. E.g. 'show_if' => if( is_admin() ) ? true, false, .
282 'repeat-notice-after' => '', // Optional, Dismiss-able notice time. It'll auto show after given time.
283 'display-notice-after' => false, // Optional, Dismiss-able notice time. It'll auto show after given time.
284 'class' => '', // Optional, Additional notice wrapper class.
285 'priority' => 10, // Priority of the notice.
286 'display-with-other-notices' => true, // Should the notice be displayed if other notices are being displayed from BSF_Admin_Notices.
287 'is_dismissible' => true,
288 'capability' => 'manage_options', // User capability - This capability is required for the current user to see this notice.
289 );
290
291 // Count for the notices that are rendered.
292 $notices_displayed = 0;
293 $notices = $this->get_notices();
294
295 foreach ( $notices as $key => $notice ) {
296 $notice = wp_parse_args( $notice, $defaults );
297
298 // Show notices only for users with `manage_options` cap.
299 if ( ! current_user_can( $notice['capability'] ) ) {
300 continue;
301 }
302
303 $notice['id'] = self::get_notice_id( $notice, $key );
304 $notice['classes'] = self::get_wrap_classes( $notice );
305
306 // Notices visible after transient expire.
307 if ( isset( $notice['show_if'] ) && true === $notice['show_if'] ) {
308
309 // don't display the notice if it is not supposed to be displayed with other notices.
310 if ( 0 !== $notices_displayed && false === $notice['display-with-other-notices'] ) {
311 continue;
312 }
313
314 if ( self::is_expired( $notice ) ) {
315
316 self::markup( $notice );
317 ++$notices_displayed;
318 }
319 }
320 }
321 }
322
323 /**
324 * Render a notice.
325 *
326 * @since 1.2.0
327 * @param array $notice Notice markup.
328 * @return void
329 */
330 public static function markup( $notice = array() ) {
331 wp_enqueue_script( 'bsf-astra-notices' );
332 wp_enqueue_style( 'bsf-astra-notices' );
333
334 // Dual-emit: legacy (astra_notice_*) + new (bsf_admin_notice_*) hooks for backward compat.
335 // Note: consumers hooking BOTH names for the same event will be called twice.
336 do_action( 'astra_notice_before_markup' );
337 do_action( 'bsf_admin_notice_before_markup' );
338
339 do_action( "astra_notice_before_markup_{$notice['id']}" );
340 do_action( "bsf_admin_notice_before_markup_{$notice['id']}" );
341
342 ?>
343 <div id="<?php echo esc_attr( $notice['id'] ); ?>" class="<?php echo esc_attr( 'astra-notice-wrapper ' . $notice['classes'] ); ?>" data-repeat-notice-after="<?php echo esc_attr( $notice['repeat-notice-after'] ); ?>">
344 <div class="astra-notice-container">
345 <?php do_action( "astra_notice_inside_markup_{$notice['id']}" ); ?>
346 <?php do_action( "bsf_admin_notice_inside_markup_{$notice['id']}" ); ?>
347 <?php echo wp_kses_post( $notice['message'] ); ?>
348 </div>
349 </div>
350 <?php
351
352 do_action( "astra_notice_after_markup_{$notice['id']}" );
353 do_action( "bsf_admin_notice_after_markup_{$notice['id']}" );
354
355 do_action( 'astra_notice_after_markup' );
356 do_action( 'bsf_admin_notice_after_markup' );
357 }
358
359 /**
360 * Get wrapper classes for a notice.
361 *
362 * @since 1.2.0
363 *
364 * @param array $notice Notice arguments.
365 * @return array Notice wrapper classes.
366 */
367 private static function get_wrap_classes( $notice ) {
368 $classes = array( 'astra-notice', 'notice' );
369
370 if ( $notice['is_dismissible'] ) {
371 $classes[] = 'is-dismissible';
372 }
373
374 $classes[] = $notice['class'];
375 if ( isset( $notice['type'] ) && '' !== $notice['type'] ) {
376 $classes[] = 'notice-' . $notice['type'];
377 }
378
379 return esc_attr( implode( ' ', $classes ) );
380 }
381
382 /**
383 * Get HTML ID for a given notice.
384 *
385 * @since 1.2.0
386 *
387 * @param array $notice Notice arguments.
388 * @param int $key Notice array index.
389 * @return string HTML if for the notice.
390 */
391 private static function get_notice_id( $notice, $key ) {
392 if ( isset( $notice['id'] ) && ! empty( $notice['id'] ) ) {
393 return $notice['id'];
394 }
395
396 return 'astra-notices-id-' . $key;
397 }
398
399 /**
400 * Check if the notice is expires.
401 *
402 * @since 1.2.0
403 *
404 * @param array $notice Notice arguments.
405 * @return boolean
406 */
407 private static function is_expired( $notice ) {
408 $transient_status = get_transient( $notice['id'] );
409
410 if ( false === $transient_status ) {
411
412 if ( isset( $notice['display-notice-after'] ) && false !== $notice['display-notice-after'] ) {
413
414 if ( 'delayed-notice' !== get_user_meta( get_current_user_id(), $notice['id'], true ) &&
415 'notice-dismissed' !== get_user_meta( get_current_user_id(), $notice['id'], true ) ) {
416 set_transient( $notice['id'], 'delayed-notice', $notice['display-notice-after'] );
417 update_user_meta( get_current_user_id(), $notice['id'], 'delayed-notice' );
418
419 return false;
420 }
421 }
422
423 // Check the user meta status if current notice is dismissed or delay completed.
424 $meta_status = get_user_meta( get_current_user_id(), $notice['id'], true );
425
426 if ( empty( $meta_status ) || 'delayed-notice' === $meta_status ) {
427 return true;
428 }
429 }
430
431 return false;
432 }
433
434 /**
435 * Get base URL for the library assets.
436 *
437 * @return mixed URL.
438 */
439 public static function get_url() {
440 $path = wp_normalize_path( dirname( __FILE__ ) ); // phpcs:ignore Modernize.FunctionCalls.Dirname.FileConstant
441 $theme_dir = wp_normalize_path( get_template_directory() );
442
443 if ( false !== strpos( $path, $theme_dir ) ) {
444 return trailingslashit( get_template_directory_uri() . str_replace( $theme_dir, '', $path ) );
445 } else {
446 return plugin_dir_url( __FILE__ );
447 }
448 }
449 }
450
451 /**
452 * Kicking this off by calling 'get_instance()' method
453 */
454 BSF_Admin_Notices::get_instance();
455
456 endif;
457
458 // Backward compatibility alias for bsf-analytics library and third-party plugins
459 // that still reference the old class name. Safe to remove once all consumers
460 // are updated.
461 if ( ! class_exists( 'Astra_Notices' ) ) {
462 class_alias( 'BSF_Admin_Notices', 'Astra_Notices' ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.class_aliasFound
463 }
464