PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.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.9.8, at includes/registries/icons.php

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