PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.2
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.2
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 / components.php

components.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.2, at includes/components.php

377 lines 12.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 — PHP helpers for plugin authors.
4 *
5 * Two companion helpers live here:
6 *
7 * - {@see desktop_mode_component()} prints a `<wpd-*>` tag with
8 * safely-escaped attributes. The intent is explicit (we're
9 * rendering a kit component, not arbitrary HTML) and the
10 * escape discipline is automatic.
11 *
12 * - {@see desktop_mode_register_window()} collapses the
13 * boilerplate for declaring a PHP-owned native window: one
14 * call emits the `<template>` the shell clones, enqueues
15 * the plugin's JS render bundle, and wires a dock tile on
16 * window-ready. Plugins write the template callback
17 * + the render callback on the JS side — the plumbing is ours.
18 *
19 * @package WPDesktopMode
20 * @since 0.10.0
21 */
22
23 defined( 'ABSPATH' ) || exit;
24
25 /**
26 * Output a `<wpd-*>` component with safely escaped attributes.
27 *
28 * ```php
29 * desktop_mode_component( 'wpd-button', array(
30 * 'variant' => 'primary',
31 * 'data-op' => 'add',
32 * 'aria-label' => __( 'Add', 'my-plugin' ),
33 * ), '+' );
34 * ```
35 *
36 * Attribute values flow through `esc_attr()` — no HTML injection
37 * surface. Content is passed through verbatim; callers that want
38 * to render user text should pre-escape with `esc_html()` /
39 * `wp_kses()` themselves.
40 *
41 * Boolean-style attributes (present with a `true` value or an
42 * empty string) render as bare attributes (`disabled`,
43 * `fill-cell`) — matches the HTML5 boolean-attribute convention
44 * every `<wpd-*>` follows.
45 *
46 * ## Inline styles
47 *
48 * The `style` key accepts either the usual string value or an
49 * associative array of CSS-property → value pairs. The array form
50 * auto-serializes to a CSS declaration list and auto-units bare
51 * integers on length-shaped properties (padding, margin, width,
52 * …) so `'padding' => 0` produces `padding: 0` and
53 * `'padding' => 16` produces `padding: 16px`.
54 *
55 * ```php
56 * desktop_mode_component( 'wpd-stack', array(
57 * 'gap' => 12,
58 * 'style' => array(
59 * 'padding' => 0,
60 * 'background' => 'rgba(0,0,0,0.04)',
61 * 'border-radius' => 8,
62 * ),
63 * ), $children );
64 * // <wpd-stack gap="12" style="padding: 0; background: rgba(0,0,0,0.04); border-radius: 8px">
65 * ```
66 *
67 * Plain string form (for one-line overrides) keeps working:
68 *
69 * ```php
70 * desktop_mode_component( 'wpd-stack', array(
71 * 'style' => 'padding: 0; margin-top: 16px',
72 * ), $children );
73 * ```
74 *
75 * @since 0.10.0
76 * @since 0.13.0 `style` accepts an array of CSS declarations.
77 *
78 * @param string $tag Tag name, e.g. `wpd-button`.
79 * Whitelisted to the `wpd-*` prefix
80 * to prevent the helper being
81 * misused as a generic HTML emitter.
82 * @param array<string,mixed> $attrs Attribute key/value pairs.
83 * `style` may be a string or an
84 * associative array (see above).
85 * @param string $content Inner HTML. Pass pre-escaped.
86 */
87 function desktop_mode_component( $tag, $attrs = array(), $content = '' ) {
88 $tag = strtolower( (string) $tag );
89 if ( ! preg_match( '/^wpd-[a-z][a-z0-9-]*$/', $tag ) ) {
90 // Fail loud in debug so a typo surfaces immediately; silently
91 // drop the output in production so a plugin with a bad tag
92 // doesn't blow up the page.
93 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
94 _doing_it_wrong(
95 __FUNCTION__,
96 sprintf(
97 /* translators: %s: the attempted tag name. */
98 esc_html__( 'desktop_mode_component() only accepts tags with the wpd- prefix; got "%s".', 'desktop-mode' ),
99 esc_html( $tag )
100 ),
101 '0.10.0'
102 );
103 }
104 return;
105 }
106
107 $attr_parts = array();
108 foreach ( (array) $attrs as $key => $value ) {
109 $key = (string) $key;
110 if ( ! preg_match( '/^[A-Za-z_][A-Za-z0-9_:.-]*$/', $key ) ) {
111 // Silently skip attribute names that don't match the HTML5
112 // name grammar. Same debug-vs-production split as the tag.
113 continue;
114 }
115 if ( false === $value || null === $value ) {
116 continue;
117 }
118 // Style array — serialize to a CSS declaration list. Plain
119 // string values fall through to the generic attribute path
120 // below so `'style' => 'padding:0'` keeps working.
121 if ( 'style' === strtolower( $key ) && is_array( $value ) ) {
122 $serialized = desktop_mode_serialize_style_array( $value );
123 if ( '' === $serialized ) {
124 continue;
125 }
126 $attr_parts[] = sprintf(
127 'style="%s"',
128 esc_attr( $serialized )
129 );
130 continue;
131 }
132 if ( true === $value || '' === $value ) {
133 // Boolean attribute — render bare.
134 $attr_parts[] = esc_attr( $key );
135 continue;
136 }
137 if ( is_array( $value ) || is_object( $value ) ) {
138 // Wrong-shape value on a non-style key. Without this
139 // guard PHP's string cast would emit `key="Array"` /
140 // `key="Object"` — embarrassing in production, silent
141 // in debug. Surface it loudly under WP_DEBUG and drop
142 // the attribute everywhere else.
143 _doing_it_wrong(
144 __FUNCTION__,
145 sprintf(
146 /* translators: 1: attribute name, 2: tag name. */
147 esc_html__( 'Attribute "%1$s" on <%2$s> received a non-scalar value (array/object). Only the `style` attribute accepts an array; other attributes must be strings, booleans, or null. The attribute was skipped.', 'desktop-mode' ),
148 esc_html( $key ),
149 esc_html( $tag )
150 ),
151 '0.18.0'
152 );
153 continue;
154 }
155 $attr_parts[] = sprintf(
156 '%s="%s"',
157 esc_attr( $key ),
158 esc_attr( (string) $value )
159 );
160 }
161
162 $attr_str = $attr_parts ? ' ' . implode( ' ', $attr_parts ) : '';
163
164 printf(
165 '<%1$s%2$s>%3$s</%1$s>',
166 // `$tag` is validated above against the wpd- allowlist; safe.
167 $tag, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
168 // `$attr_str` is pre-escaped via esc_attr() for each component.
169 $attr_str, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
170 // `$content` is the caller's responsibility to pre-escape.
171 $content // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
172 );
173 }
174
175 /**
176 * CSS properties that treat bare integers as pixels. Mirrors
177 * the length-shaped property list used by plugin JS code when
178 * interpreting raw numeric values — keeping the same list in
179 * one place so PHP `'padding' => 16` and JS `padding: 16` make
180 * the same visual decision.
181 *
182 * @since 0.13.0
183 */
184 const DESKTOP_MODE_LENGTH_CSS_PROPERTIES = array(
185 'width', 'height',
186 'min-width', 'min-height', 'max-width', 'max-height',
187 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
188 'padding-inline', 'padding-inline-start', 'padding-inline-end',
189 'padding-block', 'padding-block-start', 'padding-block-end',
190 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
191 'margin-inline', 'margin-inline-start', 'margin-inline-end',
192 'margin-block', 'margin-block-start', 'margin-block-end',
193 'gap', 'row-gap', 'column-gap',
194 'border-width', 'border-top-width', 'border-right-width',
195 'border-bottom-width', 'border-left-width',
196 'border-radius',
197 'border-top-left-radius', 'border-top-right-radius',
198 'border-bottom-left-radius', 'border-bottom-right-radius',
199 'top', 'right', 'bottom', 'left',
200 'inset',
201 'inset-inline-start', 'inset-inline-end',
202 'inset-block-start', 'inset-block-end',
203 'font-size', 'letter-spacing', 'word-spacing', 'text-indent',
204 'outline-width', 'outline-offset',
205 );
206
207 /**
208 * Serialize an associative array of CSS declarations into a
209 * `prop: value; prop: value` string for the `style` attribute.
210 *
211 * Property names are validated as CSS-shaped (kebab-case letters,
212 * digits, hyphens); malformed names are silently dropped. Bare
213 * integer values on length-shaped properties auto-unit to `px`
214 * so callers can write `'padding' => 16` without remembering the
215 * unit. The literal `0` is left unit-less because CSS treats it
216 * as dimensionally valid on any property.
217 *
218 * @since 0.13.0
219 *
220 * @param array<string,mixed> $styles
221 * @return string CSS declaration list, or empty string when no
222 * valid declarations were produced.
223 */
224 function desktop_mode_serialize_style_array( $styles ) {
225 if ( ! is_array( $styles ) ) {
226 return '';
227 }
228 $parts = array();
229 foreach ( $styles as $prop => $value ) {
230 $prop = strtolower( trim( (string) $prop ) );
231 if ( ! preg_match( '/^-?[a-z][a-z0-9-]*$/', $prop ) ) {
232 continue;
233 }
234 if ( false === $value || null === $value ) {
235 continue;
236 }
237 $serialized = desktop_mode_format_css_value( $prop, $value );
238 if ( '' === $serialized ) {
239 continue;
240 }
241 $parts[] = $prop . ': ' . $serialized;
242 }
243 return implode( '; ', $parts );
244 }
245
246 /**
247 * Serialize a raw PHP value into a CSS declaration value.
248 *
249 * Handles the two conveniences callers want from an ergonomic
250 * style array:
251 *
252 * - Integer + length-shaped property → append `px`
253 * (`'padding' => 16` → `16px`).
254 * - Integer `0` → keep unit-less (`0` is valid everywhere).
255 *
256 * Everything else (strings, floats already unitted, calc(…)
257 * expressions, color keywords) passes through verbatim.
258 *
259 * @since 0.13.0
260 *
261 * @param string $property CSS property name.
262 * @param mixed $value Raw value (int, float, string).
263 * @return string CSS value, or empty string when $value is
264 * not serializable.
265 */
266 function desktop_mode_format_css_value( $property, $value ) {
267 if ( is_bool( $value ) || null === $value ) {
268 return '';
269 }
270 $text = trim( (string) $value );
271 if ( '' === $text ) {
272 return '';
273 }
274 if ( preg_match( '/^-?\d+(\.\d+)?$/', $text ) ) {
275 if ( '0' === $text ) {
276 return '0';
277 }
278 if ( in_array( $property, DESKTOP_MODE_LENGTH_CSS_PROPERTIES, true ) ) {
279 return $text . 'px';
280 }
281 }
282 return $text;
283 }
284
285
286 // Native-windows registry (register_window, allowed_html,
287 // template-html builder, enqueue + render hooks) was moved to
288 // `includes/registries/native-windows.php` in 0.8.1.
289
290
291
292 // Widgets registry was moved to
293 // `includes/registries/widgets.php` in 0.8.1.
294
295
296
297 // Wallpapers registry was moved to
298 // `includes/registries/wallpapers.php` in 0.8.1.
299
300
301 // Desktop-icons registry was moved to
302 // `includes/registries/icons.php` in 0.8.1.
303
304
305
306 // Native-window tabs registry was moved to
307 // `includes/registries/window-tabs.php` in 0.8.1.
308
309
310 /**
311 * Enqueue a plugin script that extends the desktop shell.
312 *
313 * Thin wrapper around `wp_enqueue_script` that pre-wires the correct
314 * dependencies so the script:
315 *
316 * - Runs AFTER `desktop-mode` (the shell bundle) so `wp.desktop.*` is
317 * guaranteed available.
318 * - Runs AFTER `wp-hooks` so `wp.hooks.addAction( 'desktop-mode.init', ... )`
319 * works without the plugin author having to remember that dep.
320 * - Is only enqueued in the admin (shell only boots there).
321 *
322 * Drop-in replacement for the boilerplate:
323 *
324 * ```php
325 * add_action( 'admin_enqueue_scripts', function () {
326 * wp_enqueue_script(
327 * 'my-plugin',
328 * plugins_url( 'my-plugin.js', __FILE__ ),
329 * array( 'desktop-mode', 'wp-hooks' ),
330 * '1.0.0',
331 * true
332 * );
333 * } );
334 * ```
335 *
336 * which becomes:
337 *
338 * ```php
339 * add_action( 'admin_enqueue_scripts', function () {
340 * desktop_mode_enqueue_script(
341 * 'my-plugin',
342 * plugins_url( 'my-plugin.js', __FILE__ ),
343 * array(), // extra deps on top of the desktop defaults
344 * '1.0.0'
345 * );
346 * } );
347 * ```
348 *
349 * @since 0.14.0
350 *
351 * @param string $handle Script handle.
352 * @param string $src Full URL of the script, or path relative
353 * to the WordPress root directory.
354 * @param string[] $extra_deps Additional dependency handles. `desktop-mode`
355 * and `wp-hooks` are always prepended.
356 * @param string|bool|null $version Version string, or `false` for none.
357 * Defaults to `DESKTOP_MODE_VERSION` so plugin authors
358 * don't have to busy-track cache busting.
359 * @param bool $in_footer Whether to enqueue in the footer. Defaults
360 * to `true` — the shell is always in head.
361 * @return void
362 */
363 function desktop_mode_enqueue_script( $handle, $src, $extra_deps = array(), $version = null, $in_footer = true ) {
364 $deps = array_merge(
365 array( 'desktop-mode', 'wp-hooks' ),
366 is_array( $extra_deps ) ? $extra_deps : array()
367 );
368
369 wp_enqueue_script(
370 $handle,
371 $src,
372 $deps,
373 null === $version ? DESKTOP_MODE_VERSION : $version,
374 $in_footer
375 );
376 }
377