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

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