PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.4
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.4
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 / desktop-themes / rest.php

rest.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.4, at includes/desktop-themes/rest.php

258 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Desktop-theme REST routes.
4 *
5 * GET /desktop-mode/v1/desktop-themes full entries
6 * POST /desktop-mode/v1/desktop-themes multipart `file`
7 * DELETE /desktop-mode/v1/desktop-themes/<slug>
8 *
9 * The GET exists for the boot-payload diet, and it is NOT a second
10 * source of truth: it returns exactly
11 * `openstation_build_desktop_themes_payload()` — the same builder,
12 * the same `openstation_desktop_themes` filter — with the FULL
13 * entries. The boot payload ships the library slimmed (no `cssText`,
14 * no `tokens`; `cssDeferred: true` marks the gap) because the active
15 * theme's stylesheet is server-delivered at boot and an inactive
16 * theme's ~20 KB of compiled CSS is only needed at the moment the
17 * user picks it — which is when the shell calls this route.
18 *
19 * @package OpenStation
20 */
21
22 defined( 'ABSPATH' ) || exit;
23
24 /**
25 * Permission gate: the standard openstation REST gate plus the
26 * theme-management capability.
27 *
28 * @return true|WP_Error
29 */
30 function openstation_desktop_themes_rest_permission() {
31 $base = openstation_rest_require_enabled();
32 if ( is_wp_error( $base ) ) {
33 return $base;
34 }
35 if ( ! current_user_can( openstation_desktop_theme_upload_capability() ) ) {
36 return new WP_Error(
37 'openstation_desktop_theme_cannot_manage',
38 __( 'You are not allowed to manage desktop themes.', 'desktop-mode' ),
39 array( 'status' => 403 )
40 );
41 }
42 return true;
43 }
44
45 /**
46 * Register the routes.
47 */
48 function openstation_register_desktop_themes_rest_routes() {
49 register_rest_route(
50 'desktop-mode/v1',
51 '/desktop-themes',
52 array(
53 array(
54 // Read gate is the shell's own, NOT the manage
55 // capability: the same full entries used to ride the
56 // boot payload to every desktop user, so the route
57 // exposes nothing the payload didn't.
58 'methods' => WP_REST_Server::READABLE,
59 'permission_callback' => 'openstation_rest_require_enabled',
60 'callback' => 'openstation_rest_list_desktop_themes',
61 ),
62 array(
63 // POST only — PHP populates `$_FILES` for real POSTs only.
64 'methods' => WP_REST_Server::CREATABLE,
65 'permission_callback' => 'openstation_desktop_themes_rest_permission',
66 'callback' => 'openstation_rest_upload_desktop_theme',
67 ),
68 )
69 );
70
71 register_rest_route(
72 'desktop-mode/v1',
73 '/desktop-themes/(?P<slug>[a-z0-9_-]+)',
74 array(
75 'methods' => WP_REST_Server::DELETABLE,
76 'permission_callback' => 'openstation_desktop_themes_rest_permission',
77 'callback' => 'openstation_rest_delete_desktop_theme',
78 'args' => array(
79 'slug' => array(
80 'type' => 'string',
81 'required' => true,
82 ),
83 ),
84 )
85 );
86 }
87 add_action( 'rest_api_init', 'openstation_register_desktop_themes_rest_routes' );
88
89 /**
90 * GET /desktop-themes — the full theme library.
91 *
92 * The on-demand counterpart of the slimmed boot payload: same
93 * builder, same filter, full `cssText` / `tokens`. The shell calls
94 * it from `ensureFullDesktopThemes()` the first time a deferred
95 * entry's stylesheet is actually needed (the user picks a theme in
96 * Preferences → Themes).
97 *
98 * @return WP_REST_Response
99 */
100 function openstation_rest_list_desktop_themes() {
101 return rest_ensure_response(
102 array(
103 'themes' => openstation_build_desktop_themes_payload(),
104 )
105 );
106 }
107
108 /**
109 * POST /desktop-mode/v1/desktop-themes
110 *
111 * @param WP_REST_Request $request Request.
112 * @return WP_REST_Response|WP_Error The payload-shaped entry.
113 */
114 function openstation_rest_upload_desktop_theme( WP_REST_Request $request ) {
115 $files = $request->get_file_params();
116
117 // A body over `post_max_size` reaches PHP with $_POST and $_FILES
118 // both empty while CONTENT_LENGTH says bytes were sent. Answer a
119 // clear 413 rather than the baffling "missing parameter" default
120 // (same treatment as the stored-files upload route).
121 if ( empty( $files ) ) {
122 $content_length = isset( $_SERVER['CONTENT_LENGTH'] ) ? (int) $_SERVER['CONTENT_LENGTH'] : 0;
123 if ( $content_length > 0 ) {
124 return new WP_Error(
125 'openstation_desktop_theme_too_large',
126 __( 'That theme archive is larger than this server accepts.', 'desktop-mode' ),
127 array( 'status' => 413 )
128 );
129 }
130 return new WP_Error(
131 'openstation_desktop_theme_no_file',
132 __( 'No theme archive was uploaded.', 'desktop-mode' ),
133 array( 'status' => 400 )
134 );
135 }
136 if ( empty( $files['file'] ) || ! is_array( $files['file'] ) ) {
137 return new WP_Error(
138 'openstation_desktop_theme_no_file',
139 __( 'No theme archive was uploaded.', 'desktop-mode' ),
140 array( 'status' => 400 )
141 );
142 }
143
144 $file = $files['file'];
145 $name = isset( $file['name'] ) ? (string) $file['name'] : '';
146
147 // Name check: must END in `.zip`, and no dot-segment anywhere in
148 // the name may look executable (`theme.php.zip` is refused even
149 // though its final extension is fine — OWASP double-extension).
150 $segments = explode( '.', strtolower( $name ) );
151 $last = array_pop( $segments );
152 if ( 'zip' !== $last ) {
153 return new WP_Error(
154 'openstation_desktop_theme_not_zip',
155 __( 'A desktop theme must be uploaded as a .zip archive.', 'desktop-mode' ),
156 array( 'status' => 400 )
157 );
158 }
159 $denied = array( 'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar', 'pht', 'phps', 'cgi', 'pl', 'asp', 'aspx', 'jsp', 'shtml', 'html', 'htm', 'js' );
160 array_shift( $segments ); // First segment is the base name.
161 foreach ( $segments as $segment ) {
162 if ( in_array( $segment, $denied, true ) ) {
163 return new WP_Error(
164 'openstation_desktop_theme_not_zip',
165 __( 'That file name is not allowed.', 'desktop-mode' ),
166 array( 'status' => 400 )
167 );
168 }
169 }
170
171 $max = (int) wp_max_upload_size();
172 if ( $max > 0 && isset( $file['size'] ) && (int) $file['size'] > $max ) {
173 return new WP_Error(
174 'openstation_desktop_theme_too_large',
175 sprintf(
176 /* translators: %s: formatted maximum file size. */
177 __( 'That theme archive is larger than the allowed maximum of %s.', 'desktop-mode' ),
178 size_format( $max )
179 ),
180 array( 'status' => 413 )
181 );
182 }
183
184 $tmp = isset( $file['tmp_name'] ) ? (string) $file['tmp_name'] : '';
185 if ( '' === $tmp || ! file_exists( $tmp ) ) {
186 return new WP_Error(
187 'openstation_desktop_theme_no_file',
188 __( 'The uploaded archive could not be read.', 'desktop-mode' ),
189 array( 'status' => 400 )
190 );
191 }
192
193 $entry = openstation_desktop_theme_install_from_zip( $tmp );
194 if ( is_wp_error( $entry ) ) {
195 return $entry;
196 }
197
198 $shaped = openstation_shape_desktop_theme_payload_entry( $entry, 'upload' );
199 if ( ! $shaped ) {
200 return new WP_Error(
201 'openstation_desktop_theme_install_failed',
202 __( 'The theme installed but could not be described back to the shell.', 'desktop-mode' ),
203 array( 'status' => 500 )
204 );
205 }
206
207 // Wallpapers the theme brought with it.
208 //
209 // `openstation_register_desktop_theme_wallpapers()` already ran on
210 // `init` for THIS request — before the upload existed — so the new
211 // theme's wallpapers are not in the registry yet. Re-running it now
212 // picks them up (registration is idempotent: same ids, same store),
213 // and the shell applies the rebuilt list without a reload. Without
214 // this the wallpapers only appeared on the next page load, which is
215 // exactly the kind of "it works after F5" seam this payload channel
216 // exists to remove.
217 openstation_register_desktop_theme_wallpapers();
218 $shaped['serverWallpapers'] = openstation_build_desktop_wallpapers_payload();
219
220 return rest_ensure_response( $shaped );
221 }
222
223 /**
224 * DELETE /desktop-mode/v1/desktop-themes/<slug>
225 *
226 * @param WP_REST_Request $request Request.
227 * @return WP_REST_Response|WP_Error
228 */
229 function openstation_rest_delete_desktop_theme( WP_REST_Request $request ) {
230 $slug = sanitize_key( (string) $request['slug'] );
231 $deleted = openstation_desktop_theme_delete( $slug );
232 if ( is_wp_error( $deleted ) ) {
233 return $deleted;
234 }
235 // The deleted theme's wallpapers were registered on `init`, into a
236 // per-request static store we have no unregister API for. Filtering
237 // them out of the response is enough and avoids inventing one: the
238 // store dies with the request, and the next one never registers
239 // them because the theme is gone.
240 $prefix = OPENSTATION_DESKTOP_THEME_WALLPAPER_PREFIX . $slug . '/';
241 $wallpapers = array();
242 foreach ( openstation_build_desktop_wallpapers_payload() as $wallpaper ) {
243 $id = isset( $wallpaper['id'] ) ? (string) $wallpaper['id'] : '';
244 if ( '' !== $id && 0 === strpos( $id, $prefix ) ) {
245 continue;
246 }
247 $wallpapers[] = $wallpaper;
248 }
249
250 return rest_ensure_response(
251 array(
252 'deleted' => true,
253 'slug' => $slug,
254 'serverWallpapers' => $wallpapers,
255 )
256 );
257 }
258