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

319 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 * 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 * 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 OpenStation
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 * openstation_register_window( 'jorvy', array( …window args… ) );
39 * openstation_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 `openstation_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 openstation_capability_denied`.
85 * }
86 * @return true|WP_Error `true` on success; `WP_Error` otherwise.
87 */
88 function openstation_register_icon( $id, $args = array() ) {
89 $id = sanitize_key( (string) $id );
90 if ( '' === $id ) {
91 return openstation_registration_error(
92 'openstation_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 openstation_registration_error(
117 'openstation_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 openstation_registration_error(
124 'openstation_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 openstation_registration_error(
135 'openstation_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(
142 'capability' => (string) $cap,
143 'id' => $id,
144 )
145 );
146 }
147 }
148
149 if ( '' === (string) $args['title'] ) {
150 return openstation_registration_error(
151 'openstation_missing_title',
152 __( 'Desktop icon registration requires a non-empty `title`.', 'desktop-mode' ),
153 array( 'id' => $id )
154 );
155 }
156
157 $window = sanitize_key( (string) $args['window'] );
158 $url = (string) $args['url'];
159 if ( '' !== $window && '' !== $url ) {
160 return openstation_registration_error(
161 'openstation_conflicting_target',
162 __( 'Desktop icon cannot declare both `window` and `url`; pick one target.', 'desktop-mode' ),
163 array( 'id' => $id )
164 );
165 }
166 if ( '' === $window && '' === $url ) {
167 return openstation_registration_error(
168 'openstation_missing_target',
169 __( 'Desktop icon must declare a `window` id or a `url` target.', 'desktop-mode' ),
170 array( 'id' => $id )
171 );
172 }
173 if ( '' !== $url ) {
174 // Accept any http(s) URL. Same-origin admin URLs open in a
175 // desktop window; off-site URLs open in a new browser tab at
176 // click time (shell decides).
177 $url = esc_url_raw( $url, array( 'http', 'https' ) );
178 if ( '' === $url ) {
179 return openstation_registration_error(
180 'openstation_invalid_url',
181 __( 'Desktop icon `url` must be a valid http(s) URL.', 'desktop-mode' ),
182 array( 'id' => $id )
183 );
184 }
185 }
186
187 $entry = array(
188 'id' => $id,
189 'title' => (string) $args['title'],
190 'icon' => openstation_sanitize_dock_icon( (string) $args['icon'] ),
191 'window' => $window,
192 'url' => $url,
193 'position' => (int) $args['position'],
194 'pinned' => (bool) $args['pinned'],
195 );
196 openstation_desktop_icon_registry( $id, $entry );
197
198 /**
199 * Fires after a desktop icon is successfully registered.
200 *
201 * Does NOT fire when `openstation_register_icon()` returns a
202 * `WP_Error`.
203 *
204 * @param string $id The icon id.
205 * @param array $entry The stored registry entry (id, title,
206 * icon, window, url, position, pinned).
207 */
208 do_action( 'openstation_icon_registered', $id, $entry );
209
210 return true;
211 }
212
213 /**
214 * Internal module-level registry for desktop icons registered via
215 * {@see openstation_register_icon()}. Same static-store pattern as
216 * the widget + native-window + wallpaper registries.
217 *
218 * @internal
219 */
220 function openstation_desktop_icon_registry( $id = '', $entry = null ) {
221 static $store = array();
222
223 if ( '' === (string) $id ) {
224 return $store;
225 }
226 // Sentinel write: passing the literal string `__unset__` removes
227 // the entry. Used by `openstation_unregister_icon()` and by
228 // PHPUnit teardowns; lets us clear test-only registrations
229 // without exposing the static `$store` directly.
230 if ( '__unset__' === $entry ) {
231 unset( $store[ $id ] );
232 return null;
233 }
234 if ( null !== $entry ) {
235 $store[ $id ] = $entry;
236 }
237 return isset( $store[ $id ] ) ? $store[ $id ] : null;
238 }
239
240 /**
241 * Remove a previously registered desktop icon from the static
242 * registry. Mirror of `openstation_register_icon()` — handy for
243 * plugins that register icons conditionally and need to drop them
244 * mid-request, and for PHPUnit teardowns that shouldn't leak
245 * registrations into other tests.
246 *
247 * @param string $id Icon id passed to `openstation_register_icon()`.
248 * @return void
249 */
250 function openstation_unregister_icon( $id ) {
251 $id = sanitize_key( (string) $id );
252 if ( '' === $id ) {
253 return;
254 }
255 openstation_desktop_icon_registry( $id, '__unset__' );
256 }
257
258 /**
259 * Build the desktop-icon list for the shell payload. Applies a
260 * `openstation_icons` filter so plugins can hide / reorder / rename
261 * entries registered by others — mirrors the wallpaper payload
262 * builder's filter discipline.
263 *
264 * @return array[]
265 */
266 function openstation_build_desktop_icons_payload() {
267 $registry = openstation_desktop_icon_registry();
268 if ( ! is_array( $registry ) || empty( $registry ) ) {
269 return array();
270 }
271
272 /**
273 * Filters the full desktop-icon registry before it ships to the
274 * shell. Each entry is the shape stored by
275 * `openstation_register_icon()` (`id`, `title`, `icon`, `window`,
276 * `url`, `position`, `pinned`). Return a reordered / filtered array.
277 *
278 * @param array[] $registry The registered icon entries.
279 */
280 $registry = apply_filters( 'openstation_icons', $registry );
281 if ( ! is_array( $registry ) ) {
282 return array();
283 }
284
285 $out = array();
286 foreach ( $registry as $entry ) {
287 if ( ! is_array( $entry ) || empty( $entry['id'] ) ) {
288 continue;
289 }
290 $out[] = array(
291 'id' => (string) $entry['id'],
292 'title' => isset( $entry['title'] ) ? (string) $entry['title'] : '',
293 'icon' => isset( $entry['icon'] ) ? (string) $entry['icon'] : 'dashicons-admin-generic',
294 'window' => isset( $entry['window'] ) ? (string) $entry['window'] : '',
295 'url' => isset( $entry['url'] ) ? (string) $entry['url'] : '',
296 'position' => isset( $entry['position'] ) ? (int) $entry['position'] : 100,
297 'pinned' => ! empty( $entry['pinned'] ),
298 );
299 }
300
301 // Pinned icons first; then by position. Ties break on insertion order.
302 usort(
303 $out,
304 static function ( $a, $b ) {
305 $ap = ! empty( $a['pinned'] ) ? 0 : 1;
306 $bp = ! empty( $b['pinned'] ) ? 0 : 1;
307 if ( $ap !== $bp ) {
308 return $ap - $bp;
309 }
310 if ( $a['position'] === $b['position'] ) {
311 return 0;
312 }
313 return $a['position'] < $b['position'] ? -1 : 1;
314 }
315 );
316
317 return $out;
318 }
319