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

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