PluginProbe
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts / 2.7.1
Woody Code Snippets – Insert PHP, CSS, JS, and Header/Footer Scripts v2.7.1
2.7.7 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 All 28 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.1, at admin/includes/class.notices.php

427 lines 10.7 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
299 $data_attrs = '';
300 if ( $notice['dismissible'] ) {
301 $data_attrs = sprintf(
302 'data-notice-id="%s" data-nonce="%s"',
303 esc_attr( $notice['id'] ),
304 wp_create_nonce( 'winp_dismiss_notice_' . $notice['id'] )
305 );
306 }
307
308 printf(
309 '<div class="%s winp-admin-notice" %s>%s</div>',
310 esc_attr( $notice_class ),
311 $data_attrs, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
312 $notice['text'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
313 );
314 }
315
316 /**
317 * Check if notice is dismissed
318 *
319 * @param string $notice_id Notice ID.
320 *
321 * @return bool
322 */
323 private function is_notice_dismissed( $notice_id ) {
324 $dismissed_notices = get_option( 'winp_dismissed_notices', [] );
325
326 if ( ! is_array( $dismissed_notices ) || ! isset( $dismissed_notices[ $notice_id ] ) ) {
327 return false;
328 }
329
330 $expires = (int) $dismissed_notices[ $notice_id ];
331
332 // If expires is 0, dismissed forever.
333 // If expires is set and still in future, still dismissed.
334 return 0 === $expires || $expires > time();
335 }
336
337 /**
338 * Handle AJAX dismiss request
339 *
340 * @return void
341 */
342 public function ajax_dismiss_notice() {
343 $notice_id = isset( $_POST['notice_id'] ) ? sanitize_key( $_POST['notice_id'] ) : '';
344 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( $_POST['nonce'] ) : '';
345
346 if ( ! $notice_id || ! wp_verify_nonce( $nonce, 'winp_dismiss_notice_' . $notice_id ) ) {
347 wp_send_json_error();
348 }
349
350 if ( ! WINP_Plugin::app()->current_user_car() ) {
351 wp_send_json_error();
352 }
353
354 // Find the notice to get its expiration time.
355 $expire_time = 0; // Default: dismiss forever.
356 foreach ( self::$notices as $notice ) {
357 if ( $notice['id'] === $notice_id ) {
358 $expire_time = $notice['dismiss_expires'] > 0 ? time() + $notice['dismiss_expires'] : 0;
359 break;
360 }
361 }
362
363 // Get existing dismissed notices.
364 $dismissed_notices = get_option( 'winp_dismissed_notices', [] );
365 if ( ! is_array( $dismissed_notices ) ) {
366 $dismissed_notices = [];
367 }
368
369 // Clean up expired dismissals.
370 foreach ( $dismissed_notices as $id => $expires ) {
371 if ( 0 !== $expires && $expires < time() ) {
372 unset( $dismissed_notices[ $id ] );
373 }
374 }
375
376 // Limit size to prevent excessive growth (keep last 10 dismissals).
377 if ( count( $dismissed_notices ) > 10 ) {
378 // Sort by expiration time (oldest first).
379 asort( $dismissed_notices );
380 // Remove oldest entries beyond limit.
381 $dismissed_notices = array_slice( $dismissed_notices, -10, null, true );
382 }
383
384 // Add this dismissal.
385 $dismissed_notices[ $notice_id ] = $expire_time;
386
387 // Update option.
388 update_option( 'winp_dismissed_notices', $dismissed_notices, false );
389
390 wp_send_json_success();
391 }
392
393 /**
394 * Add notice to be displayed
395 *
396 * @param string $id ID of the notice.
397 * @param string|null $message Message of the notice.
398 * @param string $type Type of the notice. Possible values: 'error', 'warning', 'success', 'info'.
399 * @param bool $dismissible Whether the notice can be dismissed.
400 * @param int $dismiss_expires Time in seconds, after which the notice will be shown again. 0 - forever.
401 * @param array<string> $where Where to show the notice. Screen base values: 'post', 'post-new', 'edit', etc.
402 *
403 * @return void
404 */
405 public static function add_notice( $id, $message, $type = 'warning', $dismissible = true, $dismiss_expires = 0, $where = [] ) {
406 if ( is_null( $message ) || empty( $message ) ) {
407 return;
408 }
409
410 // Prevent duplicate notices.
411 foreach ( self::$notices as $notice ) {
412 if ( $notice['id'] === self::$prefix . $id ) {
413 return;
414 }
415 }
416
417 self::$notices[] = [
418 'id' => self::$prefix . $id,
419 'type' => $type,
420 'dismissible' => (bool) $dismissible,
421 'dismiss_expires' => (int) $dismiss_expires,
422 'where' => $where,
423 'text' => $message,
424 ];
425 }
426 }
427