PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 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 All 36 releases
← All changes | includes/helpers.php +205 -41 0.8.9 → 1.1.12 View file →
@@ -1,21 +1,49 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode helper functions.
3 + * OpenStation helper functions.
4 4 *
5 - * @package WPDesktopMode
5 + * @package OpenStation
6 6 */
7 7
8 8 defined( 'ABSPATH' ) || exit;
9 9
10 10 /**
11 - * Checks whether a user has desktop mode enabled.
11 + * Filename suffix for built JS/CSS bundles: `.min` in production,
12 + * `''` (the unminified dev build) under SCRIPT_DEBUG.
12 13 *
14 + * Centralised because the SCRIPT_DEBUG branch needs a guard the old
15 + * per-file ternaries didn't have: release zips ship the minified
16 + * bundles only (the ~4–5 MB of dev bundles are a source-checkout
17 + * artifact — see bin/package.sh), so a production site that happens
18 + * to define SCRIPT_DEBUG would otherwise request dev files that
19 + * don't exist and 404 every openstation script. Probe one
20 + * canonical dev bundle; if it's absent, this is a minified-only
21 + * install and `.min` is the only truth available.
22 + *
23 + * @return string `'.min'` or `''`.
24 + */
25 +function openstation_asset_suffix() {
26 + static $suffix = null;
27 + if ( null !== $suffix ) {
28 + return $suffix;
29 + }
30 + if ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ) {
31 + $suffix = '.min';
32 + return $suffix;
33 + }
34 + $suffix = file_exists( OPENSTATION_DIR . 'assets/js/desktop.js' ) ? '' : '.min';
35 + return $suffix;
36 +}
37 +
38 +/**
39 + * Checks whether a user has OpenStation enabled.
40 + *
13 41 * Two gates, both must pass:
14 42 *
15 43 * 1. The user's `desktop_mode_mode` user-meta is `'1'` (the per-user
16 44 * opt-in toggle the admin-bar button writes via the AJAX endpoint).
17 - * 2. The `desktop_mode_mode_enabled` filter returns truthy for that user.
45 + * 2. The `openstation_mode_enabled` filter returns truthy for that user.
18 46 *
19 47 * Centralising the filter check here means render-time gates (chromeless
20 48 * detection, payload generation, REST permission callbacks) can rely on
21 49 * a single helper instead of every call site re-running the filter.
@@ -22,15 +50,13 @@
22 50 * A user whose meta is `'1'` but whose filter denies them is treated as
23 51 * not-enabled everywhere, which is the documented contract of the
24 52 * filter — see docs/examples/gate-by-role.md.
25 53 *
26 - * @since 0.1.0
27 - *
28 54 * @param int $user_id Optional. User ID to check. Defaults to the
29 55 * current user.
30 - * @return bool True if the user has desktop mode active.
56 + * @return bool True if the user has OpenStation active.
31 57 */
32 -function desktop_mode_is_enabled( $user_id = 0 ) {
58 +function openstation_is_enabled( $user_id = 0 ) {
33 59 $user_id = (int) $user_id;
34 60 if ( $user_id <= 0 ) {
35 61 if ( ! is_user_logged_in() ) {
36 62 return false;
@@ -42,35 +68,76 @@
42 68 return false;
43 69 }
44 70
45 71 /**
46 - * Filters whether desktop mode is available for this user.
72 + * Filters whether OpenStation is available for this user.
47 73 *
48 - * See `docs/hooks-reference.md` (`desktop_mode_mode_enabled`) for the
74 + * See `docs/hooks-reference.md` (`openstation_mode_enabled`) for the
49 75 * full contract. Returning `false` here makes the helper return
50 76 * `false` for the user even when their meta is set, which propagates
51 77 * to every render-time gate that consults the helper.
52 78 *
53 - * @since 0.1.0
54 - *
55 - * @param bool $enabled Whether desktop mode is enabled. Default true.
79 + * @param bool $enabled Whether OpenStation is enabled. Default true.
56 80 * @param int $user_id The user ID being checked.
57 81 */
58 - return (bool) apply_filters( 'desktop_mode_mode_enabled', true, $user_id );
82 + return (bool) apply_filters( 'openstation_mode_enabled', true, $user_id );
59 83 }
60 84
85 +/**
86 + * Shared REST permission gate for OpenStation's per-user endpoints.
87 + *
88 + * Routes that only ever read or write the *current* user's own Desktop
89 + * Mode state (OS settings, session, default-window, seen-intros, PWA
90 + * state, presence) must not be reachable by accounts that haven't
91 + * actually entered OpenStation.
92 + *
93 + * `current_user_can( 'read' )` alone is too loose: every authenticated
94 + * role — Subscriber included — carries `read`, so the old gate let any
95 + * logged-in user touch these routes without ever enabling OpenStation.
96 + * We gate on {@see openstation_is_enabled()} instead (the same opt-in +
97 + * `openstation_mode_enabled` filter the shell itself uses) and return
98 + * the conventional 401/403 split so REST clients can tell "log in" from
99 + * "not allowed".
100 + *
101 + * This is the canonical gate; `openstation_presence_rest_permission()`
102 + * pioneered the shape and now delegates here.
103 + *
104 + * @return true|WP_Error True when allowed; a `rest_forbidden` WP_Error
105 + * (401 when logged out, 403 when OpenStation is
106 + * not enabled for the account) otherwise.
107 + */
108 +function openstation_rest_require_enabled() {
109 + if ( ! is_user_logged_in() ) {
110 + return new WP_Error(
111 + 'rest_forbidden',
112 + __( 'Authentication required.', 'desktop-mode' ),
113 + array( 'status' => 401 )
114 + );
115 + }
116 +
117 + if ( ! openstation_is_enabled() ) {
118 + return new WP_Error(
119 + 'rest_forbidden',
120 + __( 'OpenStation is not enabled for your account.', 'desktop-mode' ),
121 + array( 'status' => 403 )
122 + );
123 + }
124 +
125 + return true;
126 +}
127 +
61 128 // Chromeless / classic admin-bar suppression and the `wp_redirect`
62 129 // flag-preservation filter pair were moved to
63 -// `includes/core/routing.php` in 0.8.1. The functions and the
130 +// `includes/core/routing.php`. The functions and the
64 131 // add_filter / add_action hookings live there now; this file
65 -// remains the home of `desktop_mode_is_chromeless_request()` and
66 -// `desktop_mode_is_classic_request()` (called from the routing
67 -// helpers at hook-fire time), which is why
68 -// `desktop-mode.php` requires routing.php BEFORE helpers.php.
132 +// remains the home of `openstation_is_enabled()` (called from the
133 +// routing helpers at hook-fire time, after every include has
134 +// loaded), which is why `desktop-mode.php` can safely require
135 +// routing.php BEFORE helpers.php.
69 136
70 137 /**
71 - * `desktop_mode_is_chromeless_request()` and `desktop_mode_is_classic_request()`
72 - * were moved to `includes/core/routing.php` in 0.8.1 — see that
138 + * `openstation_is_chromeless_request()` and `openstation_is_classic_request()`
139 + * were moved to `includes/core/routing.php` — see that
73 140 * file for the canonical definitions. The function names didn't
74 141 * change; PHP looks them up by name at call time, so every
75 142 * existing caller (helpers, render, hooks) keeps working.
76 143 */
@@ -83,9 +150,9 @@
83 150 * Exposed as a filter so themes/plugins can set a site-wide default
84 151 * without forking the TS build.
85 152 *
86 153 * ```php
87 - * add_filter( 'desktop_mode_default_wallpaper', function () {
154 + * add_filter( 'openstation_default_wallpaper', function () {
88 155 * return 'my-plugin/brand';
89 156 * } );
90 157 * ```
91 158 *
@@ -90,24 +157,20 @@
90 157 * ```
91 158 *
92 159 * The returned string is passed through `sanitize_key()` so a filter
93 160 * that returns an invalid slug degrades to the empty string (and the
94 - * shell falls back to its hard-coded `'dark'` preset).
161 + * shell falls back to its hard-coded default preset).
95 162 *
96 - * @since 0.11.0
97 - *
98 163 * @return string Wallpaper id. Empty string if the filter returns
99 164 * an invalid value.
100 165 */
101 -function desktop_mode_get_default_wallpaper() {
166 +function openstation_get_default_wallpaper() {
102 167 /**
103 168 * Filters the wallpaper id loaded on first boot / new user.
104 169 *
105 - * @since 0.11.0
106 - *
107 170 * @param string $id Default wallpaper slug.
108 171 */
109 - $id = apply_filters( 'desktop_mode_default_wallpaper', 'dark' );
172 + $id = apply_filters( 'openstation_default_wallpaper', 'galaxy' );
110 173 if ( ! is_string( $id ) ) {
111 174 return '';
112 175 }
113 176 return sanitize_key( $id );
@@ -113,23 +176,124 @@
113 176 return sanitize_key( $id );
114 177 }
115 178
116 179 /**
117 - * Build a `WP_Error` for a desktop-mode registration failure.
180 + * The site's own name, ready to use as a window / icon title.
118 181 *
182 + * The desktop shows objects, not the software running it — so the
183 + * *root folder* of a site's content is named after the site itself
184 + * ("Izzi's Gym"), not after WordPress. That is the breadcrumb root and
185 + * the Content Graph's site label; the app that browses it is called WP
186 + * Explorer, which is a different string (see
187 + * `openstation_my_wordpress_app_title()`). This is the single source
188 + * for the site one.
189 + *
190 + * `get_bloginfo( 'name' )` returns the display-filtered option, which
191 + * carries HTML entities (`&amp;`, `&#039;`). Titles land in
192 + * `title=` attributes and JS-rendered text nodes, so the entities are
193 + * decoded here — leaving them encoded would render a literal
194 + * `Ben &amp; Jerry` on the desktop.
195 + *
196 + * @return string Decoded site title. Falls back to `WordPress` when
197 + * the site has no name set.
198 + */
199 +function openstation_site_title() {
200 + $title = wp_specialchars_decode( (string) get_bloginfo( 'name' ), ENT_QUOTES );
201 + $title = trim( $title );
202 +
203 + if ( '' === $title ) {
204 + $title = __( 'WordPress', 'desktop-mode' );
205 + }
206 +
207 + /**
208 + * Filters the site title used for openstation window and icon
209 + * titles — WP Explorer's breadcrumb root, the Content Graph's site
210 + * label, and any "Open in <site>" action.
211 + *
212 + * Return a different string to label the desktop objects after
213 + * something other than `blogname` (a brand, a network name, a
214 + * per-user workspace label).
215 + *
216 + * @param string $title Decoded site title, never empty.
217 + */
218 + $filtered = apply_filters( 'openstation_site_title', $title );
219 +
220 + return is_string( $filtered ) && '' !== trim( $filtered ) ? $filtered : $title;
221 +}
222 +
223 +/**
224 + * Decode a rendered title into the plain text the desktop paints with.
225 + *
226 + * `wptexturize()` encodes the characters titles are full of — `&` as
227 + * `&#038;`, an apostrophe as `&#8217;` — and the shell writes titles
228 + * into text nodes, where the entity renders as itself. Same reasoning
229 + * as {@see openstation_site_title()}, one layer down. Stored names need
230 + * it too: a display name, term name or comment author is saved with
231 + * `&` as `&amp;`, and so is a title kses filtered on save.
232 + *
233 + * Decode BEFORE the tag strip, never after: `&lt;script&gt;` decodes
234 + * into a real tag, and stripping second is what removes it.
235 + *
236 + * A `<` opens a tag only before an ASCII letter, `/`, `!` or `?`, the
237 + * HTML tokenizer's rule, whether or not a `>` closes it. Any other `<`
238 + * is text (`I <3 WordPress`), and `strip_tags()` on its own would drop
239 + * it with everything after it.
240 + *
241 + * @param string $rendered A title or name, rendered or as stored.
242 + * @return string Plain text, tag-free.
243 + */
244 +function openstation_plain_text_title( $rendered ) {
245 + $decoded = html_entity_decode(
246 + (string) $rendered,
247 + ENT_QUOTES,
248 + get_bloginfo( 'charset' )
249 + );
250 +
251 + // A `<` that opens no tag sits out the strip as `&lt;`. `&` is
252 + // escaped first and restored last, so an `&lt;` the decode left as
253 + // text (`&amp;lt;` in the source) does not come back as a `<` too.
254 + $tag_start = '[a-zA-Z\/!?]';
255 + $text = str_replace( '&', '&amp;', $decoded );
256 + $text = openstation_strip_all_tags( $text );
257 + $text = str_replace( '&lt;', '<', $text );
258 + $text = str_replace( '&amp;', '&', $text );
259 +
260 + // Removing a tag can leave a kept `<` against the text after it
261 + // (`<<b>script>`): a space stops the pair from reading as a tag.
262 + return trim( preg_replace( "/<(?={$tag_start})/", '< ', $text ) );
263 +}
264 +
265 +/**
266 + * Strip the tags from stored HTML, keeping a `<` that opens no tag.
267 + *
268 + * A `<` opens a tag only before an ASCII letter, `/`, `!` or `?`. Any
269 + * other one comes back as `&lt;`, the form kses saves it in, so the
270 + * result is still HTML text: `wp_trim_words()` and `wp_html_excerpt()`,
271 + * which strip tags themselves, pass it on to whatever decodes last.
272 + *
273 + * @param string $html Stored HTML.
274 + * @return string Tag-free text, entities still encoded.
275 + */
276 +function openstation_strip_all_tags( $html ) {
277 + return wp_strip_all_tags(
278 + preg_replace( '/<(?![a-zA-Z\/!?])/', '&lt;', (string) $html )
279 + );
280 +}
281 +
282 +/**
283 + * Build a `WP_Error` for a openstation registration failure.
284 + *
119 285 * Centralises the error-code vocabulary used by every
120 - * `desktop_mode_register_*()` function so plugin authors see a
286 + * `openstation_register_*()` function so plugin authors see a
121 287 * consistent contract. The canonical error-code list lives in
122 288 * `docs/hooks-reference.md`.
123 289 *
124 - * @since 0.11.0
125 - *
126 - * @param string $code Short error slug (e.g. `desktop_mode_missing_title`).
290 + * @param string $code Short error slug (e.g. `openstation_missing_title`).
127 291 * @param string $message Human-readable message. Should be translated.
128 292 * @param array $data Optional extra context attached to the error.
129 293 * @return WP_Error
130 294 */
131 -function desktop_mode_registration_error( $code, $message, $data = array() ) {
295 +function openstation_registration_error( $code, $message, $data = array() ) {
132 296 return new WP_Error(
133 297 (string) $code,
134 298 (string) $message,
135 299 is_array( $data ) ? $data : array()
@@ -135,12 +299,12 @@
135 299 is_array( $data ) ? $data : array()
136 300 );
137 301 }
138 302
139 -// `desktop_mode_url_is_same_admin()`,
140 -// `desktop_mode_resolve_admin_target()` and
141 -// `desktop_mode_admin_target_allowlist()` were moved to
142 -// `includes/core/routing.php` in 0.8.1 — see that file for the
303 +// `openstation_url_is_same_admin()`,
304 +// `openstation_resolve_admin_target()` and
305 +// `openstation_admin_target_allowlist()` were moved to
306 +// `includes/core/routing.php` — see that file for the
143 307 // canonical definitions. Function names didn't change; PHP's
144 308 // runtime resolution finds them across the module split.
145 309
146 310
@@ -145,9 +309,9 @@
145 309
146 310
147 311 // Dock building, menu / native-windows payload assembly and the
148 312 // script/style handle resolvers were moved to
149 -// `includes/core/payload.php` in 0.8.1. Function names didn't
313 +// `includes/core/payload.php`. Function names didn't
150 314 // change; existing callers find them via PHP's runtime function
151 315 // resolution. desktop-mode.php loads payload.php right after
152 -// helpers.php so the foundational helpers (desktop_mode_is_enabled
316 +// helpers.php so the foundational helpers (openstation_is_enabled
153 317 // etc.) are present when payload functions are invoked.