PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
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 0.8.6 All 33 releases
desktop-mode / includes / default-window.php

default-window.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.8, at includes/default-window.php

258 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Default window preference.
4 *
5 * Stores the user's choice of "what window opens when I enter the
6 * desktop with nothing currently in session." Two signals matter:
7 *
8 * 1. `enabled` — false when the user has explicitly opted out of
9 * auto-opening anything. Entering an empty session under this
10 * flag gives the user a blank desktop, no surprise Dashboard.
11 * 2. `url` — the admin URL that opens when enabled. Populated on
12 * first configure from whichever window the user marks as
13 * their startup window.
14 *
15 * Stored as a serialized array on user-meta `desktop_mode_default_window`.
16 * A missing meta entry is treated as `{ enabled: true, url: <dashboard> }`
17 * for backward compatibility with older installs.
18 *
19 * @package OpenStation
20 */
21
22 defined( 'ABSPATH' ) || exit;
23
24 /**
25 * User-meta key.
26 *
27 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
28 * persisted or externally-visible identifier, so renaming it would
29 * orphan data already written by live installs (or break a live
30 * URL). The mismatch between this constant's name and its value is
31 * deliberate — it is NOT a half-finished rename.
32 */
33 const OPENSTATION_DEFAULT_WINDOW_META = 'desktop_mode_default_window';
34
35 /**
36 * Fetch the user's default-window preference as a normalized array.
37 *
38 * @param int $user_id User ID. Falls back to the current user when 0.
39 * @return array{enabled: bool, url: string} Always returns both keys.
40 */
41 function openstation_get_default_window( $user_id = 0 ) {
42 $user_id = $user_id ? (int) $user_id : get_current_user_id();
43 $fallback_url = admin_url( 'index.php' );
44 $default = array(
45 'enabled' => true,
46 'url' => $fallback_url,
47 );
48
49 if ( ! $user_id ) {
50 return $default;
51 }
52
53 $raw = get_user_meta( $user_id, OPENSTATION_DEFAULT_WINDOW_META, true );
54 if ( ! is_array( $raw ) ) {
55 return $default;
56 }
57
58 $enabled = ! empty( $raw['enabled'] );
59 $url = isset( $raw['url'] ) && is_string( $raw['url'] ) ? $raw['url'] : $fallback_url;
60 if ( '' === $url ) {
61 $url = $fallback_url;
62 }
63
64 return array(
65 'enabled' => $enabled,
66 'url' => $url,
67 );
68 }
69
70 /**
71 * Persist the user's default-window preference.
72 *
73 * Passing `null` for $url disables the default entirely — the shell
74 * will open an empty desktop on portal entry. Passing a URL enables
75 * the default and sets it.
76 *
77 * @param int $user_id User ID. Must be positive.
78 * @param string|null $url URL to set, or null to disable.
79 * @return bool True on success, false on invalid URL or unknown user.
80 */
81 function openstation_set_default_window( $user_id, $url ) {
82 $user_id = (int) $user_id;
83 if ( $user_id <= 0 ) {
84 return false;
85 }
86
87 if ( null === $url ) {
88 update_user_meta(
89 $user_id,
90 OPENSTATION_DEFAULT_WINDOW_META,
91 array(
92 'enabled' => false,
93 'url' => admin_url( 'index.php' ),
94 )
95 );
96 return true;
97 }
98
99 $clean = openstation_validate_default_window_url( $url );
100 if ( '' === $clean ) {
101 return false;
102 }
103
104 update_user_meta(
105 $user_id,
106 OPENSTATION_DEFAULT_WINDOW_META,
107 array(
108 'enabled' => true,
109 'url' => $clean,
110 )
111 );
112 return true;
113 }
114
115 /**
116 * Accept either a `native:<slug>` marker for a registered native
117 * window, or a URL that resolves to a same-origin `wp-admin/` path.
118 * A stricter net than `esc_url_raw` because the value flows back into
119 * the portal-entry redirect — we don't want an attacker's CSRF-seeded
120 * preference to hijack the user into an off-site landing page.
121 *
122 * @param string $url Raw input.
123 * @return string Fully-qualified admin URL or `native:<slug>` marker, or empty string if rejected.
124 */
125 function openstation_validate_default_window_url( $url ) {
126 $url = trim( (string) $url );
127 if ( '' === $url ) {
128 return '';
129 }
130
131 // Native-window marker: "native:<slug>" stores a registered native
132 // window id (OS Settings, Recycle Bin, plugin-registered native
133 // apps) instead of an admin URL. The slug must match
134 // /^[a-z0-9_-]+$/i so a malicious save cannot smuggle path
135 // traversal or whitespace through the marker. The shell handles
136 // the actual open-on-startup at boot via nativeWindows.openById.
137 if ( 0 === strpos( $url, 'native:' ) ) {
138 $slug = substr( $url, strlen( 'native:' ) );
139 if ( '' === $slug || ! preg_match( '/^[a-z0-9_\-]+$/i', $slug ) ) {
140 return '';
141 }
142 return 'native:' . $slug;
143 }
144
145 // Allow same-origin http(s) URLs only.
146 $parsed = wp_parse_url( $url );
147 if ( ! is_array( $parsed ) || empty( $parsed['path'] ) ) {
148 return '';
149 }
150
151 $home_origin = wp_parse_url( home_url( '/' ) );
152 $url_host = isset( $parsed['host'] ) ? strtolower( $parsed['host'] ) : '';
153 $url_scheme = isset( $parsed['scheme'] ) ? strtolower( $parsed['scheme'] ) : '';
154 $home_host = is_array( $home_origin ) && isset( $home_origin['host'] ) ? strtolower( $home_origin['host'] ) : '';
155 $home_scheme = is_array( $home_origin ) && isset( $home_origin['scheme'] ) ? strtolower( $home_origin['scheme'] ) : '';
156
157 if ( '' !== $url_host && $url_host !== $home_host ) {
158 return '';
159 }
160 if ( '' !== $url_scheme && ! in_array( $url_scheme, array( 'http', 'https' ), true ) ) {
161 return '';
162 }
163 // Make sure the path is inside wp-admin/.
164 $admin_path = wp_parse_url( admin_url(), PHP_URL_PATH );
165 if ( ! is_string( $admin_path ) ) {
166 return '';
167 }
168 if ( 0 !== strpos( $parsed['path'], $admin_path ) ) {
169 return '';
170 }
171
172 // Reassemble as a clean same-origin URL so downstream consumers
173 // always get a fully-qualified string.
174 $query = isset( $parsed['query'] ) ? '?' . $parsed['query'] : '';
175 return esc_url_raw( home_url( $parsed['path'] . $query ), array( $home_scheme ? $home_scheme : 'https', 'http', 'https' ) );
176 }
177
178 /**
179 * REST route: `POST /desktop-mode/v1/default-window`.
180 *
181 * Body: `{ url: string | null }`. Null disables the default.
182 */
183 function openstation_register_default_window_routes() {
184 register_rest_route(
185 'desktop-mode/v1',
186 '/default-window',
187 array(
188 'methods' => 'POST',
189 'callback' => 'openstation_rest_set_default_window',
190 // Logged in + OpenStation enabled. `read` alone is too
191 // loose — see openstation_rest_require_enabled().
192 'permission_callback' => 'openstation_rest_require_enabled',
193 // No schema type on `url` — the param is fundamentally
194 // mixed (string | null) and WP REST's multi-type schema
195 // validation has historically been flaky for this case
196 // across core versions. Validate in the callback instead,
197 // where both branches are explicit.
198 'args' => array(
199 'url' => array(
200 'description' => __( 'Admin URL to open on portal entry, or null to disable.', 'desktop-mode' ),
201 ),
202 ),
203 )
204 );
205 }
206 add_action( 'rest_api_init', 'openstation_register_default_window_routes' );
207
208 /**
209 * REST handler — writes the default-window meta and returns the
210 * normalized state.
211 *
212 * Accepts:
213 * - `{"url": "<same-origin wp-admin URL>"}` → sets this as default.
214 * - `{"url": null}` → explicitly disables the default.
215 * - `{}` (missing key) → treated same as null, for clients that
216 * encode "clear this value" as an absent key rather than an
217 * explicit null.
218 *
219 * @param WP_REST_Request $request REST request.
220 * @return WP_REST_Response|WP_Error
221 */
222 function openstation_rest_set_default_window( $request ) {
223 $user_id = get_current_user_id();
224 $params = $request->get_json_params();
225
226 // Distinguish "url was sent" (possibly null or '') from "url key
227 // absent entirely." For JSON payloads we look at get_json_params()
228 // directly because get_param() loses the null-vs-missing distinction
229 // when combined with a null-type schema.
230 $has_url = is_array( $params ) && array_key_exists( 'url', $params );
231 $url = $has_url ? $params['url'] : null;
232
233 // Null / missing / empty string all disable the default.
234 if ( null === $url || '' === $url ) {
235 openstation_set_default_window( $user_id, null );
236 return rest_ensure_response( openstation_get_default_window( $user_id ) );
237 }
238
239 if ( ! is_string( $url ) ) {
240 return new WP_Error(
241 'openstation_invalid_url',
242 __( 'The `url` parameter must be a string or null.', 'desktop-mode' ),
243 array( 'status' => 400 )
244 );
245 }
246
247 $ok = openstation_set_default_window( $user_id, $url );
248 if ( ! $ok ) {
249 return new WP_Error(
250 'openstation_invalid_url',
251 __( 'The URL is not a valid same-origin wp-admin URL.', 'desktop-mode' ),
252 array( 'status' => 400 )
253 );
254 }
255
256 return rest_ensure_response( openstation_get_default_window( $user_id ) );
257 }
258