PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.7
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 / desktop-mode.php

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

288 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Desktop Mode
4 * Plugin URI: https://github.com/WordPress/desktop-mode
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: 0.9.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 *
15 * @package WPDesktopMode
16 */
17
18 defined( 'ABSPATH' ) || exit;
19
20 define( 'DESKTOP_MODE_VERSION', '0.9.7' );
21 define( 'DESKTOP_MODE_FILE', __FILE__ );
22 define( 'DESKTOP_MODE_DIR', plugin_dir_path( __FILE__ ) );
23 define( 'DESKTOP_MODE_URL', plugin_dir_url( __FILE__ ) );
24
25 /**
26 * Whether the current request needs the admin-rendering modules.
27 *
28 * Most of the plugin must load on every request — feature modules
29 * register REST routes, record content mutations that can originate
30 * on the frontend (comment submission, WooCommerce orders, plugin-
31 * driven post saves), and expose `desktop_mode_register_*()` APIs
32 * that third-party plugins call from their own `init` hooks in any
33 * context. But the admin *rendering* layer (shell markup, asset
34 * enqueues, the chromeless bridge, admin notices, migrations, the
35 * `wp_ajax_*` save handler) only ever fires on hooks that don't
36 * exist outside wp-admin — so a pure frontend page view can skip
37 * parsing and wiring ~6,500 lines of it.
38 *
39 * Anything ambiguous loads everything: admin (including admin-ajax,
40 * where Heartbeat lands), REST (sniffed early — `REST_REQUEST` isn't
41 * defined until `parse_request`), cron, WP-CLI, and the PHPUnit
42 * environment (the test bootstrap loads the plugin outside admin
43 * context but exercises the render layer directly).
44 *
45 * @since 0.9.7
46 *
47 * @return bool True to load the admin-rendering modules.
48 */
49 function desktop_mode_request_needs_admin_modules() {
50 $needs = is_admin()
51 || wp_doing_cron()
52 || ( defined( 'WP_CLI' ) && WP_CLI )
53 || ( defined( 'WP_TESTS_DOMAIN' ) )
54 || ( defined( 'REST_REQUEST' ) && REST_REQUEST );
55
56 if ( ! $needs ) {
57 // Early REST sniff. False positives just load a little more
58 // code (safe direction); plain-permalink REST is covered by
59 // the `rest_route` query var.
60 $rest_prefix = function_exists( 'rest_get_url_prefix' ) ? rest_get_url_prefix() : 'wp-json';
61 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : '';
62 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- request-shape sniff only, no data is read.
63 if ( ( '' !== $rest_prefix && false !== strpos( $request_uri, '/' . $rest_prefix ) ) || isset( $_GET['rest_route'] ) ) {
64 $needs = true;
65 }
66 }
67
68 /**
69 * Filter whether the admin-rendering modules load on this request.
70 *
71 * Escape hatch for unusual setups — e.g. a frontend integration
72 * that internally dispatches `desktop-mode/v1` REST routes via
73 * `rest_do_request()` outside a sniffable REST request can force
74 * the full load by returning true.
75 *
76 * @since 0.9.7
77 *
78 * @param bool $needs Whether the current request loads the
79 * admin-rendering module set.
80 */
81 return (bool) apply_filters( 'desktop_mode_load_admin_modules', $needs );
82 }
83
84 // Foundation primitives — must load before anything that consumes them.
85 require_once DESKTOP_MODE_DIR . 'includes/core/registry-factory.php';
86
87 // Routing helpers register filters at file-load time but call
88 // chromeless / classic detection helpers (still in helpers.php)
89 // at hook-fire time — both files load before any hook fires, so
90 // the order is strict-but-flexible. Routing first because it is
91 // the smaller, more isolated piece.
92 require_once DESKTOP_MODE_DIR . 'includes/core/routing.php';
93
94 require_once DESKTOP_MODE_DIR . 'includes/helpers.php';
95
96 // Dock + payload assembly. Loaded right after helpers.php so the
97 // foundational `desktop_mode_is_enabled()` etc. exist by the time
98 // any payload function is invoked at hook-fire time.
99 require_once DESKTOP_MODE_DIR . 'includes/core/payload.php';
100 require_once DESKTOP_MODE_DIR . 'includes/assets.php';
101 require_once DESKTOP_MODE_DIR . 'includes/admin-bar.php';
102 require_once DESKTOP_MODE_DIR . 'includes/session.php';
103 require_once DESKTOP_MODE_DIR . 'includes/presence.php';
104 require_once DESKTOP_MODE_DIR . 'includes/nonce-refresh.php';
105 require_once DESKTOP_MODE_DIR . 'includes/sticky-notes/heartbeat.php';
106 require_once DESKTOP_MODE_DIR . 'includes/os-settings.php';
107 require_once DESKTOP_MODE_DIR . 'includes/seen-intros.php';
108 require_once DESKTOP_MODE_DIR . 'includes/portal.php';
109 require_once DESKTOP_MODE_DIR . 'includes/default-window.php';
110 require_once DESKTOP_MODE_DIR . 'includes/themes-tabs.php';
111 require_once DESKTOP_MODE_DIR . 'includes/media-query.php';
112 require_once DESKTOP_MODE_DIR . 'includes/accents.php';
113 require_once DESKTOP_MODE_DIR . 'includes/toast-types.php';
114 require_once DESKTOP_MODE_DIR . 'includes/registries/native-windows.php';
115 require_once DESKTOP_MODE_DIR . 'includes/registries/window-tabs.php';
116 require_once DESKTOP_MODE_DIR . 'includes/registries/icons.php';
117 require_once DESKTOP_MODE_DIR . 'includes/registries/wallpapers.php';
118 require_once DESKTOP_MODE_DIR . 'includes/registries/widgets.php';
119 require_once DESKTOP_MODE_DIR . 'includes/components.php';
120 require_once DESKTOP_MODE_DIR . 'includes/commands.php';
121 require_once DESKTOP_MODE_DIR . 'includes/settings-tabs.php';
122 require_once DESKTOP_MODE_DIR . 'includes/dock-rail-renderer.php';
123 require_once DESKTOP_MODE_DIR . 'includes/title-bar-buttons.php';
124 require_once DESKTOP_MODE_DIR . 'includes/unfocus-effects.php';
125 require_once DESKTOP_MODE_DIR . 'includes/window-links.php';
126 require_once DESKTOP_MODE_DIR . 'includes/window-chrome.php';
127 require_once DESKTOP_MODE_DIR . 'includes/window-notices.php';
128 require_once DESKTOP_MODE_DIR . 'includes/wallpapers.php';
129 require_once DESKTOP_MODE_DIR . 'includes/widgets/heartbeat.php';
130 require_once DESKTOP_MODE_DIR . 'includes/widgets/widget-comments.php';
131 require_once DESKTOP_MODE_DIR . 'includes/widgets/widget-post-stats.php';
132 require_once DESKTOP_MODE_DIR . 'includes/widgets/widget-site-views.php';
133 require_once DESKTOP_MODE_DIR . 'includes/widgets/widget-jazz-quote.php';
134 require_once DESKTOP_MODE_DIR . 'includes/widgets/widget-starter.php';
135 require_once DESKTOP_MODE_DIR . 'includes/widgets/widget-notes.php';
136 require_once DESKTOP_MODE_DIR . 'includes/widgets/widget-focus-timer.php';
137 require_once DESKTOP_MODE_DIR . 'includes/extended-options.php';
138 require_once DESKTOP_MODE_DIR . 'includes/oauth-relay.php';
139 require_once DESKTOP_MODE_DIR . 'includes/ai-copilot/bootstrap.php';
140 // Content-changes must load before the recycle bin — the bin's
141 // changelog delegates into the generic recorder.
142 require_once DESKTOP_MODE_DIR . 'includes/content-changes.php';
143 require_once DESKTOP_MODE_DIR . 'includes/recycle-bin/bootstrap.php';
144 require_once DESKTOP_MODE_DIR . 'includes/desktop-files/bootstrap.php';
145 require_once DESKTOP_MODE_DIR . 'includes/notes/bootstrap.php';
146 require_once DESKTOP_MODE_DIR . 'includes/posts-window/bootstrap.php';
147 require_once DESKTOP_MODE_DIR . 'includes/pages-window/bootstrap.php';
148 require_once DESKTOP_MODE_DIR . 'includes/users-window/bootstrap.php';
149 require_once DESKTOP_MODE_DIR . 'includes/user-edit-window/bootstrap.php';
150 require_once DESKTOP_MODE_DIR . 'includes/plugins-window/bootstrap.php';
151 require_once DESKTOP_MODE_DIR . 'includes/comments-window/bootstrap.php';
152 require_once DESKTOP_MODE_DIR . 'includes/my-wordpress/bootstrap.php';
153 require_once DESKTOP_MODE_DIR . 'includes/content-graph/bootstrap.php';
154 require_once DESKTOP_MODE_DIR . 'includes/living-tree/bootstrap.php';
155 require_once DESKTOP_MODE_DIR . 'includes/games/bootstrap.php';
156 require_once DESKTOP_MODE_DIR . 'includes/pwa.php';
157 require_once DESKTOP_MODE_DIR . 'includes/compat/divi.php';
158
159 // Admin-rendering modules — every hook these register (`admin_init`,
160 // `admin_enqueue_scripts`, `in_admin_header`, `admin_head`,
161 // `admin_footer`, `admin_body_class`, `wp_ajax_*`) only fires inside
162 // wp-admin, so pure frontend page views skip them entirely. REST,
163 // cron, WP-CLI, admin-ajax, and the PHPUnit environment all load
164 // them (see desktop_mode_request_needs_admin_modules()). Relative
165 // order preserved from the historical unconditional list.
166 if ( desktop_mode_request_needs_admin_modules() ) {
167 require_once DESKTOP_MODE_DIR . 'includes/ajax.php';
168 // One-time data migrations. After os-settings.php so the meta-key
169 // constant and save/sanitize helpers the migrations call already exist.
170 require_once DESKTOP_MODE_DIR . 'includes/migrations.php';
171 require_once DESKTOP_MODE_DIR . 'includes/welcome-dialog.php';
172 require_once DESKTOP_MODE_DIR . 'includes/update-notice.php';
173 require_once DESKTOP_MODE_DIR . 'includes/core-notices.php';
174 require_once DESKTOP_MODE_DIR . 'includes/plugin-notices.php';
175 require_once DESKTOP_MODE_DIR . 'includes/render.php';
176 require_once DESKTOP_MODE_DIR . 'includes/devtools.php';
177 }
178
179 /**
180 * Cascade-deactivate plugins that declare `Requires Plugins: desktop-mode`
181 * when Desktop Mode itself is being deactivated.
182 *
183 * Without this, a third-party plugin like `wp-desktop-messages` keeps
184 * its `init` hook armed after Desktop Mode is turned off — the next
185 * admin page load fires `init`, the hook calls
186 * `desktop_mode_register_window()`, and the request fatals because the
187 * function lives in the plugin we just deactivated. The user lands on
188 * a white screen instead of the classic admin we redirected them to.
189 *
190 * WordPress's plugin dependency feature (6.5+) blocks ACTIVATION of a
191 * plugin whose declared `Requires Plugins` aren't all active, but it
192 * does NOT auto-deactivate dependents when their requirement is
193 * deactivated through any path — the user is expected to do that
194 * themselves. The classic plugins.php list shows a warning; the REST
195 * controller (`PUT /wp/v2/plugins/desktop-mode/desktop-mode`) we route
196 * through has no such gate. So we cascade ourselves.
197 *
198 * Timing: `deactivate_plugins()` writes `active_plugins` to the DB
199 * AFTER its hook loop finishes. If we call `deactivate_plugins()`
200 * from inside our `register_deactivation_hook` callback, our nested
201 * write-back is overwritten the moment the outer call runs its own
202 * `update_option`. So we register the cascade to fire on
203 * `shutdown` — by then the outer `deactivate_plugins` has finished
204 * and our write-back sticks.
205 *
206 * Matching uses `WP_Plugin_Dependencies::get_dependents()` keyed on
207 * our own directory slug (`dirname( plugin_basename( __FILE__ ) )`),
208 * which is the same string the dependent's `Requires Plugins` header
209 * resolves against. The filter below lets sites widen or narrow the
210 * cascade — e.g. add a plugin that wraps `desktop_mode_*` calls in
211 * `function_exists` guards and shouldn't be torn down with us.
212 *
213 * Silent deactivation (`$silent = true`) skips the dependent
214 * plugins' own deactivation hooks. Those callbacks routinely call
215 * `desktop_mode_*` helpers expecting Desktop Mode to be wired up;
216 * firing them mid-cascade can trigger the same fatal we're trying
217 * to prevent. Skipping them is the safer default.
218 *
219 * @since 0.8.2
220 */
221 function desktop_mode_cascade_deactivate_dependents() {
222 // Defer to `shutdown` so we run AFTER the outer
223 // `deactivate_plugins()` has flushed its own option update.
224 // Inline cascade gets clobbered by the outer's write-back.
225 add_action( 'shutdown', 'desktop_mode_do_cascade_deactivate', 0 );
226 }
227 register_deactivation_hook( DESKTOP_MODE_FILE, 'desktop_mode_cascade_deactivate_dependents' );
228
229 /**
230 * The deferred cascade body — runs on `shutdown` after the
231 * outer `deactivate_plugins()` has persisted its update.
232 *
233 * Split out from the `register_deactivation_hook` callback because
234 * that callback must register, not execute, the cascade. See
235 * {@see desktop_mode_cascade_deactivate_dependents} for the timing
236 * rationale.
237 *
238 * @since 0.8.2
239 */
240 function desktop_mode_do_cascade_deactivate() {
241 if ( ! class_exists( 'WP_Plugin_Dependencies' ) ) {
242 // Pre-6.5 — no native dependency tracking. Sites running an
243 // old Core won't have dependents declared via the header
244 // either; nothing to cascade.
245 return;
246 }
247
248 WP_Plugin_Dependencies::initialize();
249
250 $slug = dirname( plugin_basename( DESKTOP_MODE_FILE ) );
251 if ( '' === $slug || '.' === $slug ) {
252 return;
253 }
254
255 $dependents = (array) WP_Plugin_Dependencies::get_dependents( $slug );
256
257 /**
258 * Filter the list of plugin files to cascade-deactivate when
259 * Desktop Mode is deactivated. Defaults to every plugin whose
260 * `Requires Plugins` header lists our directory slug.
261 *
262 * @since 0.8.2
263 *
264 * @param string[] $dependents Plugin files (e.g. "foo/foo.php").
265 * @param string $slug Desktop Mode's directory slug.
266 */
267 $dependents = (array) apply_filters(
268 'desktop_mode_cascade_deactivate_dependents',
269 $dependents,
270 $slug
271 );
272
273 if ( empty( $dependents ) ) {
274 return;
275 }
276
277 $active = (array) get_option( 'active_plugins', array() );
278 $targets = array_values( array_intersect( $active, $dependents ) );
279 if ( empty( $targets ) ) {
280 return;
281 }
282
283 if ( ! function_exists( 'deactivate_plugins' ) ) {
284 require_once ABSPATH . 'wp-admin/includes/plugin.php';
285 }
286 deactivate_plugins( $targets, true );
287 }
288