PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.0
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.0, at includes/default-window.php

260 lines 8.0 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 * Only accept URLs that resolve to a same-origin `wp-admin/` path. A
114 * stricter net than `esc_url_raw` because the value flows back into
115 * the portal-entry redirect — we don't want an attacker's CSRF-seeded
116 * preference to hijack the user into an off-site landing page.
117 *
118 * @since 0.6.0
119 *
120 * @param string $url Raw input.
121 * @return string Fully-qualified URL, or empty string if rejected.
122 */
123 function desktop_mode_validate_default_window_url( $url ) {
124 $url = trim( (string) $url );
125 if ( '' === $url ) {
126 return '';
127 }
128
129 // Native-window marker: "native:<slug>" stores a registered native
130 // window id (OS Settings, Recycle Bin, plugin-registered native
131 // apps) instead of an admin URL. The slug must match
132 // /^[a-z0-9_-]+$/i so a malicious save cannot smuggle path
133 // traversal or whitespace through the marker. The shell handles
134 // the actual open-on-startup at boot via nativeWindows.openById.
135 if ( 0 === strpos( $url, 'native:' ) ) {
136 $slug = substr( $url, strlen( 'native:' ) );
137 if ( '' === $slug || ! preg_match( '/^[a-z0-9_\-]+$/i', $slug ) ) {
138 return '';
139 }
140 return 'native:' . $slug;
141 }
142
143 // Allow same-origin http(s) URLs only.
144 $parsed = wp_parse_url( $url );
145 if ( ! is_array( $parsed ) || empty( $parsed['path'] ) ) {
146 return '';
147 }
148
149 $home_origin = wp_parse_url( home_url( '/' ) );
150 $url_host = isset( $parsed['host'] ) ? strtolower( $parsed['host'] ) : '';
151 $url_scheme = isset( $parsed['scheme'] ) ? strtolower( $parsed['scheme'] ) : '';
152 $home_host = is_array( $home_origin ) && isset( $home_origin['host'] ) ? strtolower( $home_origin['host'] ) : '';
153 $home_scheme = is_array( $home_origin ) && isset( $home_origin['scheme'] ) ? strtolower( $home_origin['scheme'] ) : '';
154
155 if ( '' !== $url_host && $url_host !== $home_host ) {
156 return '';
157 }
158 if ( '' !== $url_scheme && ! in_array( $url_scheme, array( 'http', 'https' ), true ) ) {
159 return '';
160 }
161 // Make sure the path is inside wp-admin/.
162 $admin_path = wp_parse_url( admin_url(), PHP_URL_PATH );
163 if ( ! is_string( $admin_path ) ) {
164 return '';
165 }
166 if ( 0 !== strpos( $parsed['path'], $admin_path ) ) {
167 return '';
168 }
169
170 // Reassemble as a clean same-origin URL so downstream consumers
171 // always get a fully-qualified string.
172 $query = isset( $parsed['query'] ) ? '?' . $parsed['query'] : '';
173 return esc_url_raw( home_url( $parsed['path'] . $query ), array( $home_scheme ?: 'https', 'http', 'https' ) );
174 }
175
176 /**
177 * REST route: `POST /desktop-mode/v1/default-window`.
178 *
179 * Body: `{ url: string | null }`. Null disables the default.
180 *
181 * @since 0.6.0
182 */
183 function desktop_mode_register_default_window_routes() {
184 register_rest_route(
185 'desktop-mode/v1',
186 '/default-window',
187 array(
188 'methods' => 'POST',
189 'callback' => 'desktop_mode_rest_set_default_window',
190 // Logged in + desktop mode enabled. `read` alone is too
191 // loose — see desktop_mode_rest_require_enabled().
192 'permission_callback' => 'desktop_mode_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', 'desktop_mode_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 * @since 0.6.0
220 *
221 * @param WP_REST_Request $request REST request.
222 * @return WP_REST_Response|WP_Error
223 */
224 function desktop_mode_rest_set_default_window( $request ) {
225 $user_id = get_current_user_id();
226 $params = $request->get_json_params();
227
228 // Distinguish "url was sent" (possibly null or '') from "url key
229 // absent entirely." For JSON payloads we look at get_json_params()
230 // directly because get_param() loses the null-vs-missing distinction
231 // when combined with a null-type schema.
232 $has_url = is_array( $params ) && array_key_exists( 'url', $params );
233 $url = $has_url ? $params['url'] : null;
234
235 // Null / missing / empty string all disable the default.
236 if ( null === $url || '' === $url ) {
237 desktop_mode_set_default_window( $user_id, null );
238 return rest_ensure_response( desktop_mode_get_default_window( $user_id ) );
239 }
240
241 if ( ! is_string( $url ) ) {
242 return new WP_Error(
243 'desktop_mode_invalid_url',
244 __( 'The `url` parameter must be a string or null.', 'desktop-mode' ),
245 array( 'status' => 400 )
246 );
247 }
248
249 $ok = desktop_mode_set_default_window( $user_id, $url );
250 if ( ! $ok ) {
251 return new WP_Error(
252 'desktop_mode_invalid_url',
253 __( 'The URL is not a valid same-origin wp-admin URL.', 'desktop-mode' ),
254 array( 'status' => 400 )
255 );
256 }
257
258 return rest_ensure_response( desktop_mode_get_default_window( $user_id ) );
259 }
260