PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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 / multisite.php

multisite.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.10, at includes/multisite.php

403 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Multisite integration: the shell's multisite block (which
4 * instance this shell is, the sites the user may switch to, the network
5 * admin menu mirroring the admin bar's Network Admin node — every URL a
6 * link OUT, never a window source, see docs/multisite.md), and the
7 * per-site table cleanup when a subsite is deleted.
8 *
9 * @package OpenStation
10 */
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * How many of the network's sites a super admin's switcher lists beyond
16 * their own: the network Sites screen's first page. A row, not a
17 * directory — a larger network picks its set through the filter below.
18 */
19 const OPENSTATION_MULTISITE_SWITCHER_SITES = 20;
20
21 /**
22 * The multisite block of the shell config.
23 *
24 * On a network every site is its own OpenStation, and so is the network
25 * admin: the block says which instance this shell is, lists every site
26 * the user may switch to (the overview's site switcher), and carries
27 * the Network Admin tile's rows. On a single-site install the block
28 * comes from the OpenStation network the site belongs to, or is the
29 * hub of (`includes/network/`), and is null outside one. `networkAdmin`
30 * is null for users who cannot reach the network admin, which is what
31 * keeps the tile from registering.
32 *
33 * @return array|null
34 */
35 function openstation_multisite_payload() {
36 if ( ! is_user_logged_in() ) {
37 return null;
38 }
39 if ( ! is_multisite() ) {
40 // A single site has a switcher only as a hub or member of an
41 // OpenStation network, and only while that module is on.
42 if ( ! openstation_network_on() ) {
43 return null;
44 }
45 $member = openstation_network_member_payload();
46 return openstation_multisite_with_hop( null !== $member ? $member : openstation_network_hub_payload() );
47 }
48
49 $network_admin = null;
50 if ( current_user_can( 'manage_network' ) ) {
51 $network_admin = array(
52 'url' => esc_url_raw( network_admin_url() ),
53 'shellUrl' => esc_url_raw( network_admin_url( 'admin.php?page=' . OPENSTATION_SHELL_PAGE_SLUG ) ),
54 'rows' => openstation_multisite_network_admin_rows(),
55 'foreign' => false,
56 );
57 }
58
59 return openstation_multisite_with_hop(
60 array(
61 'isNetworkAdmin' => is_network_admin(),
62 'networkAdmin' => $network_admin,
63 'current' => is_network_admin() ? 'network' : (string) get_current_blog_id(),
64 'sites' => openstation_multisite_sites(),
65 )
66 );
67 }
68
69 /**
70 * The mint route for hop tokens, on any block that has somewhere to
71 * hop to. See `includes/network/hop.php`.
72 *
73 * @param array|null $block The multisite block.
74 * @return array|null
75 */
76 function openstation_multisite_with_hop( $block ) {
77 if ( is_array( $block ) && openstation_network_on() ) {
78 $block['hopUrl'] = esc_url_raw( rest_url( 'desktop-mode/v1/network/hop' ) );
79 }
80 return $block;
81 }
82
83 /**
84 * Whether the OpenStation Network module is on, from anywhere that
85 * runs whether or not it loaded. The bootstrap always loads; the
86 * module does not.
87 *
88 * @return bool
89 */
90 function openstation_network_on() {
91 return function_exists( 'openstation_network_enabled' ) && openstation_network_enabled();
92 }
93
94 /**
95 * The Network Admin tile's rows: the network dashboard and the screens
96 * the admin bar's Network Admin node offers, with its capability gates
97 * copied one for one so the tile can never offer a screen the admin
98 * bar would have hidden.
99 *
100 * @param bool $all Every row regardless of the current user — for the
101 * list served to a network's members, whose users the
102 * hub gates on arrival instead.
103 * @return array<int,array{title:string,url:string}>
104 */
105 function openstation_multisite_network_admin_rows( $all = false ) {
106 $rows = array(
107 array(
108 'title' => __( 'Dashboard', 'desktop-mode' ),
109 'url' => esc_url_raw( network_admin_url() ),
110 ),
111 );
112 $gated = array(
113 'manage_sites' => array( 'sites.php', __( 'Sites', 'desktop-mode' ) ),
114 'manage_network_users' => array( 'users.php', __( 'Users', 'desktop-mode' ) ),
115 'manage_network_themes' => array( 'themes.php', __( 'Themes', 'desktop-mode' ) ),
116 'manage_network_plugins' => array( 'plugins.php', __( 'Plugins', 'desktop-mode' ) ),
117 'manage_network_options' => array( 'settings.php', __( 'Settings', 'desktop-mode' ) ),
118 );
119 foreach ( $gated as $capability => $row ) {
120 if ( $all || current_user_can( $capability ) ) {
121 $rows[] = array(
122 'title' => $row[1],
123 'url' => esc_url_raw( network_admin_url( $row[0] ) ),
124 );
125 }
126 }
127 return $rows;
128 }
129
130 /**
131 * The network's sites by path, up to {@see OPENSTATION_MULTISITE_SWITCHER_SITES},
132 * minus the archived, spam and deleted.
133 *
134 * @return array<int,string> Site names keyed by blog id.
135 */
136 function openstation_multisite_network_sites() {
137 $names = array();
138 $sites = get_sites(
139 array(
140 'number' => OPENSTATION_MULTISITE_SWITCHER_SITES,
141 'archived' => 0,
142 'spam' => 0,
143 'deleted' => 0,
144 'orderby' => 'path',
145 'order' => 'ASC',
146 )
147 );
148 foreach ( $sites as $site ) {
149 $names[ (int) $site->blog_id ] = (string) $site->blogname;
150 }
151 return $names;
152 }
153
154 /**
155 * Whether OpenStation runs on a site of this network.
156 *
157 * @param int $blog_id The site.
158 * @return bool
159 */
160 function openstation_multisite_active_on( $blog_id ) {
161 $plugin = plugin_basename( OPENSTATION_FILE );
162 if ( isset( get_site_option( 'active_sitewide_plugins', array() )[ $plugin ] ) ) {
163 return true;
164 }
165 // Running here without being in this site's list means the plugin is
166 // loaded some other way (an mu-plugin loader), which reaches every site.
167 if ( ! in_array( $plugin, (array) get_option( 'active_plugins', array() ), true ) ) {
168 return true;
169 }
170 return in_array( $plugin, (array) get_blog_option( $blog_id, 'active_plugins', array() ), true );
171 }
172
173 /**
174 * My Sites inside a window: its row links follow the shell instead of
175 * opening as tabs of the Dashboard window. Visit opens a browser tab;
176 * this site's Dashboard goes back to Home; another site's hops to its
177 * shell when OpenStation is active there, or opens its wp-admin in a
178 * browser tab. The bridge leaves `_blank` and `_top` links to the
179 * browser, and a top-level navigation is the same hop the shell takes.
180 *
181 * @return void
182 */
183 function openstation_multisite_my_sites_load() {
184 if ( ! is_multisite() || ! openstation_is_chromeless_request() ) {
185 return;
186 }
187 // Decided up front: Core runs the filter while switched to each
188 // site, where `openstation_multisite_active_on()` would read that
189 // site's plugin list as this one's.
190 $current = get_current_blog_id();
191 $active = array();
192 foreach ( get_blogs_of_user( get_current_user_id() ) as $blog ) {
193 $active[ (int) $blog->userblog_id ] = openstation_multisite_active_on( (int) $blog->userblog_id );
194 }
195 add_filter(
196 'myblogs_blog_actions',
197 static function ( $actions, $user_blog ) use ( $current, $active ) {
198 $blog_id = (int) $user_blog->userblog_id;
199 return openstation_multisite_my_sites_actions( $actions, $blog_id, $current, ! empty( $active[ $blog_id ] ) );
200 },
201 10,
202 2
203 );
204 }
205 add_action( 'load-my-sites.php', 'openstation_multisite_my_sites_load' );
206
207 /**
208 * One My Sites row's links, rewritten for the shell. Runs switched to
209 * that site, as Core's `myblogs_blog_actions` does, and leaves markup
210 * it does not recognise alone.
211 *
212 * @param string $actions The row's links.
213 * @param int $blog_id The row's site.
214 * @param int $current_blog_id The site whose shell the window is in.
215 * @param bool $active Whether OpenStation runs on the row's site.
216 * @return string
217 */
218 function openstation_multisite_my_sites_actions( $actions, $blog_id, $current_blog_id, $active ) {
219 $home = esc_url( home_url() );
220 $admin = esc_url( admin_url() );
221
222 if ( $blog_id === $current_blog_id ) {
223 $dashboard = "<a href='" . esc_url( admin_url( 'index.php' ) ) . "'>";
224 } elseif ( $active ) {
225 $dashboard = "<a href='" . $admin . "' target='_top'>";
226 } else {
227 $dashboard = "<a href='" . $admin . "' target='_blank' rel='noopener'>";
228 }
229
230 return str_replace(
231 array( "<a href='" . $home . "'>", "<a href='" . $admin . "'>" ),
232 array( "<a href='" . $home . "' target='_blank' rel='noopener'>", $dashboard ),
233 (string) $actions
234 );
235 }
236
237 /**
238 * The network Sites list inside a window: the same rules as My Sites,
239 * minus a current site, since the network admin is its own instance.
240 * Visit opens a browser tab, and Dashboard hops to the site's shell
241 * when OpenStation is active there, or opens its wp-admin in a tab.
242 *
243 * @return void
244 */
245 function openstation_multisite_sites_list_load() {
246 if ( ! is_network_admin() || ! openstation_is_chromeless_request() ) {
247 return;
248 }
249 add_filter(
250 'manage_sites_action_links',
251 static function ( $actions, $blog_id ) {
252 return openstation_multisite_sites_row_actions( (array) $actions, openstation_multisite_active_on( (int) $blog_id ) );
253 },
254 10,
255 2
256 );
257 }
258 add_action( 'load-sites.php', 'openstation_multisite_sites_list_load' );
259
260 /**
261 * One network Sites row's actions, with a browsing context on the two
262 * links that leave the network admin. A link that already names one
263 * is left alone.
264 *
265 * @param array<string,string> $actions The row's actions, keyed as Core keys them.
266 * @param bool $active Whether OpenStation runs on the row's site.
267 * @return array<string,string>
268 */
269 function openstation_multisite_sites_row_actions( array $actions, $active ) {
270 $targets = array(
271 'visit' => '_blank',
272 'backend' => $active ? '_top' : '_blank',
273 );
274 foreach ( $targets as $key => $target ) {
275 if ( isset( $actions[ $key ] ) && is_string( $actions[ $key ] ) && false === strpos( $actions[ $key ], 'target=' ) ) {
276 $actions[ $key ] = (string) preg_replace( '/^<a /', '<a target="' . $target . '" ', $actions[ $key ], 1 );
277 }
278 }
279 return $actions;
280 }
281
282 /**
283 * The sites the user may switch to, each with its own shell screen.
284 *
285 * The user's own sites first — `get_blogs_of_user()`, the list behind
286 * the admin bar's My Sites, minus the archived, spam and deleted, in
287 * the order Core keeps them — then, for a super admin, who can reach
288 * every site whether or not they are a member, the network's sites by
289 * path up to {@see OPENSTATION_MULTISITE_SWITCHER_SITES}; then the
290 * members of the OpenStation network this install is the hub of.
291 *
292 * @return array[] Each `id` (the blog id, or `member:<id>`), `name`, `shellUrl`.
293 */
294 function openstation_multisite_sites() {
295 $names = array();
296 foreach ( get_blogs_of_user( get_current_user_id() ) as $blog ) {
297 $names[ (int) $blog->userblog_id ] = (string) $blog->blogname;
298 }
299 if ( current_user_can( 'manage_network' ) ) {
300 foreach ( openstation_multisite_network_sites() as $blog_id => $name ) {
301 if ( ! isset( $names[ $blog_id ] ) ) {
302 $names[ $blog_id ] = $name;
303 }
304 }
305 }
306
307 $sites = array();
308 foreach ( $names as $blog_id => $name ) {
309 $sites[] = array(
310 'id' => (string) $blog_id,
311 'name' => $name,
312 'shellUrl' => esc_url_raw( get_admin_url( $blog_id, 'admin.php?page=' . OPENSTATION_SHELL_PAGE_SLUG ) ),
313 'adminUrl' => esc_url_raw( get_admin_url( $blog_id ) ),
314 'active' => openstation_multisite_active_on( $blog_id ),
315 'kind' => 'local',
316 'foreign' => false,
317 );
318 }
319 foreach ( openstation_network_on() ? openstation_network_member_entries() : array() as $member ) {
320 $sites[] = array(
321 'id' => $member['id'],
322 'name' => $member['name'],
323 'shellUrl' => $member['shellUrl'],
324 'kind' => 'member',
325 'foreign' => true,
326 );
327 }
328
329 /**
330 * Filters the sites the overview's site switcher offers.
331 *
332 * Trim it on a large network, reorder it, rename an entry, or build
333 * a different set. A site dropped here is not offered, though the
334 * admin bar still reaches it.
335 *
336 * @param array[] $sites Each `id` (blog id as a string, or `member:<id>`), `name`, `shellUrl`,
337 * `adminUrl` and `active` (whether OpenStation runs there; the switcher
338 * opens a site without it at `adminUrl` in a browser tab) on a site of
339 * this network, `kind` (`local` for a site of this network, `member` for an install
340 * that joined from elsewhere, which the switcher marks as external),
341 * `foreign` (whether the entry is another install, which a switch to
342 * it needs a login token for).
343 */
344 return apply_filters( 'openstation_multisite_sites', $sites );
345 }
346
347 /**
348 * The plugin's per-site tables, unprefixed.
349 *
350 * A STATIC list, not a read from the schema helpers, on purpose:
351 * deleting a site must drop every table the plugin ever created there,
352 * and the games module (owner of the last two) only loads while its
353 * feature toggle is on — its helper may not exist on the request that
354 * deletes the site, but the tables it created earlier still do. The
355 * names are frozen identifiers (see AGENTS.md), and
356 * `Tests_OpenStation_Multisite` pins this list against the loaded
357 * schema helpers so a new table cannot be forgotten here.
358 *
359 * @return string[] Table names without any prefix.
360 */
361 function openstation_site_table_names() {
362 return array(
363 'desktop_mode_file_placements',
364 'desktop_mode_folders',
365 'desktop_mode_file_tombstones',
366 'desktop_mode_folder_shares',
367 'desktop_mode_share_user_decisions',
368 'desktop_mode_stored_files',
369 'desktop_mode_game_scores',
370 'desktop_mode_game_challenges',
371 'openstation_presence',
372 );
373 }
374
375 /**
376 * Adds the plugin's tables to the set Core drops when a site is
377 * deleted. Without this, every deleted subsite left its
378 * `wp_N_desktop_mode_*` tables behind forever.
379 *
380 * Core drops with `DROP TABLE IF EXISTS`, so listing a table the site
381 * never created (games disabled, or a site deleted before its lazy
382 * `init` table creation ran) is fine.
383 *
384 * @param string[] $tables Table names Core will drop.
385 * @param int $site_id The site being deleted.
386 * @return string[] The list with the plugin's tables appended.
387 */
388 function openstation_filter_wpmu_drop_tables( $tables, $site_id ) {
389 global $wpdb;
390
391 // Core switches to the deleted site before applying the filter,
392 // but the prefix is anchored on the passed id rather than trusted
393 // from the switch — a future caller that forgets to switch would
394 // otherwise drop the CURRENT site's tables.
395 $prefix = $wpdb->get_blog_prefix( $site_id );
396 foreach ( openstation_site_table_names() as $name ) {
397 $tables[] = $prefix . $name;
398 }
399
400 return $tables;
401 }
402 add_filter( 'wpmu_drop_tables', 'openstation_filter_wpmu_drop_tables', 10, 2 );
403