PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / trunk
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin vtrunk
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 0.8.6 0.8.5 0.8.4 All 31 releases
desktop-mode / desktop-mode.php

desktop-mode.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin trunk, at desktop-mode.php

320 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: OpenStation
4 * Plugin URI: https://github.com/WordPress/openstation
5 * Description: Renders the WordPress admin as a desktop OS. Admin screens become draggable, resizable, minimizable windows floating on a desktop with a dock. Purely opt-in per user.
6 * Version: 1.1.7
7 * Requires at least: 6.0
8 * Requires PHP: 7.4
9 * Author: Daniel López Sánchez
10 * Author URI: https://github.com/allterraindeveloper
11 * License: GPLv2 or later
12 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
13 * Text Domain: desktop-mode
14 * Domain Path: /languages
15 *
16 * @package OpenStation
17 */
18
19 defined( 'ABSPATH' ) || exit;
20
21 define( 'OPENSTATION_VERSION', '1.1.7' );
22 define( 'OPENSTATION_FILE', __FILE__ );
23 define( 'OPENSTATION_DIR', plugin_dir_path( __FILE__ ) );
24 define( 'OPENSTATION_URL', plugin_dir_url( __FILE__ ) );
25
26 /**
27 * Whether the current request needs the admin-rendering modules.
28 *
29 * Most of the plugin must load on every request — feature modules
30 * register REST routes, record content mutations that can originate
31 * on the frontend (comment submission, WooCommerce orders, plugin-
32 * driven post saves), and expose `openstation_register_*()` APIs
33 * that third-party plugins call from their own `init` hooks in any
34 * context. But the admin *rendering* layer (shell markup, asset
35 * enqueues, the chromeless bridge, admin notices, migrations, the
36 * `wp_ajax_*` save handler) only ever fires on hooks that don't
37 * exist outside wp-admin — so a pure frontend page view can skip
38 * parsing and wiring ~6,500 lines of it.
39 *
40 * Anything ambiguous loads everything: admin (including admin-ajax,
41 * where Heartbeat lands), REST (sniffed early — `REST_REQUEST` isn't
42 * defined until `parse_request`), cron, WP-CLI, and the PHPUnit
43 * environment (the test bootstrap loads the plugin outside admin
44 * context but exercises the render layer directly).
45 *
46 * @return bool True to load the admin-rendering modules.
47 */
48 function openstation_request_needs_admin_modules() {
49 $needs = is_admin()
50 || wp_doing_cron()
51 || ( defined( 'WP_CLI' ) && WP_CLI )
52 || ( defined( 'WP_TESTS_DOMAIN' ) )
53 || ( defined( 'REST_REQUEST' ) && REST_REQUEST );
54
55 if ( ! $needs ) {
56 // Early REST sniff. False positives just load a little more
57 // code (safe direction); plain-permalink REST is covered by
58 // the `rest_route` query var.
59 $rest_prefix = function_exists( 'rest_get_url_prefix' ) ? rest_get_url_prefix() : 'wp-json';
60 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
61 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- request-shape sniff only, no data is read.
62 if ( ( '' !== $rest_prefix && false !== strpos( $request_uri, '/' . $rest_prefix ) ) || isset( $_GET['rest_route'] ) ) {
63 $needs = true;
64 }
65 }
66
67 /**
68 * Filter whether the admin-rendering modules load on this request.
69 *
70 * Escape hatch for unusual setups — e.g. a frontend integration
71 * that internally dispatches `desktop-mode/v1` REST routes via
72 * `rest_do_request()` outside a sniffable REST request can force
73 * the full load by returning true.
74 *
75 * @param bool $needs Whether the current request loads the
76 * admin-rendering module set.
77 */
78 return (bool) apply_filters( 'openstation_load_admin_modules', $needs );
79 }
80
81 // Foundation primitives — must load before anything that consumes them.
82 require_once OPENSTATION_DIR . 'includes/core/registry-factory.php';
83
84 // Routing helpers register filters at file-load time but call
85 // chromeless / classic detection helpers (still in helpers.php)
86 // at hook-fire time — both files load before any hook fires, so
87 // the order is strict-but-flexible. Routing first because it is
88 // the smaller, more isolated piece.
89 require_once OPENSTATION_DIR . 'includes/core/routing.php';
90
91 require_once OPENSTATION_DIR . 'includes/helpers.php';
92
93 // Dock + payload assembly. Loaded right after helpers.php so the
94 // foundational `openstation_is_enabled()` etc. exist by the time
95 // any payload function is invoked at hook-fire time.
96 require_once OPENSTATION_DIR . 'includes/core/payload.php';
97 require_once OPENSTATION_DIR . 'includes/assets.php';
98 require_once OPENSTATION_DIR . 'includes/admin-bar.php';
99 // Loaded before the session: `openstation_sanitize_session()` calls
100 // `openstation_sanitize_workspace_profile()` on every desktop it reads.
101 require_once OPENSTATION_DIR . 'includes/workspaces.php';
102 require_once OPENSTATION_DIR . 'includes/session.php';
103 require_once OPENSTATION_DIR . 'includes/multisite.php';
104 require_once OPENSTATION_DIR . 'includes/presence.php';
105 require_once OPENSTATION_DIR . 'includes/nonce-refresh.php';
106 require_once OPENSTATION_DIR . 'includes/os-settings.php';
107 require_once OPENSTATION_DIR . 'includes/mobile.php';
108 // The About journal is still lazy — this file only registers its authenticated
109 // AJAX action. Load the registration unconditionally so alternate admin-ajax
110 // bootstraps cannot skip it while classifying the request shape.
111 require_once OPENSTATION_DIR . 'includes/about-feed.php';
112 require_once OPENSTATION_DIR . 'includes/seen-intros.php';
113 // One-time data migrations. After os-settings.php and seen-intros.php,
114 // whose meta-key constants and helpers the migrations call.
115 //
116 // Unconditional, unlike the rest of the admin-only set below, because
117 // its activation hook has to be registered on ANY request that can
118 // dispatch activation. `activate_plugin()` includes the plugin file and
119 // fires `activate_<basename>` in whatever context it was called from,
120 // and a programmatic activation (a Playground Blueprint, a provisioning
121 // script) need not look like an admin request at all. Registering the
122 // runner's `admin_init` hook on a frontend request costs nothing: it
123 // never fires there.
124 require_once OPENSTATION_DIR . 'includes/migrations.php';
125 require_once OPENSTATION_DIR . 'includes/portal.php';
126 require_once OPENSTATION_DIR . 'includes/default-window.php';
127 // Solo window rendering mode (`?openstation_solo=<id>`). Unconditional
128 // because `includes/render/` reads its flag, and because extensions
129 // (the Electron adapter) call its helpers from their own hooks.
130 require_once OPENSTATION_DIR . 'includes/solo-window.php';
131 // The shell screen (`admin.php?page=openstation`) and the one "this
132 // request paints the shell" predicate. Unconditional because modules
133 // loaded outside the admin block (PWA, native windows, desktop themes)
134 // gate on it.
135 require_once OPENSTATION_DIR . 'includes/shell-screen.php';
136 require_once OPENSTATION_DIR . 'includes/themes-tabs.php';
137 require_once OPENSTATION_DIR . 'includes/media-query.php';
138 require_once OPENSTATION_DIR . 'includes/accents.php';
139 require_once OPENSTATION_DIR . 'includes/toast-types.php';
140 require_once OPENSTATION_DIR . 'includes/wp-icon-registry.php';
141 require_once OPENSTATION_DIR . 'includes/registries/native-windows.php';
142 require_once OPENSTATION_DIR . 'includes/registries/window-tabs.php';
143 require_once OPENSTATION_DIR . 'includes/registries/icons.php';
144 require_once OPENSTATION_DIR . 'includes/registries/wallpapers.php';
145 require_once OPENSTATION_DIR . 'includes/registries/widgets.php';
146 require_once OPENSTATION_DIR . 'includes/components.php';
147 require_once OPENSTATION_DIR . 'includes/commands.php';
148 require_once OPENSTATION_DIR . 'includes/settings-tabs.php';
149 require_once OPENSTATION_DIR . 'includes/dock-rail-renderer.php';
150 require_once OPENSTATION_DIR . 'includes/title-bar-buttons.php';
151 require_once OPENSTATION_DIR . 'includes/window-actions.php';
152 require_once OPENSTATION_DIR . 'includes/unfocus-effects.php';
153 require_once OPENSTATION_DIR . 'includes/window-links.php';
154 require_once OPENSTATION_DIR . 'includes/window-chrome.php';
155 require_once OPENSTATION_DIR . 'includes/window-notices.php';
156 require_once OPENSTATION_DIR . 'includes/wallpapers.php';
157 require_once OPENSTATION_DIR . 'includes/mio.php';
158 require_once OPENSTATION_DIR . 'includes/mio-portrait.php';
159 require_once OPENSTATION_DIR . 'includes/widgets/heartbeat.php';
160 require_once OPENSTATION_DIR . 'includes/widgets/widget-comments.php';
161 require_once OPENSTATION_DIR . 'includes/widgets/widget-post-stats.php';
162 require_once OPENSTATION_DIR . 'includes/widgets/widget-site-views.php';
163 require_once OPENSTATION_DIR . 'includes/widgets/widget-jazz-quote.php';
164 require_once OPENSTATION_DIR . 'includes/widgets/widget-starter.php';
165 require_once OPENSTATION_DIR . 'includes/widgets/widget-notes.php';
166 require_once OPENSTATION_DIR . 'includes/widgets/widget-drafts.php';
167 require_once OPENSTATION_DIR . 'includes/widgets/widget-focus-timer.php';
168 require_once OPENSTATION_DIR . 'includes/extended-options.php';
169 require_once OPENSTATION_DIR . 'includes/oauth-relay.php';
170 require_once OPENSTATION_DIR . 'includes/ai-copilot/bootstrap.php';
171 // Content-changes must load before the recycle bin — the bin's
172 // changelog delegates into the generic recorder.
173 require_once OPENSTATION_DIR . 'includes/content-changes.php';
174 require_once OPENSTATION_DIR . 'includes/recycle-bin/bootstrap.php';
175 require_once OPENSTATION_DIR . 'includes/desktop-files/bootstrap.php';
176 require_once OPENSTATION_DIR . 'includes/desktop-themes/bootstrap.php';
177 require_once OPENSTATION_DIR . 'includes/notes/bootstrap.php';
178 // Posts, Pages, Users, User Edit, Plugins and Comments are App
179 // Framework apps (`apps/posts/`, `apps/pages/`, `apps/users/`,
180 // `apps/user-edit/`, `apps/plugins/`, `apps/comments/`): each `.os.php`
181 // pulls in its own PHP parts (permissions, REST fields and routes,
182 // AJAX handlers) when the framework loads it on `init`.
183 // Station Home is an App Framework app (`apps/station-home/`); only
184 // its plugin-card registry — a public API plugins call on `init` —
185 // lives here, so it exists before any app loads.
186 require_once OPENSTATION_DIR . 'includes/station-home/cards.php';
187 require_once OPENSTATION_DIR . 'includes/my-wordpress/bootstrap.php';
188 require_once OPENSTATION_DIR . 'includes/content-graph/bootstrap.php';
189 // The App Framework: `.os.php` windows under `apps/` (Code Blue lives
190 // there) and any directory a plugin adds. Loads after the registries
191 // it registers into and after the feature modules, so an app can
192 // hook them.
193 require_once OPENSTATION_DIR . 'includes/framework/wordpress.php';
194 require_once OPENSTATION_DIR . 'includes/living-tree/bootstrap.php';
195 require_once OPENSTATION_DIR . 'includes/games/bootstrap.php';
196 require_once OPENSTATION_DIR . 'includes/agents/bootstrap.php';
197 require_once OPENSTATION_DIR . 'includes/pwa.php';
198 require_once OPENSTATION_DIR . 'includes/compat/divi.php';
199
200 // Admin-rendering modules — every hook these register (`admin_init`,
201 // `admin_enqueue_scripts`, `in_admin_header`, `admin_head`,
202 // `admin_footer`, `admin_body_class`, `wp_ajax_*`) only fires inside
203 // wp-admin, so pure frontend page views skip them entirely. REST,
204 // cron, WP-CLI, admin-ajax, and the PHPUnit environment all load
205 // them (see openstation_request_needs_admin_modules()). Relative
206 // order preserved from the historical unconditional list.
207 if ( openstation_request_needs_admin_modules() ) {
208 require_once OPENSTATION_DIR . 'includes/ajax.php';
209 require_once OPENSTATION_DIR . 'includes/welcome-dialog.php';
210 require_once OPENSTATION_DIR . 'includes/update-notice.php';
211 require_once OPENSTATION_DIR . 'includes/core-notices.php';
212 require_once OPENSTATION_DIR . 'includes/plugin-notices.php';
213 require_once OPENSTATION_DIR . 'includes/render.php';
214 require_once OPENSTATION_DIR . 'includes/devtools.php';
215 }
216
217 /**
218 * Cascade-deactivate plugins that declare `Requires Plugins: desktop-mode`
219 * when OpenStation itself is being deactivated.
220 *
221 * Without this, a third-party plugin like `os-messages` keeps
222 * its `init` hook armed after OpenStation is turned off — the next
223 * admin page load fires `init`, the hook calls
224 * `openstation_register_window()`, and the request fatals because the
225 * function lives in the plugin we just deactivated. The user lands on
226 * a white screen instead of the classic admin we redirected them to.
227 *
228 * WordPress's plugin dependency feature (6.5+) blocks ACTIVATION of a
229 * plugin whose declared `Requires Plugins` aren't all active, but it
230 * does NOT auto-deactivate dependents when their requirement is
231 * deactivated through any path — the user is expected to do that
232 * themselves. The classic plugins.php list shows a warning; the REST
233 * controller (`PUT /wp/v2/plugins/desktop-mode/openstation`) we route
234 * through has no such gate. So we cascade ourselves.
235 *
236 * Timing: `deactivate_plugins()` writes `active_plugins` to the DB
237 * AFTER its hook loop finishes. If we call `deactivate_plugins()`
238 * from inside our `register_deactivation_hook` callback, our nested
239 * write-back is overwritten the moment the outer call runs its own
240 * `update_option`. So we register the cascade to fire on
241 * `shutdown` — by then the outer `deactivate_plugins` has finished
242 * and our write-back sticks.
243 *
244 * Matching uses `WP_Plugin_Dependencies::get_dependents()` keyed on
245 * our own directory slug (`dirname( plugin_basename( __FILE__ ) )`),
246 * which is the same string the dependent's `Requires Plugins` header
247 * resolves against. The filter below lets sites widen or narrow the
248 * cascade — e.g. add a plugin that wraps `openstation_*` calls in
249 * `function_exists` guards and shouldn't be torn down with us.
250 *
251 * Silent deactivation (`$silent = true`) skips the dependent
252 * plugins' own deactivation hooks. Those callbacks routinely call
253 * `openstation_*` helpers expecting OpenStation to be wired up;
254 * firing them mid-cascade can trigger the same fatal we're trying
255 * to prevent. Skipping them is the safer default.
256 */
257 function openstation_cascade_deactivate_dependents() {
258 // Defer to `shutdown` so we run AFTER the outer
259 // `deactivate_plugins()` has flushed its own option update.
260 // Inline cascade gets clobbered by the outer's write-back.
261 add_action( 'shutdown', 'openstation_do_cascade_deactivate', 0 );
262 }
263 register_deactivation_hook( OPENSTATION_FILE, 'openstation_cascade_deactivate_dependents' );
264
265 /**
266 * The deferred cascade body — runs on `shutdown` after the
267 * outer `deactivate_plugins()` has persisted its update.
268 *
269 * Split out from the `register_deactivation_hook` callback because
270 * that callback must register, not execute, the cascade. See
271 * {@see openstation_cascade_deactivate_dependents} for the timing
272 * rationale.
273 */
274 function openstation_do_cascade_deactivate() {
275 if ( ! class_exists( 'WP_Plugin_Dependencies' ) ) {
276 // Pre-6.5 — no native dependency tracking. Sites running an
277 // old Core won't have dependents declared via the header
278 // either; nothing to cascade.
279 return;
280 }
281
282 WP_Plugin_Dependencies::initialize();
283
284 $slug = dirname( plugin_basename( OPENSTATION_FILE ) );
285 if ( '' === $slug || '.' === $slug ) {
286 return;
287 }
288
289 $dependents = (array) WP_Plugin_Dependencies::get_dependents( $slug );
290
291 /**
292 * Filter the list of plugin files to cascade-deactivate when
293 * OpenStation is deactivated. Defaults to every plugin whose
294 * `Requires Plugins` header lists our directory slug.
295 *
296 * @param string[] $dependents Plugin files (e.g. "foo/foo.php").
297 * @param string $slug OpenStation's directory slug.
298 */
299 $dependents = (array) apply_filters(
300 'openstation_cascade_deactivate_dependents',
301 $dependents,
302 $slug
303 );
304
305 if ( empty( $dependents ) ) {
306 return;
307 }
308
309 $active = (array) get_option( 'active_plugins', array() );
310 $targets = array_values( array_intersect( $active, $dependents ) );
311 if ( empty( $targets ) ) {
312 return;
313 }
314
315 if ( ! function_exists( 'deactivate_plugins' ) ) {
316 require_once ABSPATH . 'wp-admin/includes/plugin.php';
317 }
318 deactivate_plugins( $targets, true );
319 }
320