PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.5
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.5
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 / desktop-themes / registry.php

registry.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.5, at includes/desktop-themes/registry.php

397 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Code-registered desktop themes + payload builder.
4 *
5 * Two ways a theme reaches the shell:
6 *
7 * - **Uploaded** — a ZIP installed through the REST route. Assets
8 * live in `uploads/desktop-mode-themes/<slug>/` and the compiled
9 * stylesheet is a real file, so the payload ships a `cssUrl`.
10 * - **Code-registered** — a plugin calls
11 * `openstation_register_desktop_theme()` with asset URLs it
12 * already publishes. There is no file to link, so the payload
13 * ships the compiled stylesheet as `cssText` and `cssUrl` is
14 * empty. PHP prints it via `wp_add_inline_style()` at boot; the
15 * shell injects a `<style>` for live switches.
16 *
17 * Both travel through the same sanitizer and the same compiler, so a
18 * code theme is exactly as constrained as an uploaded one — a plugin
19 * that wants arbitrary CSS should enqueue a stylesheet, not pretend
20 * to be a theme.
21 *
22 * @package OpenStation
23 */
24
25 defined( 'ABSPATH' ) || exit;
26
27 /**
28 * Internal static store for code-registered themes. Same
29 * `__unset__` sentinel idiom as the icon / wallpaper / widget
30 * registries.
31 *
32 * @internal
33 *
34 * @param string $slug Theme slug, or `''` to read the whole store.
35 * @param mixed $entry Entry to write, `'__unset__'` to remove, or
36 * `null` to read.
37 * @return array|null
38 */
39 function openstation_desktop_theme_registry( $slug = '', $entry = null ) {
40 static $store = array();
41
42 if ( '' === (string) $slug ) {
43 return $store;
44 }
45 if ( '__unset__' === $entry ) {
46 unset( $store[ $slug ] );
47 return null;
48 }
49 if ( null !== $entry ) {
50 $store[ $slug ] = $entry;
51 }
52 return isset( $store[ $slug ] ) ? $store[ $slug ] : null;
53 }
54
55 /**
56 * Register a desktop theme from code.
57 *
58 * Every asset (`preview`, icon `path`s, texture `path`s) must be an
59 * absolute http(s) URL the plugin already serves — there is no ZIP,
60 * so there is nothing for us to host.
61 *
62 * ```php
63 * openstation_register_desktop_theme( 'my-plugin/neon', array(
64 * 'name' => __( 'Neon', 'my-plugin' ),
65 * 'version' => '1.0.0',
66 * 'preview' => plugins_url( 'theme/preview.png', __FILE__ ),
67 * 'tokens' => array(
68 * '--os-window-radius' => '14px',
69 * '--os-titlebar-bg' => '#12122a',
70 * ),
71 * 'icons' => array(
72 * 'WINDOW_CONTROL_CLOSE' => array(
73 * 'type' => 'image',
74 * 'path' => plugins_url( 'theme/close.svg', __FILE__ ),
75 * ),
76 * ),
77 * 'textures' => array(
78 * 'TITLEBAR' => array(
79 * 'type' => 'image',
80 * 'path' => plugins_url( 'theme/titlebar.png', __FILE__ ),
81 * 'repeat' => 'repeat-x',
82 * ),
83 * ),
84 * 'fonts' => array(
85 * array(
86 * 'family' => 'Neon Grotesk',
87 * 'weight' => '400',
88 * 'display' => 'swap',
89 * 'src' => array( plugins_url( 'theme/neon.woff2', __FILE__ ) ),
90 * ),
91 * ),
92 * ) );
93 * ```
94 *
95 * @param string $id Theme id (`slug` or `vendor/slug`).
96 * @param array $args {
97 * @type string $name Display name. Required.
98 * @type string $version Version string. Optional.
99 * @type string $author Author name. Optional.
100 * @type string $description Short description. Optional.
101 * @type string $preview Absolute URL of a preview image.
102 * @type array $tokens Map of `--os-*` => value.
103 * @type string $iconColor Default tint for every icon that does
104 * not set its own `color`.
105 * @type array $icons Map of slot => icon descriptor.
106 * @type array $textures Map of slot => texture descriptor.
107 * @type array $fonts List of `@font-face` descriptors.
108 * `src` entries are absolute URLs.
109 * @type array|string $wallpapers One absolute image URL, one
110 * descriptor (`path` / `label` / `size`
111 * / `repeat` / `position`), or a list or
112 * map of either. Each becomes a pickable
113 * wallpaper.
114 * @type array $recommendedOsSettings Presentation preferences the
115 * theme would like the user to wear
116 * (`dockSize`, `desktopLayout`,
117 * `windowRadius`, `dockRailRenderer`).
118 * Applied once, the first time a user
119 * activates the theme.
120 * }
121 * @return true|WP_Error
122 */
123 function openstation_register_desktop_theme( $id, $args = array() ) {
124 $args = wp_parse_args(
125 is_array( $args ) ? $args : array(),
126 array(
127 'name' => '',
128 'version' => '',
129 'author' => '',
130 'description' => '',
131 'preview' => '',
132 'tokens' => array(),
133 'iconColor' => '',
134 'icons' => array(),
135 'textures' => array(),
136 'fonts' => array(),
137 'wallpapers' => array(),
138 'recommendedOsSettings' => array(),
139 )
140 );
141
142 $manifest = openstation_sanitize_desktop_theme_manifest(
143 array(
144 // Code registrations declare v2 unconditionally: the array
145 // they hand over always HAS the recommendation key, empty
146 // or not, so there is nothing for a version to disambiguate.
147 'manifestVersion' => 2,
148 'id' => (string) $id,
149 'name' => (string) $args['name'],
150 'version' => (string) $args['version'],
151 'author' => (string) $args['author'],
152 'description' => (string) $args['description'],
153 'preview' => (string) $args['preview'],
154 'tokens' => $args['tokens'],
155 'iconColor' => (string) $args['iconColor'],
156 'icons' => $args['icons'],
157 'textures' => $args['textures'],
158 'fonts' => $args['fonts'],
159 'wallpapers' => $args['wallpapers'],
160 'recommendedOsSettings' => $args['recommendedOsSettings'],
161 ),
162 openstation_desktop_theme_url_asset_resolver()
163 );
164 if ( is_wp_error( $manifest ) ) {
165 return openstation_registration_error(
166 $manifest->get_error_code(),
167 $manifest->get_error_message(),
168 array( 'id' => (string) $id )
169 );
170 }
171
172 $slug = (string) $manifest['slug'];
173 $entry = array(
174 'slug' => $slug,
175 'manifest' => $manifest,
176 // Code themes carry absolute asset URLs already, so the
177 // compiler needs no base to join against.
178 'cssText' => openstation_desktop_theme_compile_css( $manifest, $slug, '' ),
179 );
180 openstation_desktop_theme_registry( $slug, $entry );
181
182 /**
183 * Fires after a desktop theme is registered from code.
184 *
185 * Does NOT fire when the registration returned a `WP_Error`.
186 *
187 * @param string $slug Theme slug.
188 * @param array $entry Stored registry entry.
189 */
190 do_action( 'openstation_desktop_theme_registered', $slug, $entry );
191
192 return true;
193 }
194
195 /**
196 * Remove a code-registered desktop theme.
197 *
198 * Has no effect on uploaded themes — those are removed with
199 * {@see openstation_desktop_theme_delete()}.
200 *
201 * @param string $id Theme id or slug.
202 * @return void
203 */
204 function openstation_unregister_desktop_theme( $id ) {
205 $slug = openstation_desktop_theme_slug_from_id( $id );
206 if ( '' === $slug ) {
207 return;
208 }
209 openstation_desktop_theme_registry( $slug, '__unset__' );
210 }
211
212 /**
213 * Shape one stored/registered entry into the payload entry the
214 * shell consumes.
215 *
216 * @internal
217 *
218 * @param array $entry Stored entry (`slug`, `manifest`, …).
219 * @param string $source `'upload'` or `'code'`.
220 * @return array|null
221 */
222 function openstation_shape_desktop_theme_payload_entry( $entry, $source ) {
223 if ( ! is_array( $entry ) || empty( $entry['manifest'] ) || ! is_array( $entry['manifest'] ) ) {
224 return null;
225 }
226 $manifest = $entry['manifest'];
227 $slug = isset( $entry['slug'] ) ? sanitize_key( (string) $entry['slug'] ) : '';
228 if ( '' === $slug ) {
229 return null;
230 }
231
232 $is_upload = 'upload' === $source;
233 $base_url = $is_upload ? openstation_desktop_themes_url( $slug ) : '';
234 $installed_at = isset( $entry['installedAt'] ) ? (int) $entry['installedAt'] : 0;
235 // Every generated asset URL carries the install timestamp so a
236 // re-upload (which reuses the same paths by design) cannot be
237 // served from the browser's cache. Without it an author fixes
238 // their artwork, re-uploads, and sees the old files.
239 $asset_version = $is_upload && $installed_at > 0 ? (string) $installed_at : '';
240
241 // Icon map — the shell only ever needs a paintable string per
242 // slot: a dashicon class or an absolute URL.
243 //
244 // Tints ride in a PARALLEL map rather than turning `icons` into a
245 // map of objects. That keeps `resolveIcon()` returning a paintable
246 // string (its documented contract, and what the
247 // `os.desktop-theme.icon` filter is typed against) and
248 // keeps the resolver's hot path a single lookup. A slot with no
249 // tint is simply absent from `iconColors`, which is also the
250 // "paint it the way you always did" signal.
251 $icons = array();
252 $icon_colors = array();
253 if ( ! empty( $manifest['icons'] ) && is_array( $manifest['icons'] ) ) {
254 foreach ( $manifest['icons'] as $slot => $icon ) {
255 if ( ! is_array( $icon ) ) {
256 continue;
257 }
258 if ( 'dashicon' === $icon['type'] ) {
259 $icons[ $slot ] = (string) $icon['name'];
260 } else {
261 $url = openstation_desktop_theme_asset_url( $icon['path'], $base_url, $asset_version );
262 if ( '' === $url ) {
263 continue;
264 }
265 $icons[ $slot ] = $url;
266 }
267 if ( ! empty( $icon['color'] ) ) {
268 $icon_colors[ $slot ] = (string) $icon['color'];
269 }
270 }
271 }
272
273 // Distinct family names, in declaration order. A family declared at
274 // four weights is one entry, which is what a UI wants to show.
275 $font_families = array();
276 if ( ! empty( $manifest['fonts'] ) && is_array( $manifest['fonts'] ) ) {
277 foreach ( $manifest['fonts'] as $face ) {
278 if ( ! is_array( $face ) || empty( $face['family'] ) ) {
279 continue;
280 }
281 $family = (string) $face['family'];
282 if ( ! in_array( $family, $font_families, true ) ) {
283 $font_families[] = $family;
284 }
285 }
286 }
287
288 $preview_url = '';
289 if ( ! empty( $manifest['preview'] ) ) {
290 $preview_url = openstation_desktop_theme_asset_url( $manifest['preview'], $base_url, $asset_version );
291 }
292
293 $css_url = '';
294 $css_text = '';
295 if ( $is_upload ) {
296 $css_url = add_query_arg(
297 'ver',
298 (string) $installed_at,
299 openstation_desktop_themes_url( $slug ) . '/theme.css'
300 );
301 } else {
302 $css_text = isset( $entry['cssText'] ) ? (string) $entry['cssText'] : '';
303 }
304
305 // Recommendations are re-sanitized on the way OUT, not just on the
306 // way in. A stored manifest can predate a schema change (an enum
307 // value core dropped, a key a plugin stopped offering), and the
308 // shell must never be handed a value the current build no longer
309 // understands.
310 $recommended = openstation_sanitize_desktop_theme_recommended_os_settings(
311 isset( $manifest['recommendedOsSettings'] ) ? $manifest['recommendedOsSettings'] : null
312 );
313
314 return array(
315 'id' => isset( $manifest['id'] ) ? (string) $manifest['id'] : $slug,
316 'slug' => $slug,
317 'name' => isset( $manifest['name'] ) ? (string) $manifest['name'] : $slug,
318 'version' => isset( $manifest['version'] ) ? (string) $manifest['version'] : '',
319 'author' => isset( $manifest['author'] ) ? (string) $manifest['author'] : '',
320 'description' => isset( $manifest['description'] ) ? (string) $manifest['description'] : '',
321 'previewUrl' => $preview_url,
322 'cssUrl' => $css_url,
323 'cssText' => $css_text,
324 'tokens' => isset( $manifest['tokens'] ) && is_array( $manifest['tokens'] )
325 ? $manifest['tokens']
326 : array(),
327 // Informational, like `tokens`: the compiled stylesheet is what
328 // actually loads the faces. Shipped so `desktopThemes.list()`
329 // can tell a UI which families a theme brings with it without
330 // parsing CSS.
331 'fonts' => $font_families,
332 'icons' => $icons,
333 'iconColors' => $icon_colors,
334 'recommendedOsSettings' => $recommended,
335 'installedAt' => $installed_at,
336 'source' => $is_upload ? 'upload' : 'code',
337 );
338 }
339
340 /**
341 * Build the desktop-theme library for the shell payload.
342 *
343 * Uploaded themes win on a slug collision — a site admin who
344 * installed a theme by hand outranks a plugin that later registers
345 * the same slug from code.
346 *
347 * @return array[]
348 */
349 function openstation_build_desktop_themes_payload() {
350 $entries = array();
351
352 foreach ( openstation_desktop_theme_registry() as $slug => $entry ) {
353 $shaped = openstation_shape_desktop_theme_payload_entry( $entry, 'code' );
354 if ( $shaped ) {
355 $entries[ $slug ] = $shaped;
356 }
357 }
358 foreach ( openstation_desktop_themes_index() as $slug => $entry ) {
359 $shaped = openstation_shape_desktop_theme_payload_entry( $entry, 'upload' );
360 if ( $shaped ) {
361 $entries[ $slug ] = $shaped;
362 }
363 }
364
365 /**
366 * Filters the desktop-theme library before it ships to the shell.
367 *
368 * Keyed by slug. Entries carry the shape documented in
369 * `docs/desktop-themes.md`; removing one hides it from the
370 * picker without touching the stored files.
371 *
372 * @param array[] $entries Map of slug => payload entry.
373 */
374 $entries = apply_filters( 'openstation_desktop_themes', $entries );
375 if ( ! is_array( $entries ) ) {
376 return array();
377 }
378
379 $out = array();
380 foreach ( $entries as $entry ) {
381 if ( ! is_array( $entry ) || empty( $entry['slug'] ) ) {
382 continue;
383 }
384 $out[] = $entry;
385 }
386
387 // Stable, human-sensible order for the picker grid.
388 usort(
389 $out,
390 static function ( $a, $b ) {
391 return strcasecmp( (string) $a['name'], (string) $b['name'] );
392 }
393 );
394
395 return array_slice( $out, 0, openstation_desktop_themes_payload_cap() );
396 }
397