| 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.1 |
| 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.1' ); |
| 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 |
require_once OPENSTATION_DIR . 'includes/session.php'; |
| 100 |
require_once OPENSTATION_DIR . 'includes/presence.php'; |
| 101 |
require_once OPENSTATION_DIR . 'includes/nonce-refresh.php'; |
| 102 |
require_once OPENSTATION_DIR . 'includes/os-settings.php'; |
| 103 |
require_once OPENSTATION_DIR . 'includes/seen-intros.php'; |
| 104 |
// One-time data migrations. After os-settings.php and seen-intros.php, |
| 105 |
// whose meta-key constants and helpers the migrations call. |
| 106 |
// |
| 107 |
// Unconditional, unlike the rest of the admin-only set below, because |
| 108 |
// its activation hook has to be registered on ANY request that can |
| 109 |
// dispatch activation. `activate_plugin()` includes the plugin file and |
| 110 |
// fires `activate_<basename>` in whatever context it was called from, |
| 111 |
// and a programmatic activation (a Playground Blueprint, a provisioning |
| 112 |
// script) need not look like an admin request at all. Registering the |
| 113 |
// runner's `admin_init` hook on a frontend request costs nothing: it |
| 114 |
// never fires there. |
| 115 |
require_once OPENSTATION_DIR . 'includes/migrations.php'; |
| 116 |
require_once OPENSTATION_DIR . 'includes/portal.php'; |
| 117 |
require_once OPENSTATION_DIR . 'includes/default-window.php'; |
| 118 |
// Solo window rendering mode (`?openstation_solo=<id>`). Unconditional |
| 119 |
// because `includes/render/` reads its flag, and because extensions |
| 120 |
// (the Electron adapter) call its helpers from their own hooks. |
| 121 |
require_once OPENSTATION_DIR . 'includes/solo-window.php'; |
| 122 |
require_once OPENSTATION_DIR . 'includes/themes-tabs.php'; |
| 123 |
require_once OPENSTATION_DIR . 'includes/media-query.php'; |
| 124 |
require_once OPENSTATION_DIR . 'includes/accents.php'; |
| 125 |
require_once OPENSTATION_DIR . 'includes/toast-types.php'; |
| 126 |
require_once OPENSTATION_DIR . 'includes/registries/native-windows.php'; |
| 127 |
require_once OPENSTATION_DIR . 'includes/registries/window-tabs.php'; |
| 128 |
require_once OPENSTATION_DIR . 'includes/registries/icons.php'; |
| 129 |
require_once OPENSTATION_DIR . 'includes/registries/wallpapers.php'; |
| 130 |
require_once OPENSTATION_DIR . 'includes/registries/widgets.php'; |
| 131 |
require_once OPENSTATION_DIR . 'includes/components.php'; |
| 132 |
require_once OPENSTATION_DIR . 'includes/commands.php'; |
| 133 |
require_once OPENSTATION_DIR . 'includes/settings-tabs.php'; |
| 134 |
require_once OPENSTATION_DIR . 'includes/dock-rail-renderer.php'; |
| 135 |
require_once OPENSTATION_DIR . 'includes/title-bar-buttons.php'; |
| 136 |
require_once OPENSTATION_DIR . 'includes/window-actions.php'; |
| 137 |
require_once OPENSTATION_DIR . 'includes/unfocus-effects.php'; |
| 138 |
require_once OPENSTATION_DIR . 'includes/window-links.php'; |
| 139 |
require_once OPENSTATION_DIR . 'includes/window-chrome.php'; |
| 140 |
require_once OPENSTATION_DIR . 'includes/window-notices.php'; |
| 141 |
require_once OPENSTATION_DIR . 'includes/wallpapers.php'; |
| 142 |
require_once OPENSTATION_DIR . 'includes/mio.php'; |
| 143 |
require_once OPENSTATION_DIR . 'includes/widgets/heartbeat.php'; |
| 144 |
require_once OPENSTATION_DIR . 'includes/widgets/widget-comments.php'; |
| 145 |
require_once OPENSTATION_DIR . 'includes/widgets/widget-post-stats.php'; |
| 146 |
require_once OPENSTATION_DIR . 'includes/widgets/widget-site-views.php'; |
| 147 |
require_once OPENSTATION_DIR . 'includes/widgets/widget-jazz-quote.php'; |
| 148 |
require_once OPENSTATION_DIR . 'includes/widgets/widget-starter.php'; |
| 149 |
require_once OPENSTATION_DIR . 'includes/widgets/widget-notes.php'; |
| 150 |
require_once OPENSTATION_DIR . 'includes/widgets/widget-drafts.php'; |
| 151 |
require_once OPENSTATION_DIR . 'includes/widgets/widget-focus-timer.php'; |
| 152 |
require_once OPENSTATION_DIR . 'includes/extended-options.php'; |
| 153 |
require_once OPENSTATION_DIR . 'includes/oauth-relay.php'; |
| 154 |
require_once OPENSTATION_DIR . 'includes/ai-copilot/bootstrap.php'; |
| 155 |
// Content-changes must load before the recycle bin — the bin's |
| 156 |
// changelog delegates into the generic recorder. |
| 157 |
require_once OPENSTATION_DIR . 'includes/content-changes.php'; |
| 158 |
require_once OPENSTATION_DIR . 'includes/recycle-bin/bootstrap.php'; |
| 159 |
require_once OPENSTATION_DIR . 'includes/desktop-files/bootstrap.php'; |
| 160 |
require_once OPENSTATION_DIR . 'includes/desktop-themes/bootstrap.php'; |
| 161 |
require_once OPENSTATION_DIR . 'includes/notes/bootstrap.php'; |
| 162 |
require_once OPENSTATION_DIR . 'includes/posts-window/bootstrap.php'; |
| 163 |
require_once OPENSTATION_DIR . 'includes/pages-window/bootstrap.php'; |
| 164 |
require_once OPENSTATION_DIR . 'includes/users-window/bootstrap.php'; |
| 165 |
require_once OPENSTATION_DIR . 'includes/user-edit-window/bootstrap.php'; |
| 166 |
require_once OPENSTATION_DIR . 'includes/plugins-window/bootstrap.php'; |
| 167 |
require_once OPENSTATION_DIR . 'includes/comments-window/bootstrap.php'; |
| 168 |
require_once OPENSTATION_DIR . 'includes/my-wordpress/bootstrap.php'; |
| 169 |
require_once OPENSTATION_DIR . 'includes/content-graph/bootstrap.php'; |
| 170 |
require_once OPENSTATION_DIR . 'includes/living-tree/bootstrap.php'; |
| 171 |
require_once OPENSTATION_DIR . 'includes/games/bootstrap.php'; |
| 172 |
require_once OPENSTATION_DIR . 'includes/agents/bootstrap.php'; |
| 173 |
require_once OPENSTATION_DIR . 'includes/pwa.php'; |
| 174 |
require_once OPENSTATION_DIR . 'includes/compat/divi.php'; |
| 175 |
|
| 176 |
// Admin-rendering modules — every hook these register (`admin_init`, |
| 177 |
// `admin_enqueue_scripts`, `in_admin_header`, `admin_head`, |
| 178 |
// `admin_footer`, `admin_body_class`, `wp_ajax_*`) only fires inside |
| 179 |
// wp-admin, so pure frontend page views skip them entirely. REST, |
| 180 |
// cron, WP-CLI, admin-ajax, and the PHPUnit environment all load |
| 181 |
// them (see openstation_request_needs_admin_modules()). Relative |
| 182 |
// order preserved from the historical unconditional list. |
| 183 |
if ( openstation_request_needs_admin_modules() ) { |
| 184 |
require_once OPENSTATION_DIR . 'includes/ajax.php'; |
| 185 |
require_once OPENSTATION_DIR . 'includes/welcome-dialog.php'; |
| 186 |
require_once OPENSTATION_DIR . 'includes/update-notice.php'; |
| 187 |
require_once OPENSTATION_DIR . 'includes/core-notices.php'; |
| 188 |
require_once OPENSTATION_DIR . 'includes/plugin-notices.php'; |
| 189 |
require_once OPENSTATION_DIR . 'includes/render.php'; |
| 190 |
require_once OPENSTATION_DIR . 'includes/devtools.php'; |
| 191 |
} |
| 192 |
|
| 193 |
/** |
| 194 |
* Cascade-deactivate plugins that declare `Requires Plugins: desktop-mode` |
| 195 |
* when OpenStation itself is being deactivated. |
| 196 |
* |
| 197 |
* Without this, a third-party plugin like `os-messages` keeps |
| 198 |
* its `init` hook armed after OpenStation is turned off — the next |
| 199 |
* admin page load fires `init`, the hook calls |
| 200 |
* `openstation_register_window()`, and the request fatals because the |
| 201 |
* function lives in the plugin we just deactivated. The user lands on |
| 202 |
* a white screen instead of the classic admin we redirected them to. |
| 203 |
* |
| 204 |
* WordPress's plugin dependency feature (6.5+) blocks ACTIVATION of a |
| 205 |
* plugin whose declared `Requires Plugins` aren't all active, but it |
| 206 |
* does NOT auto-deactivate dependents when their requirement is |
| 207 |
* deactivated through any path — the user is expected to do that |
| 208 |
* themselves. The classic plugins.php list shows a warning; the REST |
| 209 |
* controller (`PUT /wp/v2/plugins/desktop-mode/openstation`) we route |
| 210 |
* through has no such gate. So we cascade ourselves. |
| 211 |
* |
| 212 |
* Timing: `deactivate_plugins()` writes `active_plugins` to the DB |
| 213 |
* AFTER its hook loop finishes. If we call `deactivate_plugins()` |
| 214 |
* from inside our `register_deactivation_hook` callback, our nested |
| 215 |
* write-back is overwritten the moment the outer call runs its own |
| 216 |
* `update_option`. So we register the cascade to fire on |
| 217 |
* `shutdown` — by then the outer `deactivate_plugins` has finished |
| 218 |
* and our write-back sticks. |
| 219 |
* |
| 220 |
* Matching uses `WP_Plugin_Dependencies::get_dependents()` keyed on |
| 221 |
* our own directory slug (`dirname( plugin_basename( __FILE__ ) )`), |
| 222 |
* which is the same string the dependent's `Requires Plugins` header |
| 223 |
* resolves against. The filter below lets sites widen or narrow the |
| 224 |
* cascade — e.g. add a plugin that wraps `openstation_*` calls in |
| 225 |
* `function_exists` guards and shouldn't be torn down with us. |
| 226 |
* |
| 227 |
* Silent deactivation (`$silent = true`) skips the dependent |
| 228 |
* plugins' own deactivation hooks. Those callbacks routinely call |
| 229 |
* `openstation_*` helpers expecting OpenStation to be wired up; |
| 230 |
* firing them mid-cascade can trigger the same fatal we're trying |
| 231 |
* to prevent. Skipping them is the safer default. |
| 232 |
*/ |
| 233 |
function openstation_cascade_deactivate_dependents() { |
| 234 |
// Defer to `shutdown` so we run AFTER the outer |
| 235 |
// `deactivate_plugins()` has flushed its own option update. |
| 236 |
// Inline cascade gets clobbered by the outer's write-back. |
| 237 |
add_action( 'shutdown', 'openstation_do_cascade_deactivate', 0 ); |
| 238 |
} |
| 239 |
register_deactivation_hook( OPENSTATION_FILE, 'openstation_cascade_deactivate_dependents' ); |
| 240 |
|
| 241 |
/** |
| 242 |
* The deferred cascade body — runs on `shutdown` after the |
| 243 |
* outer `deactivate_plugins()` has persisted its update. |
| 244 |
* |
| 245 |
* Split out from the `register_deactivation_hook` callback because |
| 246 |
* that callback must register, not execute, the cascade. See |
| 247 |
* {@see openstation_cascade_deactivate_dependents} for the timing |
| 248 |
* rationale. |
| 249 |
*/ |
| 250 |
function openstation_do_cascade_deactivate() { |
| 251 |
if ( ! class_exists( 'WP_Plugin_Dependencies' ) ) { |
| 252 |
// Pre-6.5 — no native dependency tracking. Sites running an |
| 253 |
// old Core won't have dependents declared via the header |
| 254 |
// either; nothing to cascade. |
| 255 |
return; |
| 256 |
} |
| 257 |
|
| 258 |
WP_Plugin_Dependencies::initialize(); |
| 259 |
|
| 260 |
$slug = dirname( plugin_basename( OPENSTATION_FILE ) ); |
| 261 |
if ( '' === $slug || '.' === $slug ) { |
| 262 |
return; |
| 263 |
} |
| 264 |
|
| 265 |
$dependents = (array) WP_Plugin_Dependencies::get_dependents( $slug ); |
| 266 |
|
| 267 |
/** |
| 268 |
* Filter the list of plugin files to cascade-deactivate when |
| 269 |
* OpenStation is deactivated. Defaults to every plugin whose |
| 270 |
* `Requires Plugins` header lists our directory slug. |
| 271 |
* |
| 272 |
* @param string[] $dependents Plugin files (e.g. "foo/foo.php"). |
| 273 |
* @param string $slug OpenStation's directory slug. |
| 274 |
*/ |
| 275 |
$dependents = (array) apply_filters( |
| 276 |
'openstation_cascade_deactivate_dependents', |
| 277 |
$dependents, |
| 278 |
$slug |
| 279 |
); |
| 280 |
|
| 281 |
if ( empty( $dependents ) ) { |
| 282 |
return; |
| 283 |
} |
| 284 |
|
| 285 |
$active = (array) get_option( 'active_plugins', array() ); |
| 286 |
$targets = array_values( array_intersect( $active, $dependents ) ); |
| 287 |
if ( empty( $targets ) ) { |
| 288 |
return; |
| 289 |
} |
| 290 |
|
| 291 |
if ( ! function_exists( 'deactivate_plugins' ) ) { |
| 292 |
require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 293 |
} |
| 294 |
deactivate_plugins( $targets, true ); |
| 295 |
} |
| 296 |
|