| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation helper functions. |
| 4 |
* |
| 5 |
* @package OpenStation |
| 6 |
*/ |
| 7 |
|
| 8 |
defined( 'ABSPATH' ) || exit; |
| 9 |
|
| 10 |
/** |
| 11 |
* Filename suffix for built JS/CSS bundles: `.min` in production, |
| 12 |
* `''` (the unminified dev build) under SCRIPT_DEBUG. |
| 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 |
* |
| 41 |
* Two gates, both must pass: |
| 42 |
* |
| 43 |
* 1. The user's `desktop_mode_mode` user-meta is `'1'` (the per-user |
| 44 |
* opt-in toggle the admin-bar button writes via the AJAX endpoint). |
| 45 |
* 2. The `openstation_mode_enabled` filter returns truthy for that user. |
| 46 |
* |
| 47 |
* Centralising the filter check here means render-time gates (chromeless |
| 48 |
* detection, payload generation, REST permission callbacks) can rely on |
| 49 |
* a single helper instead of every call site re-running the filter. |
| 50 |
* A user whose meta is `'1'` but whose filter denies them is treated as |
| 51 |
* not-enabled everywhere, which is the documented contract of the |
| 52 |
* filter — see docs/examples/gate-by-role.md. |
| 53 |
* |
| 54 |
* @param int $user_id Optional. User ID to check. Defaults to the |
| 55 |
* current user. |
| 56 |
* @return bool True if the user has OpenStation active. |
| 57 |
*/ |
| 58 |
function openstation_is_enabled( $user_id = 0 ) { |
| 59 |
$user_id = (int) $user_id; |
| 60 |
if ( $user_id <= 0 ) { |
| 61 |
if ( ! is_user_logged_in() ) { |
| 62 |
return false; |
| 63 |
} |
| 64 |
$user_id = get_current_user_id(); |
| 65 |
} |
| 66 |
|
| 67 |
if ( '1' !== (string) get_user_meta( $user_id, 'desktop_mode_mode', true ) ) { |
| 68 |
return false; |
| 69 |
} |
| 70 |
|
| 71 |
/** |
| 72 |
* Filters whether OpenStation is available for this user. |
| 73 |
* |
| 74 |
* See `docs/hooks-reference.md` (`openstation_mode_enabled`) for the |
| 75 |
* full contract. Returning `false` here makes the helper return |
| 76 |
* `false` for the user even when their meta is set, which propagates |
| 77 |
* to every render-time gate that consults the helper. |
| 78 |
* |
| 79 |
* @param bool $enabled Whether OpenStation is enabled. Default true. |
| 80 |
* @param int $user_id The user ID being checked. |
| 81 |
*/ |
| 82 |
return (bool) apply_filters( 'openstation_mode_enabled', true, $user_id ); |
| 83 |
} |
| 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 |
|
| 128 |
// Chromeless / classic admin-bar suppression and the `wp_redirect` |
| 129 |
// flag-preservation filter pair were moved to |
| 130 |
// `includes/core/routing.php`. The functions and the |
| 131 |
// add_filter / add_action hookings live there now; this file |
| 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. |
| 136 |
|
| 137 |
/** |
| 138 |
* `openstation_is_chromeless_request()` and `openstation_is_classic_request()` |
| 139 |
* were moved to `includes/core/routing.php` — see that |
| 140 |
* file for the canonical definitions. The function names didn't |
| 141 |
* change; PHP looks them up by name at call time, so every |
| 142 |
* existing caller (helpers, render, hooks) keeps working. |
| 143 |
*/ |
| 144 |
|
| 145 |
/** |
| 146 |
* Returns the default wallpaper id used when a user has no saved |
| 147 |
* selection (or their saved selection was unregistered by a plugin |
| 148 |
* deactivation). |
| 149 |
* |
| 150 |
* Exposed as a filter so themes/plugins can set a site-wide default |
| 151 |
* without forking the TS build. |
| 152 |
* |
| 153 |
* ```php |
| 154 |
* add_filter( 'openstation_default_wallpaper', function () { |
| 155 |
* return 'my-plugin/brand'; |
| 156 |
* } ); |
| 157 |
* ``` |
| 158 |
* |
| 159 |
* The returned string is passed through `sanitize_key()` so a filter |
| 160 |
* that returns an invalid slug degrades to the empty string (and the |
| 161 |
* shell falls back to its hard-coded default preset). |
| 162 |
* |
| 163 |
* @return string Wallpaper id. Empty string if the filter returns |
| 164 |
* an invalid value. |
| 165 |
*/ |
| 166 |
function openstation_get_default_wallpaper() { |
| 167 |
/** |
| 168 |
* Filters the wallpaper id loaded on first boot / new user. |
| 169 |
* |
| 170 |
* @param string $id Default wallpaper slug. |
| 171 |
*/ |
| 172 |
$id = apply_filters( 'openstation_default_wallpaper', 'galaxy' ); |
| 173 |
if ( ! is_string( $id ) ) { |
| 174 |
return ''; |
| 175 |
} |
| 176 |
return sanitize_key( $id ); |
| 177 |
} |
| 178 |
|
| 179 |
/** |
| 180 |
* The site's own name, ready to use as a window / icon title. |
| 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 (`&`, `'`). 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 & 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 |
* `&`, an apostrophe as `’` — 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. |
| 230 |
* |
| 231 |
* Decode BEFORE the tag strip, never after: `<script>` decodes |
| 232 |
* into a real tag, and stripping second is what removes it. |
| 233 |
* |
| 234 |
* @param string $rendered A title that has been through a display filter. |
| 235 |
* @return string Plain text, tag-free. |
| 236 |
*/ |
| 237 |
function openstation_plain_text_title( $rendered ) { |
| 238 |
$decoded = html_entity_decode( |
| 239 |
(string) $rendered, |
| 240 |
ENT_QUOTES, |
| 241 |
get_bloginfo( 'charset' ) |
| 242 |
); |
| 243 |
|
| 244 |
return trim( wp_strip_all_tags( $decoded ) ); |
| 245 |
} |
| 246 |
|
| 247 |
/** |
| 248 |
* Build a `WP_Error` for a openstation registration failure. |
| 249 |
* |
| 250 |
* Centralises the error-code vocabulary used by every |
| 251 |
* `openstation_register_*()` function so plugin authors see a |
| 252 |
* consistent contract. The canonical error-code list lives in |
| 253 |
* `docs/hooks-reference.md`. |
| 254 |
* |
| 255 |
* @param string $code Short error slug (e.g. `openstation_missing_title`). |
| 256 |
* @param string $message Human-readable message. Should be translated. |
| 257 |
* @param array $data Optional extra context attached to the error. |
| 258 |
* @return WP_Error |
| 259 |
*/ |
| 260 |
function openstation_registration_error( $code, $message, $data = array() ) { |
| 261 |
return new WP_Error( |
| 262 |
(string) $code, |
| 263 |
(string) $message, |
| 264 |
is_array( $data ) ? $data : array() |
| 265 |
); |
| 266 |
} |
| 267 |
|
| 268 |
// `openstation_url_is_same_admin()`, |
| 269 |
// `openstation_resolve_admin_target()` and |
| 270 |
// `openstation_admin_target_allowlist()` were moved to |
| 271 |
// `includes/core/routing.php` — see that file for the |
| 272 |
// canonical definitions. Function names didn't change; PHP's |
| 273 |
// runtime resolution finds them across the module split. |
| 274 |
|
| 275 |
|
| 276 |
// Dock building, menu / native-windows payload assembly and the |
| 277 |
// script/style handle resolvers were moved to |
| 278 |
// `includes/core/payload.php`. Function names didn't |
| 279 |
// change; existing callers find them via PHP's runtime function |
| 280 |
// resolution. desktop-mode.php loads payload.php right after |
| 281 |
// helpers.php so the foundational helpers (openstation_is_enabled |
| 282 |
// etc.) are present when payload functions are invoked. |
| 283 |
|