PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
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 0.8.6 All 33 releases
desktop-mode / includes / registries / icons.php

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

323 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 * OpenStation — Desktop-icons registry.
4 *
5 * Owns the registration API + payload builder for the wallpaper-
6 * surface app icons (`wp.os.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 * `openstation_register_icon()` and rendered by
10 * `src/desktop-icons.ts`.
11 *
12 * Not to be confused with `includes/wp-icon-registry.php`, which hands our
13 * eleven pieces of artwork to WordPress's own icon registry for use with
14 * `wp_get_icon()`. This file is about a surface of the desktop.
15 *
16 * Extracted from the 2,101-LOC `components.php` during the
17 * architecture-0.8.1 PHP slicing (phase 6). Behaviour is
18 * unchanged: every function name, every WP filter, every error
19 * code is identical. PHP looks function references up by name at
20 * hook-fire time, so existing call sites resolve from any module.
21 *
22 * @package OpenStation
23 */
24
25 defined( 'ABSPATH' ) || exit;
26
27
28 /**
29 * Register a desktop icon — a clickable shortcut tile that sits on
30 * the wallpaper (think macOS desktop icons / phone home screen apps)
31 * and opens a native window or a URL on click.
32 *
33 * Icons are distinct from dock tiles: the dock rail is for
34 * top-level admin pages, the desktop surface is for quick shortcuts
35 * a user or theme wants front-and-centre. A single plugin can
36 * register a window AND an icon that opens it — the two surfaces
37 * are orthogonal.
38 *
39 * Example — the classic Jorvy recipe:
40 *
41 * ```php
42 * openstation_register_window( 'jorvy', array( …window args… ) );
43 * openstation_register_icon( 'jorvy', array(
44 * 'title' => __( 'Jorvy', 'jorvy' ),
45 * 'icon' => 'dashicons-star-filled',
46 * 'window' => 'jorvy',
47 * 'position' => 10,
48 * ) );
49 * ```
50 *
51 * @param string $id Icon id. Must be a kebab-case-ish slug.
52 * @param array $args {
53 * Icon registration options.
54 *
55 * @type string $title Display label shown under the icon. Required.
56 * @type string $icon Dashicons class (`dashicons-*`), http(s)
57 * URL to an image, or `data:image/svg+xml`
58 * URI (`;base64,` or URL-encoded). Runs
59 * through the same sanitizer as dock icons
60 * — `javascript:` and other `data:` schemes
61 * are rejected. Required unless `icon_svg`
62 * is provided.
63 * @type string $icon_svg Raw SVG markup (e.g. `'<svg …>…</svg>'`).
64 * Convenience shorthand: the framework
65 * base64-encodes it into a `data:image/svg+xml;base64,…`
66 * URI and routes the result through the
67 * same sanitizer as `icon`. Wins over
68 * `icon` when both are supplied. Markup
69 * containing a `<script>` tag is rejected
70 * with `openstation_invalid_icon_svg`.
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`.
86 * @type string[] $capabilities Gate: ALL caps must match. Any
87 * missed cap returns
88 * `WP_Error openstation_capability_denied`.
89 * }
90 * @return true|WP_Error `true` on success; `WP_Error` otherwise.
91 */
92 function openstation_register_icon( $id, $args = array() ) {
93 $id = sanitize_key( (string) $id );
94 if ( '' === $id ) {
95 return openstation_registration_error(
96 'openstation_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 openstation_registration_error(
121 'openstation_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 openstation_registration_error(
128 'openstation_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 openstation_registration_error(
139 'openstation_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(
146 'capability' => (string) $cap,
147 'id' => $id,
148 )
149 );
150 }
151 }
152
153 if ( '' === (string) $args['title'] ) {
154 return openstation_registration_error(
155 'openstation_missing_title',
156 __( 'Desktop icon registration requires a non-empty `title`.', 'desktop-mode' ),
157 array( 'id' => $id )
158 );
159 }
160
161 $window = sanitize_key( (string) $args['window'] );
162 $url = (string) $args['url'];
163 if ( '' !== $window && '' !== $url ) {
164 return openstation_registration_error(
165 'openstation_conflicting_target',
166 __( 'Desktop icon cannot declare both `window` and `url`; pick one target.', 'desktop-mode' ),
167 array( 'id' => $id )
168 );
169 }
170 if ( '' === $window && '' === $url ) {
171 return openstation_registration_error(
172 'openstation_missing_target',
173 __( 'Desktop icon must declare a `window` id or a `url` target.', 'desktop-mode' ),
174 array( 'id' => $id )
175 );
176 }
177 if ( '' !== $url ) {
178 // Accept any http(s) URL. Same-origin admin URLs open in a
179 // desktop window; off-site URLs open in a new browser tab at
180 // click time (shell decides).
181 $url = esc_url_raw( $url, array( 'http', 'https' ) );
182 if ( '' === $url ) {
183 return openstation_registration_error(
184 'openstation_invalid_url',
185 __( 'Desktop icon `url` must be a valid http(s) URL.', 'desktop-mode' ),
186 array( 'id' => $id )
187 );
188 }
189 }
190
191 $entry = array(
192 'id' => $id,
193 'title' => (string) $args['title'],
194 'icon' => openstation_sanitize_dock_icon( (string) $args['icon'] ),
195 'window' => $window,
196 'url' => $url,
197 'position' => (int) $args['position'],
198 'pinned' => (bool) $args['pinned'],
199 );
200 openstation_desktop_icon_registry( $id, $entry );
201
202 /**
203 * Fires after a desktop icon is successfully registered.
204 *
205 * Does NOT fire when `openstation_register_icon()` returns a
206 * `WP_Error`.
207 *
208 * @param string $id The icon id.
209 * @param array $entry The stored registry entry (id, title,
210 * icon, window, url, position, pinned).
211 */
212 do_action( 'openstation_icon_registered', $id, $entry );
213
214 return true;
215 }
216
217 /**
218 * Internal module-level registry for desktop icons registered via
219 * {@see openstation_register_icon()}. Same static-store pattern as
220 * the widget + native-window + wallpaper registries.
221 *
222 * @internal
223 */
224 function openstation_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 `openstation_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 `openstation_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 * @param string $id Icon id passed to `openstation_register_icon()`.
252 * @return void
253 */
254 function openstation_unregister_icon( $id ) {
255 $id = sanitize_key( (string) $id );
256 if ( '' === $id ) {
257 return;
258 }
259 openstation_desktop_icon_registry( $id, '__unset__' );
260 }
261
262 /**
263 * Build the desktop-icon list for the shell payload. Applies a
264 * `openstation_icons` filter so plugins can hide / reorder / rename
265 * entries registered by others — mirrors the wallpaper payload
266 * builder's filter discipline.
267 *
268 * @return array[]
269 */
270 function openstation_build_desktop_icons_payload() {
271 $registry = openstation_desktop_icon_registry();
272 if ( ! is_array( $registry ) || empty( $registry ) ) {
273 return array();
274 }
275
276 /**
277 * Filters the full desktop-icon registry before it ships to the
278 * shell. Each entry is the shape stored by
279 * `openstation_register_icon()` (`id`, `title`, `icon`, `window`,
280 * `url`, `position`, `pinned`). Return a reordered / filtered array.
281 *
282 * @param array[] $registry The registered icon entries.
283 */
284 $registry = apply_filters( 'openstation_icons', $registry );
285 if ( ! is_array( $registry ) ) {
286 return array();
287 }
288
289 $out = array();
290 foreach ( $registry as $entry ) {
291 if ( ! is_array( $entry ) || empty( $entry['id'] ) ) {
292 continue;
293 }
294 $out[] = array(
295 'id' => (string) $entry['id'],
296 'title' => isset( $entry['title'] ) ? (string) $entry['title'] : '',
297 'icon' => isset( $entry['icon'] ) ? (string) $entry['icon'] : 'dashicons-admin-generic',
298 'window' => isset( $entry['window'] ) ? (string) $entry['window'] : '',
299 'url' => isset( $entry['url'] ) ? (string) $entry['url'] : '',
300 'position' => isset( $entry['position'] ) ? (int) $entry['position'] : 100,
301 'pinned' => ! empty( $entry['pinned'] ),
302 );
303 }
304
305 // Pinned icons first; then by position. Ties break on insertion order.
306 usort(
307 $out,
308 static function ( $a, $b ) {
309 $ap = ! empty( $a['pinned'] ) ? 0 : 1;
310 $bp = ! empty( $b['pinned'] ) ? 0 : 1;
311 if ( $ap !== $bp ) {
312 return $ap - $bp;
313 }
314 if ( $a['position'] === $b['position'] ) {
315 return 0;
316 }
317 return $a['position'] < $b['position'] ? -1 : 1;
318 }
319 );
320
321 return $out;
322 }
323