PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.8
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 / registries / icons.php

icons.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.8, at includes/registries/icons.php

326 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Desktop-icons registry.
4 *
5 * Owns the registration API + payload builder for the wallpaper-
6 * surface app icons (`wp.desktop.icons`). The dock has its own
7 * rail; this is the second surface — clickable shortcuts that
8 * sit on the desktop wallpaper itself, registered via
9 * `desktop_mode_register_icon()` and rendered by
10 * `src/desktop-icons.ts`.
11 *
12 * Extracted from the 2,101-LOC `components.php` during the
13 * architecture-0.8.1 PHP slicing (phase 6). Behaviour is
14 * unchanged: every function name, every WP filter, every error
15 * code is identical. PHP looks function references up by name at
16 * hook-fire time, so existing call sites resolve from any module.
17 *
18 * @package Desktop_Mode
19 * @since 0.8.1
20 */
21
22 defined( 'ABSPATH' ) || exit;
23
24
25 /**
26 * Register a desktop icon — a clickable shortcut tile that sits on
27 * the wallpaper (think macOS desktop icons / phone home screen apps)
28 * and opens a native window or a URL on click.
29 *
30 * Icons are distinct from dock tiles: the dock rail is for
31 * top-level admin pages, the desktop surface is for quick shortcuts
32 * a user or theme wants front-and-centre. A single plugin can
33 * register a window AND an icon that opens it — the two surfaces
34 * are orthogonal.
35 *
36 * Example — the classic Jorvy recipe:
37 *
38 * ```php
39 * desktop_mode_register_window( 'jorvy', array( …window args… ) );
40 * desktop_mode_register_icon( 'jorvy', array(
41 * 'title' => __( 'Jorvy', 'jorvy' ),
42 * 'icon' => 'dashicons-star-filled',
43 * 'window' => 'jorvy',
44 * 'position' => 10,
45 * ) );
46 * ```
47 *
48 * @since 0.11.0
49 *
50 * @param string $id Icon id. Must be a kebab-case-ish slug.
51 * @param array $args {
52 * Icon registration options.
53 *
54 * @type string $title Display label shown under the icon. Required.
55 * @type string $icon Dashicons class (`dashicons-*`), http(s)
56 * URL to an image, or `data:image/svg+xml`
57 * URI (`;base64,` or URL-encoded). Runs
58 * through the same sanitizer as dock icons
59 * — `javascript:` and other `data:` schemes
60 * are rejected. Required unless `icon_svg`
61 * is provided.
62 * @type string $icon_svg Raw SVG markup (e.g. `'<svg …>…</svg>'`).
63 * Convenience shorthand: the framework
64 * base64-encodes it into a `data:image/svg+xml;base64,…`
65 * URI and routes the result through the
66 * same sanitizer as `icon`. Wins over
67 * `icon` when both are supplied. Markup
68 * containing a `<script>` tag is rejected
69 * with `desktop_mode_invalid_icon_svg`.
70 * @since 0.8.2
71 * @type string $window Id of a registered native window to
72 * open on click. Mutually exclusive
73 * with `url`.
74 * @type string $url URL to open on click (admin URLs
75 * open as a new window in the shell;
76 * off-site URLs open in a new tab).
77 * Mutually exclusive with `window`.
78 * @type int $position Sort order; lower renders earlier
79 * (top-left on the grid). Default 100.
80 * @type bool $pinned When `true`, the icon renders before
81 * any unpinned icon regardless of
82 * `position`, and is never user-
83 * draggable. Reserved for built-in
84 * shortcuts like "My WordPress".
85 * Default `false`. @since 0.8.0
86 * @type string[] $capabilities Gate: ALL caps must match. Any
87 * missed cap returns
88 * `WP_Error desktop_mode_capability_denied`.
89 * }
90 * @return true|WP_Error `true` on success; `WP_Error` otherwise.
91 */
92 function desktop_mode_register_icon( $id, $args = array() ) {
93 $id = sanitize_key( (string) $id );
94 if ( '' === $id ) {
95 return desktop_mode_registration_error(
96 'desktop_mode_missing_id',
97 __( 'Desktop icon id is required and must be a valid slug.', 'desktop-mode' )
98 );
99 }
100
101 $defaults = array(
102 'title' => '',
103 'icon' => 'dashicons-admin-generic',
104 'icon_svg' => '',
105 'window' => '',
106 'url' => '',
107 'position' => 100,
108 'pinned' => false,
109 'capabilities' => array(),
110 );
111 $args = wp_parse_args( $args, $defaults );
112
113 $svg = trim( (string) $args['icon_svg'] );
114 if ( '' !== $svg ) {
115 // Reject markup that contains a script tag outright. The data
116 // URI is consumed via `<img src=…>` in the browser (which
117 // sandboxes scripts inside SVG), but defence-in-depth catches
118 // callers who paste an SVG harvested from an untrusted source.
119 if ( false !== stripos( $svg, '<script' ) ) {
120 return desktop_mode_registration_error(
121 'desktop_mode_invalid_icon_svg',
122 __( 'Desktop icon `icon_svg` must not contain a <script> tag.', 'desktop-mode' ),
123 array( 'id' => $id )
124 );
125 }
126 if ( 0 !== stripos( ltrim( $svg ), '<svg' ) ) {
127 return desktop_mode_registration_error(
128 'desktop_mode_invalid_icon_svg',
129 __( 'Desktop icon `icon_svg` must start with a <svg> root element.', 'desktop-mode' ),
130 array( 'id' => $id )
131 );
132 }
133 $args['icon'] = 'data:image/svg+xml;base64,' . base64_encode( $svg );
134 }
135
136 foreach ( (array) $args['capabilities'] as $cap ) {
137 if ( ! current_user_can( (string) $cap ) ) {
138 return desktop_mode_registration_error(
139 'desktop_mode_capability_denied',
140 sprintf(
141 /* translators: %s: capability slug. */
142 __( 'Current user lacks the %s capability required to register this desktop icon.', 'desktop-mode' ),
143 (string) $cap
144 ),
145 array( 'capability' => (string) $cap, 'id' => $id )
146 );
147 }
148 }
149
150 if ( '' === (string) $args['title'] ) {
151 return desktop_mode_registration_error(
152 'desktop_mode_missing_title',
153 __( 'Desktop icon registration requires a non-empty `title`.', 'desktop-mode' ),
154 array( 'id' => $id )
155 );
156 }
157
158 $window = sanitize_key( (string) $args['window'] );
159 $url = (string) $args['url'];
160 if ( '' !== $window && '' !== $url ) {
161 return desktop_mode_registration_error(
162 'desktop_mode_conflicting_target',
163 __( 'Desktop icon cannot declare both `window` and `url`; pick one target.', 'desktop-mode' ),
164 array( 'id' => $id )
165 );
166 }
167 if ( '' === $window && '' === $url ) {
168 return desktop_mode_registration_error(
169 'desktop_mode_missing_target',
170 __( 'Desktop icon must declare a `window` id or a `url` target.', 'desktop-mode' ),
171 array( 'id' => $id )
172 );
173 }
174 if ( '' !== $url ) {
175 // Accept any http(s) URL. Same-origin admin URLs open in a
176 // desktop window; off-site URLs open in a new browser tab at
177 // click time (shell decides).
178 $url = esc_url_raw( $url, array( 'http', 'https' ) );
179 if ( '' === $url ) {
180 return desktop_mode_registration_error(
181 'desktop_mode_invalid_url',
182 __( 'Desktop icon `url` must be a valid http(s) URL.', 'desktop-mode' ),
183 array( 'id' => $id )
184 );
185 }
186 }
187
188 $entry = array(
189 'id' => $id,
190 'title' => (string) $args['title'],
191 'icon' => desktop_mode_sanitize_dock_icon( (string) $args['icon'] ),
192 'window' => $window,
193 'url' => $url,
194 'position' => (int) $args['position'],
195 'pinned' => (bool) $args['pinned'],
196 );
197 desktop_mode_desktop_icon_registry( $id, $entry );
198
199 /**
200 * Fires after a desktop icon is successfully registered.
201 *
202 * Does NOT fire when `desktop_mode_register_icon()` returns a
203 * `WP_Error`.
204 *
205 * @since 0.11.0
206 *
207 * @param string $id The icon id.
208 * @param array $entry The stored registry entry (id, title,
209 * icon, window, url, position).
210 */
211 do_action( 'desktop_mode_icon_registered', $id, $entry );
212
213 return true;
214 }
215
216 /**
217 * Internal module-level registry for desktop icons registered via
218 * {@see desktop_mode_register_icon()}. Same static-store pattern as
219 * the widget + native-window + wallpaper registries.
220 *
221 * @since 0.11.0
222 * @internal
223 */
224 function desktop_mode_desktop_icon_registry( $id = '', $entry = null ) {
225 static $store = array();
226
227 if ( '' === (string) $id ) {
228 return $store;
229 }
230 // Sentinel write: passing the literal string `__unset__` removes
231 // the entry. Used by `desktop_mode_unregister_icon()` and by
232 // PHPUnit teardowns; lets us clear test-only registrations
233 // without exposing the static `$store` directly.
234 if ( '__unset__' === $entry ) {
235 unset( $store[ $id ] );
236 return null;
237 }
238 if ( null !== $entry ) {
239 $store[ $id ] = $entry;
240 }
241 return isset( $store[ $id ] ) ? $store[ $id ] : null;
242 }
243
244 /**
245 * Remove a previously registered desktop icon from the static
246 * registry. Mirror of `desktop_mode_register_icon()` — handy for
247 * plugins that register icons conditionally and need to drop them
248 * mid-request, and for PHPUnit teardowns that shouldn't leak
249 * registrations into other tests.
250 *
251 * @since 0.8.0
252 *
253 * @param string $id Icon id passed to `desktop_mode_register_icon()`.
254 * @return void
255 */
256 function desktop_mode_unregister_icon( $id ) {
257 $id = sanitize_key( (string) $id );
258 if ( '' === $id ) {
259 return;
260 }
261 desktop_mode_desktop_icon_registry( $id, '__unset__' );
262 }
263
264 /**
265 * Build the desktop-icon list for the shell payload. Applies a
266 * `desktop_mode_icons` filter so plugins can hide / reorder / rename
267 * entries registered by others — mirrors the wallpaper payload
268 * builder's filter discipline.
269 *
270 * @since 0.11.0
271 *
272 * @return array[]
273 */
274 function desktop_mode_build_desktop_icons_payload() {
275 $registry = desktop_mode_desktop_icon_registry();
276 if ( ! is_array( $registry ) || empty( $registry ) ) {
277 return array();
278 }
279
280 /**
281 * Filters the full desktop-icon registry before it ships to the
282 * shell. Each entry is the shape stored by
283 * `desktop_mode_register_icon()` (`id`, `title`, `icon`, `window`,
284 * `url`, `position`). Return a reordered / filtered array.
285 *
286 * @since 0.11.0
287 *
288 * @param array[] $registry The registered icon entries.
289 */
290 $registry = apply_filters( 'desktop_mode_icons', $registry );
291 if ( ! is_array( $registry ) ) {
292 return array();
293 }
294
295 $out = array();
296 foreach ( $registry as $entry ) {
297 if ( ! is_array( $entry ) || empty( $entry['id'] ) ) {
298 continue;
299 }
300 $out[] = array(
301 'id' => (string) $entry['id'],
302 'title' => isset( $entry['title'] ) ? (string) $entry['title'] : '',
303 'icon' => isset( $entry['icon'] ) ? (string) $entry['icon'] : 'dashicons-admin-generic',
304 'window' => isset( $entry['window'] ) ? (string) $entry['window'] : '',
305 'url' => isset( $entry['url'] ) ? (string) $entry['url'] : '',
306 'position' => isset( $entry['position'] ) ? (int) $entry['position'] : 100,
307 'pinned' => ! empty( $entry['pinned'] ),
308 );
309 }
310
311 // Pinned icons first; then by position. Ties break on insertion order.
312 usort( $out, static function ( $a, $b ) {
313 $ap = ! empty( $a['pinned'] ) ? 0 : 1;
314 $bp = ! empty( $b['pinned'] ) ? 0 : 1;
315 if ( $ap !== $bp ) {
316 return $ap - $bp;
317 }
318 if ( $a['position'] === $b['position'] ) {
319 return 0;
320 }
321 return $a['position'] < $b['position'] ? -1 : 1;
322 } );
323
324 return $out;
325 }
326