PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.3
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.3
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 / default-window.php

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

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