| 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.0 |
| 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.0' ); |
| 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 |
// Foundation primitives — must load before anything that consumes them. |
| 26 |
require_once DESKTOP_MODE_DIR . 'includes/core/registry-factory.php'; |
| 27 |
|
| 28 |
// Routing helpers register filters at file-load time but call |
| 29 |
// chromeless / classic detection helpers (still in helpers.php) |
| 30 |
// at hook-fire time — both files load before any hook fires, so |
| 31 |
// the order is strict-but-flexible. Routing first because it is |
| 32 |
// the smaller, more isolated piece. |
| 33 |
require_once DESKTOP_MODE_DIR . 'includes/core/routing.php'; |
| 34 |
|
| 35 |
require_once DESKTOP_MODE_DIR . 'includes/helpers.php'; |
| 36 |
|
| 37 |
// Dock + payload assembly. Loaded right after helpers.php so the |
| 38 |
// foundational `desktop_mode_is_enabled()` etc. exist by the time |
| 39 |
// any payload function is invoked at hook-fire time. |
| 40 |
require_once DESKTOP_MODE_DIR . 'includes/core/payload.php'; |
| 41 |
require_once DESKTOP_MODE_DIR . 'includes/ajax.php'; |
| 42 |
require_once DESKTOP_MODE_DIR . 'includes/assets.php'; |
| 43 |
require_once DESKTOP_MODE_DIR . 'includes/admin-bar.php'; |
| 44 |
require_once DESKTOP_MODE_DIR . 'includes/session.php'; |
| 45 |
require_once DESKTOP_MODE_DIR . 'includes/presence.php'; |
| 46 |
require_once DESKTOP_MODE_DIR . 'includes/nonce-refresh.php'; |
| 47 |
require_once DESKTOP_MODE_DIR . 'includes/sticky-notes/heartbeat.php'; |
| 48 |
require_once DESKTOP_MODE_DIR . 'includes/os-settings.php'; |
| 49 |
require_once DESKTOP_MODE_DIR . 'includes/seen-intros.php'; |
| 50 |
require_once DESKTOP_MODE_DIR . 'includes/welcome-dialog.php'; |
| 51 |
require_once DESKTOP_MODE_DIR . 'includes/portal.php'; |
| 52 |
require_once DESKTOP_MODE_DIR . 'includes/default-window.php'; |
| 53 |
require_once DESKTOP_MODE_DIR . 'includes/themes-tabs.php'; |
| 54 |
require_once DESKTOP_MODE_DIR . 'includes/media-query.php'; |
| 55 |
require_once DESKTOP_MODE_DIR . 'includes/accents.php'; |
| 56 |
require_once DESKTOP_MODE_DIR . 'includes/toast-types.php'; |
| 57 |
require_once DESKTOP_MODE_DIR . 'includes/registries/native-windows.php'; |
| 58 |
require_once DESKTOP_MODE_DIR . 'includes/registries/window-tabs.php'; |
| 59 |
require_once DESKTOP_MODE_DIR . 'includes/registries/icons.php'; |
| 60 |
require_once DESKTOP_MODE_DIR . 'includes/registries/wallpapers.php'; |
| 61 |
require_once DESKTOP_MODE_DIR . 'includes/registries/widgets.php'; |
| 62 |
require_once DESKTOP_MODE_DIR . 'includes/components.php'; |
| 63 |
require_once DESKTOP_MODE_DIR . 'includes/commands.php'; |
| 64 |
require_once DESKTOP_MODE_DIR . 'includes/settings-tabs.php'; |
| 65 |
require_once DESKTOP_MODE_DIR . 'includes/dock-rail-renderer.php'; |
| 66 |
require_once DESKTOP_MODE_DIR . 'includes/title-bar-buttons.php'; |
| 67 |
require_once DESKTOP_MODE_DIR . 'includes/window-chrome.php'; |
| 68 |
require_once DESKTOP_MODE_DIR . 'includes/window-notices.php'; |
| 69 |
require_once DESKTOP_MODE_DIR . 'includes/wallpapers.php'; |
| 70 |
require_once DESKTOP_MODE_DIR . 'includes/widgets/heartbeat.php'; |
| 71 |
require_once DESKTOP_MODE_DIR . 'includes/render.php'; |
| 72 |
require_once DESKTOP_MODE_DIR . 'includes/extended-options.php'; |
| 73 |
require_once DESKTOP_MODE_DIR . 'includes/oauth-relay.php'; |
| 74 |
require_once DESKTOP_MODE_DIR . 'includes/devtools.php'; |
| 75 |
require_once DESKTOP_MODE_DIR . 'includes/ai-copilot/bootstrap.php'; |
| 76 |
require_once DESKTOP_MODE_DIR . 'includes/recycle-bin/bootstrap.php'; |
| 77 |
require_once DESKTOP_MODE_DIR . 'includes/desktop-files/bootstrap.php'; |
| 78 |
require_once DESKTOP_MODE_DIR . 'includes/posts-window/bootstrap.php'; |
| 79 |
require_once DESKTOP_MODE_DIR . 'includes/pages-window/bootstrap.php'; |
| 80 |
require_once DESKTOP_MODE_DIR . 'includes/users-window/bootstrap.php'; |
| 81 |
require_once DESKTOP_MODE_DIR . 'includes/user-edit-window/bootstrap.php'; |
| 82 |
require_once DESKTOP_MODE_DIR . 'includes/plugins-window/bootstrap.php'; |
| 83 |
require_once DESKTOP_MODE_DIR . 'includes/comments-window/bootstrap.php'; |
| 84 |
require_once DESKTOP_MODE_DIR . 'includes/my-wordpress/bootstrap.php'; |
| 85 |
require_once DESKTOP_MODE_DIR . 'includes/content-graph/bootstrap.php'; |
| 86 |
require_once DESKTOP_MODE_DIR . 'includes/pwa.php'; |
| 87 |
require_once DESKTOP_MODE_DIR . 'includes/compat/divi.php'; |
| 88 |
|
| 89 |
/** |
| 90 |
* Cascade-deactivate plugins that declare `Requires Plugins: desktop-mode` |
| 91 |
* when Desktop Mode itself is being deactivated. |
| 92 |
* |
| 93 |
* Without this, a third-party plugin like `wp-desktop-messages` keeps |
| 94 |
* its `init` hook armed after Desktop Mode is turned off — the next |
| 95 |
* admin page load fires `init`, the hook calls |
| 96 |
* `desktop_mode_register_window()`, and the request fatals because the |
| 97 |
* function lives in the plugin we just deactivated. The user lands on |
| 98 |
* a white screen instead of the classic admin we redirected them to. |
| 99 |
* |
| 100 |
* WordPress's plugin dependency feature (6.5+) blocks ACTIVATION of a |
| 101 |
* plugin whose declared `Requires Plugins` aren't all active, but it |
| 102 |
* does NOT auto-deactivate dependents when their requirement is |
| 103 |
* deactivated through any path — the user is expected to do that |
| 104 |
* themselves. The classic plugins.php list shows a warning; the REST |
| 105 |
* controller (`PUT /wp/v2/plugins/desktop-mode/desktop-mode`) we route |
| 106 |
* through has no such gate. So we cascade ourselves. |
| 107 |
* |
| 108 |
* Timing: `deactivate_plugins()` writes `active_plugins` to the DB |
| 109 |
* AFTER its hook loop finishes. If we call `deactivate_plugins()` |
| 110 |
* from inside our `register_deactivation_hook` callback, our nested |
| 111 |
* write-back is overwritten the moment the outer call runs its own |
| 112 |
* `update_option`. So we register the cascade to fire on |
| 113 |
* `shutdown` — by then the outer `deactivate_plugins` has finished |
| 114 |
* and our write-back sticks. |
| 115 |
* |
| 116 |
* Matching uses `WP_Plugin_Dependencies::get_dependents()` keyed on |
| 117 |
* our own directory slug (`dirname( plugin_basename( __FILE__ ) )`), |
| 118 |
* which is the same string the dependent's `Requires Plugins` header |
| 119 |
* resolves against. The filter below lets sites widen or narrow the |
| 120 |
* cascade — e.g. add a plugin that wraps `desktop_mode_*` calls in |
| 121 |
* `function_exists` guards and shouldn't be torn down with us. |
| 122 |
* |
| 123 |
* Silent deactivation (`$silent = true`) skips the dependent |
| 124 |
* plugins' own deactivation hooks. Those callbacks routinely call |
| 125 |
* `desktop_mode_*` helpers expecting Desktop Mode to be wired up; |
| 126 |
* firing them mid-cascade can trigger the same fatal we're trying |
| 127 |
* to prevent. Skipping them is the safer default. |
| 128 |
* |
| 129 |
* @since 0.18.4 |
| 130 |
*/ |
| 131 |
function desktop_mode_cascade_deactivate_dependents() { |
| 132 |
// Defer to `shutdown` so we run AFTER the outer |
| 133 |
// `deactivate_plugins()` has flushed its own option update. |
| 134 |
// Inline cascade gets clobbered by the outer's write-back. |
| 135 |
add_action( 'shutdown', 'desktop_mode_do_cascade_deactivate', 0 ); |
| 136 |
} |
| 137 |
register_deactivation_hook( DESKTOP_MODE_FILE, 'desktop_mode_cascade_deactivate_dependents' ); |
| 138 |
|
| 139 |
/** |
| 140 |
* The deferred cascade body — runs on `shutdown` after the |
| 141 |
* outer `deactivate_plugins()` has persisted its update. |
| 142 |
* |
| 143 |
* Split out from the `register_deactivation_hook` callback because |
| 144 |
* that callback must register, not execute, the cascade. See |
| 145 |
* {@see desktop_mode_cascade_deactivate_dependents} for the timing |
| 146 |
* rationale. |
| 147 |
* |
| 148 |
* @since 0.18.4 |
| 149 |
*/ |
| 150 |
function desktop_mode_do_cascade_deactivate() { |
| 151 |
if ( ! class_exists( 'WP_Plugin_Dependencies' ) ) { |
| 152 |
// Pre-6.5 — no native dependency tracking. Sites running an |
| 153 |
// old Core won't have dependents declared via the header |
| 154 |
// either; nothing to cascade. |
| 155 |
return; |
| 156 |
} |
| 157 |
|
| 158 |
WP_Plugin_Dependencies::initialize(); |
| 159 |
|
| 160 |
$slug = dirname( plugin_basename( DESKTOP_MODE_FILE ) ); |
| 161 |
if ( '' === $slug || '.' === $slug ) { |
| 162 |
return; |
| 163 |
} |
| 164 |
|
| 165 |
$dependents = (array) WP_Plugin_Dependencies::get_dependents( $slug ); |
| 166 |
|
| 167 |
/** |
| 168 |
* Filter the list of plugin files to cascade-deactivate when |
| 169 |
* Desktop Mode is deactivated. Defaults to every plugin whose |
| 170 |
* `Requires Plugins` header lists our directory slug. |
| 171 |
* |
| 172 |
* @since 0.18.4 |
| 173 |
* |
| 174 |
* @param string[] $dependents Plugin files (e.g. "foo/foo.php"). |
| 175 |
* @param string $slug Desktop Mode's directory slug. |
| 176 |
*/ |
| 177 |
$dependents = (array) apply_filters( |
| 178 |
'desktop_mode_cascade_deactivate_dependents', |
| 179 |
$dependents, |
| 180 |
$slug |
| 181 |
); |
| 182 |
|
| 183 |
if ( empty( $dependents ) ) { |
| 184 |
return; |
| 185 |
} |
| 186 |
|
| 187 |
$active = (array) get_option( 'active_plugins', array() ); |
| 188 |
$targets = array_values( array_intersect( $active, $dependents ) ); |
| 189 |
if ( empty( $targets ) ) { |
| 190 |
return; |
| 191 |
} |
| 192 |
|
| 193 |
if ( ! function_exists( 'deactivate_plugins' ) ) { |
| 194 |
require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 195 |
} |
| 196 |
deactivate_plugins( $targets, true ); |
| 197 |
} |
| 198 |
|