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 / components.php

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

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