PluginProbe
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts / 2.7.3
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts v2.7.3
2.7.6 2.7.5 2.7.4 trunk 1.3 2.0.4 2.0.6 2.1.91 2.2.4 2.2.7 2.2.9 2.3.1 2.3.10 2.4.10 2.4.2 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.6.0 2.6.1 2.7.0 2.7.1 All 27 releases
insert-php / admin / includes / class.notices.php

class.notices.php in Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts 2.7.3, at admin/includes/class.notices.php

452 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin notices system.
4 *
5 * @package Woody_Code_Snippets
6 */
7
8 // Exit if accessed directly
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Class WINP_Notices
15 */
16 class WINP_Notices {
17
18 /**
19 * Class instance.
20 *
21 * @var WINP_Notices
22 */
23 private static $instance = null;
24
25 /**
26 * Prefix.
27 *
28 * @var string
29 */
30 private static $prefix = 'winp_notice_';
31
32 /**
33 * Notices storage.
34 *
35 * @var array<int, array<string, mixed>> Notices data
36 */
37 private static $notices = [];
38
39 /**
40 * Private constructor to prevent direct instantiation.
41 */
42 private function __construct() {
43 add_action( 'admin_notices', [ $this, 'display_notices' ] );
44 add_action( 'wp_ajax_winp_dismiss_notice', [ $this, 'ajax_dismiss_notice' ] );
45 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
46 }
47
48 /**
49 * Get singleton instance
50 *
51 * @return WINP_Notices
52 */
53 public static function instance() {
54 if ( null === self::$instance ) {
55 self::$instance = new self();
56 }
57 return self::$instance;
58 }
59
60 /**
61 * Enqueue scripts for dismissible notices
62 *
63 * @return void
64 */
65 public function enqueue_scripts() {
66 // Only enqueue if there are notices to display.
67 if ( empty( self::$notices ) ) {
68 return;
69 }
70
71 // Add CSS for error notice styling.
72 wp_add_inline_style(
73 'common',
74 '.winp-admin-notice {
75 padding: 12px 16px !important;
76 }
77 .winp-admin-notice.is-dismissible {
78 padding-right: 38px !important;
79 }
80 .winp-admin-notice p { margin: 0.5em 0; }
81 .winp-admin-notice p:first-child { margin-top: 0; }
82 .winp-admin-notice p:last-child { margin-bottom: 0; }
83 .winp-error-notice-header {
84 margin-bottom: 10px;
85 font-size: 14px;
86 line-height: 1.5;
87 }
88 .winp-snippet-edit-link {
89 color: #2271b1;
90 text-decoration: none;
91 font-weight: 500;
92 border-bottom: 1px solid transparent;
93 transition: all 0.15s ease;
94 }
95 .winp-snippet-edit-link:hover {
96 color: #135e96;
97 border-bottom-color: #135e96;
98 }
99 .winp-snippet-edit-link:focus {
100 box-shadow: 0 0 0 2px #2271b1;
101 outline: 2px solid transparent;
102 border-radius: 2px;
103 }
104 .winp-admin-notice details {
105 margin: 10px 0 4px;
106 border: 1px solid rgba(0, 0, 0, 0.15);
107 border-radius: 4px;
108 background: #fff;
109 }
110 .winp-admin-notice details summary {
111 cursor: pointer;
112 font-weight: 500;
113 color: #2271b1;
114 padding: 10px 12px;
115 user-select: none;
116 font-size: 13px;
117 display: flex;
118 align-items: center;
119 transition: all 0.15s ease;
120 }
121 .winp-admin-notice details summary::-webkit-details-marker {
122 display: none;
123 }
124 .winp-admin-notice details summary::before {
125 content: "▶";
126 display: inline-block;
127 margin-right: 8px;
128 font-size: 10px;
129 transition: transform 0.2s ease;
130 }
131 .winp-admin-notice details[open] summary::before {
132 transform: rotate(90deg);
133 }
134 .winp-admin-notice details summary:hover {
135 color: #135e96;
136 background: rgba(0, 0, 0, 0.03);
137 }
138 .winp-admin-notice details[open] summary {
139 border-bottom: 1px solid rgba(0, 0, 0, 0.1);
140 background: rgba(0, 0, 0, 0.02);
141 }
142 .winp-error-details {
143 padding: 14px;
144 }
145 .winp-error-message,
146 .winp-error-location {
147 margin-bottom: 14px;
148 }
149 .winp-error-location {
150 margin-bottom: 0;
151 }
152 .winp-error-message strong,
153 .winp-error-location strong {
154 display: block;
155 margin-bottom: 6px;
156 font-size: 11px;
157 text-transform: uppercase;
158 color: #646970;
159 font-weight: 600;
160 letter-spacing: 0.5px;
161 }
162 .winp-error-message code {
163 display: block;
164 padding: 10px 12px;
165 background: #f6f7f7;
166 border-left: 3px solid #d63638;
167 font-size: 13px;
168 line-height: 1.6;
169 color: #d63638;
170 word-break: break-word;
171 font-family: Consolas, Monaco, monospace;
172 }
173 .winp-error-location {
174 font-size: 13px;
175 color: #2c3338;
176 line-height: 1.5;
177 }
178 .winp-line-number {
179 color: #646970;
180 font-style: italic;
181 }
182 .winp-error-notice-footer {
183 margin-top: 12px;
184 padding-top: 12px;
185 border-top: 1px solid rgba(0, 0, 0, 0.1);
186 display: flex;
187 align-items: center;
188 justify-content: space-between;
189 gap: 12px;
190 }
191 .winp-dismiss-help {
192 margin: 0 !important;
193 font-size: 13px;
194 color: #646970;
195 flex: 1;
196 }
197 .winp-manual-dismiss-btn {
198 background: #f0f0f1;
199 border-color: #2271b1;
200 color: #2271b1;
201 flex-shrink: 0;
202 font-weight: 500;
203 }
204 .winp-manual-dismiss-btn:hover {
205 background: #2271b1;
206 border-color: #2271b1;
207 color: #fff;
208 }'
209 );
210
211 wp_add_inline_script(
212 'common',
213 '(function($) {
214 // Handle X button dismiss
215 $(document).on("click", ".winp-admin-notice.is-dismissible .notice-dismiss", function(e) {
216 var notice = $(this).closest(".winp-admin-notice");
217 var noticeId = notice.data("notice-id");
218 var nonce = notice.data("nonce");
219
220 if (noticeId && nonce && typeof ajaxurl !== "undefined") {
221 $.post(ajaxurl, {
222 action: "winp_dismiss_notice",
223 notice_id: noticeId,
224 nonce: nonce
225 });
226 }
227 });
228
229 // Handle manual dismiss button
230 $(document).on("click", ".winp-manual-dismiss-btn", function(e) {
231 e.preventDefault();
232 var notice = $(this).closest(".winp-admin-notice");
233 var noticeId = notice.data("notice-id");
234 var nonce = notice.data("nonce");
235
236 if (noticeId && nonce && typeof ajaxurl !== "undefined") {
237 $(this).prop("disabled", true).text("Dismissing...");
238
239 $.post(ajaxurl, {
240 action: "winp_dismiss_notice",
241 notice_id: noticeId,
242 nonce: nonce
243 }).done(function() {
244 notice.fadeOut(300, function() {
245 $(this).remove();
246 });
247 }).fail(function() {
248 alert("Failed to dismiss notice. Please try again.");
249 $(this).prop("disabled", false).text("Dismiss Notice");
250 });
251 }
252 });
253 })(jQuery);'
254 );
255 }
256
257 /**
258 * Display admin notices
259 *
260 * @return void
261 */
262 public function display_notices() {
263 if ( ! WINP_Plugin::app()->current_user_car() ) {
264 return;
265 }
266
267 $current_screen = get_current_screen();
268
269 // Display all registered notices.
270 foreach ( self::$notices as $notice ) {
271 // Check if notice should be shown on current screen.
272 if ( ! empty( $notice['where'] ) && $current_screen ) {
273 if ( ! in_array( $current_screen->base, $notice['where'] ) ) {
274 continue;
275 }
276 }
277
278 // Check if notice is dismissed.
279 if ( $notice['dismissible'] && $this->is_notice_dismissed( $notice['id'] ) ) {
280 continue;
281 }
282
283 $this->render_notice( $notice );
284 }
285 }
286
287 /**
288 * Render a single notice
289 *
290 * @param array<string, mixed> $notice Notice data.
291 *
292 * @return void
293 */
294 private function render_notice( $notice ) {
295 $type = $notice['type'];
296 $dismissible = $notice['dismissible'] ? 'is-dismissible' : '';
297 $notice_class = "notice notice-{$type} {$dismissible}";
298 $notice_text = is_string( $notice['text'] ) ? $notice['text'] : '';
299
300 ?>
301 <div
302 class="<?php echo esc_attr( $notice_class ); ?> winp-admin-notice"
303 <?php if ( $notice['dismissible'] ) : ?>
304 data-notice-id="<?php echo esc_attr( $notice['id'] ); ?>"
305 data-nonce="<?php echo esc_attr( wp_create_nonce( 'winp_dismiss_notice_' . $notice['id'] ) ); ?>"
306 <?php endif; ?>
307 >
308 <?php echo wp_kses( $notice_text, $this->get_allowed_notice_html() ); ?>
309 </div>
310 <?php
311 }
312
313 /**
314 * Allowed HTML markup for notice messages.
315 *
316 * @return array<string, array<string, bool>>
317 */
318 private function get_allowed_notice_html() {
319 $allowed_html = wp_kses_allowed_html( 'post' );
320
321 $allowed_html['a']['class'] = true;
322 $allowed_html['a']['target'] = true;
323 $allowed_html['a']['rel'] = true;
324 $allowed_html['div']['class'] = true;
325 $allowed_html['span']['class'] = true;
326 $allowed_html['details'] = [
327 'class' => true,
328 'open' => true,
329 ];
330 $allowed_html['summary'] = [ 'class' => true ];
331 $allowed_html['code']['class'] = true;
332 $allowed_html['button'] = [
333 'class' => true,
334 'type' => true,
335 'disabled' => true,
336 ];
337
338 return $allowed_html;
339 }
340
341 /**
342 * Check if notice is dismissed
343 *
344 * @param string $notice_id Notice ID.
345 *
346 * @return bool
347 */
348 private function is_notice_dismissed( $notice_id ) {
349 $dismissed_notices = get_option( 'winp_dismissed_notices', [] );
350
351 if ( ! is_array( $dismissed_notices ) || ! isset( $dismissed_notices[ $notice_id ] ) ) {
352 return false;
353 }
354
355 $expires = (int) $dismissed_notices[ $notice_id ];
356
357 // If expires is 0, dismissed forever.
358 // If expires is set and still in future, still dismissed.
359 return 0 === $expires || $expires > time();
360 }
361
362 /**
363 * Handle AJAX dismiss request
364 *
365 * @return void
366 */
367 public function ajax_dismiss_notice() {
368 $notice_id = isset( $_POST['notice_id'] ) ? sanitize_key( $_POST['notice_id'] ) : '';
369 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( $_POST['nonce'] ) : '';
370
371 if ( ! $notice_id || ! wp_verify_nonce( $nonce, 'winp_dismiss_notice_' . $notice_id ) ) {
372 wp_send_json_error();
373 }
374
375 if ( ! WINP_Plugin::app()->current_user_car() ) {
376 wp_send_json_error();
377 }
378
379 // Find the notice to get its expiration time.
380 $expire_time = 0; // Default: dismiss forever.
381 foreach ( self::$notices as $notice ) {
382 if ( $notice['id'] === $notice_id ) {
383 $expire_time = $notice['dismiss_expires'] > 0 ? time() + $notice['dismiss_expires'] : 0;
384 break;
385 }
386 }
387
388 // Get existing dismissed notices.
389 $dismissed_notices = get_option( 'winp_dismissed_notices', [] );
390 if ( ! is_array( $dismissed_notices ) ) {
391 $dismissed_notices = [];
392 }
393
394 // Clean up expired dismissals.
395 foreach ( $dismissed_notices as $id => $expires ) {
396 if ( 0 !== $expires && $expires < time() ) {
397 unset( $dismissed_notices[ $id ] );
398 }
399 }
400
401 // Limit size to prevent excessive growth (keep last 10 dismissals).
402 if ( count( $dismissed_notices ) > 10 ) {
403 // Sort by expiration time (oldest first).
404 asort( $dismissed_notices );
405 // Remove oldest entries beyond limit.
406 $dismissed_notices = array_slice( $dismissed_notices, -10, null, true );
407 }
408
409 // Add this dismissal.
410 $dismissed_notices[ $notice_id ] = $expire_time;
411
412 // Update option.
413 update_option( 'winp_dismissed_notices', $dismissed_notices, false );
414
415 wp_send_json_success();
416 }
417
418 /**
419 * Add notice to be displayed
420 *
421 * @param string $id ID of the notice.
422 * @param string|null $message Message of the notice.
423 * @param string $type Type of the notice. Possible values: 'error', 'warning', 'success', 'info'.
424 * @param bool $dismissible Whether the notice can be dismissed.
425 * @param int $dismiss_expires Time in seconds, after which the notice will be shown again. 0 - forever.
426 * @param array<string> $where Where to show the notice. Screen base values: 'post', 'post-new', 'edit', etc.
427 *
428 * @return void
429 */
430 public static function add_notice( $id, $message, $type = 'warning', $dismissible = true, $dismiss_expires = 0, $where = [] ) {
431 if ( is_null( $message ) || empty( $message ) ) {
432 return;
433 }
434
435 // Prevent duplicate notices.
436 foreach ( self::$notices as $notice ) {
437 if ( $notice['id'] === self::$prefix . $id ) {
438 return;
439 }
440 }
441
442 self::$notices[] = [
443 'id' => self::$prefix . $id,
444 'type' => $type,
445 'dismissible' => (bool) $dismissible,
446 'dismiss_expires' => (int) $dismiss_expires,
447 'where' => $where,
448 'text' => $message,
449 ];
450 }
451 }
452