PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.7
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / window-notices.php

window-notices.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.7, at includes/window-notices.php

267 lines 9.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Window notices — declarative top-of-window banners.
4 *
5 * Plugins call `desktop_mode_register_window_notice()` to surface a
6 * tone-coded banner at the top of every window matching a `match`
7 * predicate (or every window by default). The shell renders the
8 * notice via the `<wpd-notice>` web component inside the window's
9 * `after-titlebar` slot, and records the user's dismissal in
10 * `localStorage` so the same notice never reappears.
11 *
12 * Notices are pure declarative data — no JS handle is needed.
13 *
14 * Example:
15 *
16 * ```php
17 * desktop_mode_register_window_notice( array(
18 * 'id' => 'my-plugin/welcome',
19 * 'tone' => 'info',
20 * 'message' => '<strong>Welcome!</strong> Read the <a href="…">docs</a>.',
21 * 'match' => array( 'window' => 'edit-php' ), // optional
22 * ) );
23 * ```
24 *
25 * @since 0.22.0
26 * @package DesktopMode
27 */
28
29 defined( 'ABSPATH' ) || exit;
30
31 /**
32 * Allowed tones — mirror the `<wpd-notice>` component's `tone`
33 * attribute.
34 *
35 * @since 0.22.0
36 * @return string[]
37 */
38 function desktop_mode_window_notice_tones() {
39 return array( 'info', 'success', 'warning', 'error', 'danger', 'neutral' );
40 }
41
42 /**
43 * Register (or replace) a declarative window notice.
44 *
45 * @since 0.22.0
46 *
47 * @param array $args {
48 * @type string $id Required. Persistence + dedupe key.
49 * Recommended format `<plugin>/<slug>`.
50 * @type string $message Required. HTML body. Passed through
51 * `wp_kses_post()` before shipping, so
52 * links + basic formatting are allowed
53 * but `<script>` and other unsafe
54 * markup are stripped.
55 * @type string $tone Optional. One of
56 * {@see desktop_mode_window_notice_tones()}.
57 * Default `info`.
58 * @type bool $dismissible Optional. Show a close button.
59 * Default `true`.
60 * @type string $icon Optional. Dashicons class for a
61 * leading glyph (e.g.
62 * `dashicons-info`). Must match
63 * `/^dashicons-[a-z0-9-]+$/`; values
64 * that don't are silently dropped to
65 * `''` rather than failing the
66 * registration.
67 * @type array $match Optional. Per-window selector. Pick
68 * any of:
69 * - `'window' => 'edit-php'` — single
70 * window id (e.g. `edit-php` for
71 * Posts, `plugins` for the native
72 * Plugins window).
73 * - `'windows' => array( 'edit-php',
74 * 'edit-php-pagename' )` — multiple
75 * window ids ("all windows of kind
76 * X / Y / Z"). Within the array the
77 * semantics is OR (any id matches).
78 * - `'urlContains' => 'wc-admin'` —
79 * case-insensitive URL substring
80 * match. Useful for plugin pages
81 * whose id is derived from a long
82 * URL.
83 *
84 * When more than one selector type is
85 * present, all of them must match
86 * (AND). E.g. `array( 'windows' =>
87 * array( 'edit-php' ), 'urlContains'
88 * => 'wc-admin' )` shows the notice
89 * only on the Posts window when its
90 * URL also contains `wc-admin` — not
91 * on every Posts window AND every
92 * wc-admin-URL window. Use two
93 * separate `desktop_mode_register_window_notice()`
94 * calls for OR-across-selector-types.
95 *
96 * When omitted, the notice paints on
97 * every window.
98 * @type int $order Optional. Sort order — lower
99 * renders higher in a stack of
100 * notices. Default 100.
101 * }
102 * @return true|WP_Error `true` on success; `WP_Error` on validation
103 * failure.
104 */
105 function desktop_mode_register_window_notice( $args = array() ) {
106 $defaults = array(
107 'id' => '',
108 'message' => '',
109 'tone' => 'info',
110 'dismissible' => true,
111 'icon' => '',
112 'match' => array(),
113 'order' => 100,
114 );
115 $args = wp_parse_args( $args, $defaults );
116
117 $id = (string) $args['id'];
118 if ( '' === $id ) {
119 return desktop_mode_registration_error(
120 'desktop_mode_missing_id',
121 __( 'Window notice registration requires a non-empty `id`.', 'desktop-mode' )
122 );
123 }
124 if ( ! preg_match( '/^[a-z0-9_\\/-]+$/i', $id ) ) {
125 return desktop_mode_registration_error(
126 'desktop_mode_invalid_id',
127 __( 'Window notice `id` must be alphanumeric with hyphens, underscores, or slashes.', 'desktop-mode' ),
128 array( 'id' => $id )
129 );
130 }
131
132 if ( '' === (string) $args['message'] ) {
133 return desktop_mode_registration_error(
134 'desktop_mode_missing_message',
135 __( 'Window notice registration requires a non-empty `message`.', 'desktop-mode' ),
136 array( 'id' => $id )
137 );
138 }
139
140 $tone = (string) $args['tone'];
141 if ( ! in_array( $tone, desktop_mode_window_notice_tones(), true ) ) {
142 return desktop_mode_registration_error(
143 'desktop_mode_invalid_tone',
144 __( 'Window notice `tone` must be one of the documented values.', 'desktop-mode' ),
145 array(
146 'id' => $id,
147 'tone' => $tone,
148 )
149 );
150 }
151
152 // Icon validation. Dashicons class names are alphanumeric with
153 // hyphens and always start with `dashicons-`. Anything else is
154 // either a typo or attempted styling-injection; drop silently
155 // rather than reject the whole registration so a minor mistake
156 // in one field doesn't kill the banner entirely.
157 $icon_raw = (string) $args['icon'];
158 $icon = preg_match( '/^dashicons-[a-z0-9-]+$/', $icon_raw ) ? $icon_raw : '';
159
160 $entry = array(
161 'id' => strtolower( $id ),
162 'message' => wp_kses_post( (string) $args['message'] ),
163 'tone' => $tone,
164 'dismissible' => (bool) $args['dismissible'],
165 'icon' => $icon,
166 'match' => is_array( $args['match'] ) ? $args['match'] : array(),
167 'order' => (int) $args['order'],
168 );
169
170 desktop_mode_window_notice_registry( $entry['id'], $entry );
171
172 /**
173 * Fires after a window notice is successfully registered.
174 *
175 * @since 0.22.0
176 *
177 * @param string $id The notice id.
178 * @param array $entry The stored registry entry.
179 */
180 do_action( 'desktop_mode_window_notice_registered', $entry['id'], $entry );
181
182 return true;
183 }
184
185 /**
186 * Internal module-level registry for window notices.
187 *
188 * @since 0.22.0
189 * @internal
190 *
191 * @param string $id Id to read or write.
192 * @param array|null $entry Entry to store, or `null` to read.
193 * @return array|null
194 */
195 function desktop_mode_window_notice_registry( $id = '', $entry = null ) {
196 static $store = array();
197
198 if ( '__flush__' === (string) $id ) {
199 $store = array();
200 return array();
201 }
202 if ( '' === (string) $id ) {
203 return $store;
204 }
205 if ( null !== $entry ) {
206 $store[ (string) $id ] = $entry;
207 }
208 return isset( $store[ (string) $id ] ) ? $store[ (string) $id ] : null;
209 }
210
211 /**
212 * Drop every registered window notice. Intended for PHPUnit `set_up()`
213 * so a previous test's notices can't leak into the next test's
214 * payload-build assertions. **Not** a public API — production code
215 * that calls this would wipe every plugin's registered notice on the
216 * current request. Mirrors the same `flush_*` shape the
217 * commands / settings-tabs / window-chrome registries expose.
218 *
219 * @since 0.22.0
220 * @internal
221 */
222 function desktop_mode_flush_window_notice_registry() {
223 desktop_mode_window_notice_registry( '__flush__' );
224 }
225
226 /**
227 * Build the payload shipped to the shell. Each entry is the stored
228 * registry record, runnable through the
229 * `desktop_mode_window_notices` filter so plugins can mutate the
230 * final list (e.g. add a dynamic notice computed at request time).
231 *
232 * @since 0.22.0
233 *
234 * @return array[]
235 */
236 function desktop_mode_build_window_notices_payload() {
237 $registry = desktop_mode_window_notice_registry();
238 $entries = is_array( $registry ) ? array_values( $registry ) : array();
239
240 /**
241 * Filter the assembled list of window notices before it ships to
242 * the shell. Plugins can append/remove/mutate notices here for
243 * request-dependent banners (e.g. "your trial expires today")
244 * without registering them statically.
245 *
246 * @since 0.22.0
247 *
248 * @param array[] $entries List of notice entries.
249 */
250 $entries = apply_filters( 'desktop_mode_window_notices', $entries );
251
252 // Re-sort by order for deterministic emission, then by id.
253 usort(
254 $entries,
255 static function ( $a, $b ) {
256 $oa = isset( $a['order'] ) ? (int) $a['order'] : 100;
257 $ob = isset( $b['order'] ) ? (int) $b['order'] : 100;
258 if ( $oa !== $ob ) {
259 return $oa - $ob;
260 }
261 return strcmp( (string) $a['id'], (string) $b['id'] );
262 }
263 );
264
265 return $entries;
266 }
267