PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
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 0.8.6 All 33 releases
desktop-mode / apps / network / network.os.php

network.os.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.9, at apps/network/network.os.php

564 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Network — an OpenStation network as one place.
4 *
5 * On a network of separate installs, each with OpenStation, this is
6 * the window that says who belongs. On the hub (a multisite's network
7 * admin, or a single site that admitted others) it lists every site
8 * the switcher offers, local and member, admits an install by its
9 * address, and re-checks the ones already in. On a member it names the
10 * network the site belongs to, shows the list as last fetched, and
11 * leaves. A site in neither role is offered both doors. The switcher
12 * above the overview's desktop tiles is the everyday face of all this;
13 * the window is the one-time admin task behind it, and every row of it
14 * opens its site, the same hop the switcher takes. See docs/network.md.
15 *
16 * @package OpenStation
17 */
18
19 namespace OpenStation\Apps\Network;
20
21 use OpenStation\App;
22 use OpenStation\App\Os;
23 use OpenStation\App\State;
24 use function OpenStation\App\Html\esc;
25 use function OpenStation\App\Html\tag;
26
27 // Direct access, unless a standalone host is booting on bare PHP.
28 if ( ! defined( 'ABSPATH' ) ) {
29 defined( 'OPENSTATION_STANDALONE' ) || exit;
30 }
31
32 // Off unless the OpenStation Network extended option is on: the module
33 // behind this window did not load, and the registry takes no app.
34 if ( ! function_exists( 'openstation_network_enabled' ) || ! openstation_network_enabled() ) {
35 return null;
36 }
37
38 const APP_ID = 'openstation-network';
39
40 /** Three nodes joined by lines: a hub and two members. */
41 const ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="16" r="7" fill="currentColor"/><circle cx="16" cy="46" r="7" fill="currentColor"/><circle cx="48" cy="46" r="7" fill="currentColor"/><path d="M32 23 L16 39 M32 23 L48 39 M23 46 H41" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"/></svg>';
42
43 /**
44 * Which face this install shows: `hub`, `member` or `unpaired`. A
45 * multisite is always the hub side; a single site is whichever role it
46 * took, or neither.
47 *
48 * @param Os $os Host.
49 * @return string
50 */
51 function mode( Os $os ) {
52 if ( $os->env->is_network() ) {
53 return 'hub';
54 }
55 if ( \openstation_network_is_member() ) {
56 return 'member';
57 }
58 if ( \openstation_network_is_hub() ) {
59 return 'hub';
60 }
61 return 'unpaired';
62 }
63
64 /**
65 * The gate: whoever manages this install's network or, on a single
66 * site, the site.
67 *
68 * @param Os $os Host.
69 * @return bool
70 */
71 function can_use( Os $os ) {
72 return $os->env->is_network() ? $os->auth->can( 'manage_network' ) : $os->auth->can( 'manage_options' );
73 }
74
75 /**
76 * Record an outcome on the state: an error, or a notice.
77 *
78 * @param State $state State.
79 * @param mixed $result A WP_Error, or anything else for success.
80 * @param string $notice Notice on success.
81 */
82 function outcome( State $state, $result, $notice ) {
83 if ( is_wp_error( $result ) ) {
84 $state->set( 'error', $result->get_error_message() )->set( 'notice', '' );
85 return;
86 }
87 $state->set( 'error', '' )->set( 'notice', $notice );
88 }
89
90 /**
91 * A status badge for a member.
92 *
93 * @param string $status `paired`, `unreachable` or `key-changed`.
94 * @return string Markup.
95 */
96 function status_badge( $status ) {
97 switch ( $status ) {
98 case 'unreachable':
99 return tag( 'os-badge', array( 'tone' => 'warning' ), esc( __( 'Unreachable', 'desktop-mode' ) ) );
100 case 'key-changed':
101 return tag( 'os-badge', array( 'tone' => 'danger' ), esc( __( 'Key changed', 'desktop-mode' ) ) );
102 default:
103 return tag( 'os-badge', array( 'tone' => 'success' ), esc( __( 'Paired', 'desktop-mode' ) ) );
104 }
105 }
106
107 /**
108 * Which switcher entry this shell is (`network`, a blog id, `hub` or
109 * `member:<id>`), so that row offers no Open: the user is already here.
110 *
111 * @return string
112 */
113 function current_instance() {
114 $block = \openstation_multisite_payload();
115 return is_array( $block ) && isset( $block['current'] ) ? (string) $block['current'] : '';
116 }
117
118 /**
119 * One site row.
120 *
121 * @param array<string,mixed> $site `id`, `name`, `url`, `shellUrl`, `kind`, `status`, `error`.
122 * @param bool $can_remove Whether a Remove button is offered.
123 * @param string $current The switcher entry this shell is; see `current_instance()`.
124 */
125 function site_row( array $site, $can_remove, $current ) {
126 $is_member = 'member' === $site['kind'];
127 ?>
128 <li class="os-network__site" os-key="<?php echo esc( $site['id'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>">
129 <div class="os-network__site-main">
130 <strong class="os-network__site-name"><?php echo esc( $site['name'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></strong>
131 <span class="os-network__site-url"><?php echo esc( $site['url'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></span>
132 <?php if ( ! empty( $site['error'] ) ) : ?>
133 <span class="os-network__site-error"><?php echo esc( $site['error'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></span>
134 <?php endif; ?>
135 </div>
136 <div class="os-network__site-side">
137 <?php
138 if ( $is_member ) {
139 echo status_badge( $site['status'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built by tag(), which escapes every attribute; the label is esc()'d.
140 } else {
141 echo tag( 'os-badge', array( 'tone' => 'neutral' ), esc( __( 'This network', 'desktop-mode' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built by tag(); see above.
142 }
143 if ( $site['id'] !== $current ) {
144 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built by tag(); see above.
145 echo tag(
146 'os-button',
147 array(
148 'variant' => 'ghost',
149 'os-action' => 'open',
150 'os-arg-id' => $site['id'],
151 'title' => __( 'Switch to this site', 'desktop-mode' ),
152 ),
153 esc( __( 'Open', 'desktop-mode' ) )
154 );
155 }
156 if ( $is_member && $can_remove ) {
157 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built by tag(); see above.
158 echo tag(
159 'os-button',
160 array(
161 'variant' => 'ghost',
162 'os-action' => 'remove',
163 'os-arg-id' => substr( (string) $site['id'], strlen( 'member:' ) ),
164 /* translators: %s: site name. */
165 'os-confirm' => sprintf( __( 'Remove %s from the network? Its switcher will stop listing this network the next time it syncs.', 'desktop-mode' ), $site['name'] ),
166 'os-confirm-label' => __( 'Remove', 'desktop-mode' ),
167 ),
168 esc( __( 'Remove', 'desktop-mode' ) )
169 );
170 }
171 ?>
172 </div>
173 </li>
174 <?php
175 }
176
177 /**
178 * The address field and its button, shared by every face.
179 *
180 * @param State $state State.
181 * @param string $label Field label.
182 * @param string $action Action the button dispatches.
183 * @param string $button Button label.
184 * @param string $placeholder Placeholder.
185 */
186 function address_form( State $state, $label, $action, $button, $placeholder = 'https://' ) {
187 ?>
188 <div class="os-network__form">
189 <os-text-field
190 class="os-network__address"
191 label="<?php echo esc( $label ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>"
192 placeholder="<?php echo esc( $placeholder ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>"
193 value="<?php echo esc( (string) $state->get( 'url' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>"
194 os-bind="url"
195 ></os-text-field>
196 <os-button variant="primary" os-action="<?php echo esc( $action ); ?>"><?php echo esc( $button ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></os-button>
197 </div>
198 <?php
199 }
200
201 /**
202 * The notices, when there are any.
203 *
204 * @param State $state State.
205 */
206 function notices( State $state ) {
207 if ( '' !== (string) $state->get( 'error' ) ) {
208 echo '<os-notice tone="danger" os-action="dismiss">' . esc( (string) $state->get( 'error' ) ) . '</os-notice>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes.
209 }
210 if ( '' !== (string) $state->get( 'notice' ) ) {
211 echo '<os-notice tone="success" os-action="dismiss">' . esc( (string) $state->get( 'notice' ) ) . '</os-notice>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes.
212 }
213 }
214
215 /**
216 * The hub's face: every site, and the door.
217 *
218 * @param State $state State.
219 * @param Os $os Host.
220 */
221 function hub_view( State $state, Os $os ) {
222 $identity = \openstation_network_identity();
223 $sites = array_merge( \openstation_network_local_entries(), \openstation_network_member_entries() );
224 $members = \openstation_network_members();
225 $current = current_instance();
226 ?>
227 <section class="os-network__section">
228 <header class="os-network__header">
229 <h2 class="os-network__title"><?php esc_html_e( 'Sites in this network', 'desktop-mode' ); ?></h2>
230 <p class="os-network__lede">
231 <?php
232 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes.
233 echo esc(
234 $os->env->is_network()
235 ? __( 'The sites of this WordPress network, and the OpenStation installs that joined it from elsewhere. Every one of them shows the same site switcher.', 'desktop-mode' )
236 : __( 'This site is the hub of an OpenStation network: every install listed here shows the same site switcher.', 'desktop-mode' )
237 );
238 ?>
239 </p>
240 <?php if ( array() !== $members ) : ?>
241 <os-button variant="ghost" os-action="check"><?php esc_html_e( 'Check sites', 'desktop-mode' ); ?></os-button>
242 <?php endif; ?>
243 </header>
244 <ul class="os-network__sites">
245 <?php
246 foreach ( $sites as $site ) {
247 $member = 'member' === $site['kind'] ? $members[ substr( $site['id'], strlen( 'member:' ) ) ] : null;
248 site_row(
249 array(
250 'id' => $site['id'],
251 'name' => $site['name'],
252 'url' => $site['url'],
253 'kind' => $site['kind'],
254 'status' => $site['status'],
255 'error' => $member ? $member['error'] : '',
256 ),
257 true,
258 $current
259 );
260 }
261 ?>
262 </ul>
263 </section>
264 <section class="os-network__section">
265 <h3 class="os-network__subtitle"><?php esc_html_e( 'Add an external site', 'desktop-mode' ); ?></h3>
266 <p class="os-network__lede">
267 <?php esc_html_e( 'An install anywhere, with OpenStation active and reachable over HTTPS. Its key is pinned when it is added; on that site, open Network and enter this address to finish pairing:', 'desktop-mode' ); ?>
268 <code class="os-network__code"><?php echo esc( $identity['url'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></code>
269 </p>
270 <?php address_form( $state, __( 'Site address', 'desktop-mode' ), 'add', __( 'Add external site', 'desktop-mode' ) ); ?>
271 </section>
272 <?php
273 }
274
275 /**
276 * A member's face: the network it belongs to, and the list.
277 *
278 * @param State $state State.
279 * @param Os $os Host.
280 */
281 function member_view( State $state, Os $os ) {
282 $hub = \openstation_network_hub();
283 $current = current_instance();
284 ?>
285 <section class="os-network__section">
286 <header class="os-network__header">
287 <h2 class="os-network__title">
288 <?php
289 /* translators: %s: network name. */
290 echo esc( sprintf( __( 'This site belongs to %s', 'desktop-mode' ), $hub['name'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes.
291 ?>
292 </h2>
293 <p class="os-network__lede">
294 <span class="os-network__site-url"><?php echo esc( $hub['url'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></span>
295 <?php if ( $hub['fetched'] > 0 ) : ?>
296 <span class="os-network__meta">
297 <?php
298 /* translators: %s: date and time. */
299 echo esc( sprintf( __( 'Site list synced %s.', 'desktop-mode' ), $os->env->format_datetime( $hub['fetched'] ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes.
300 ?>
301 </span>
302 <?php endif; ?>
303 </p>
304 <?php if ( '' !== $hub['error'] ) : ?>
305 <os-notice tone="warning">
306 <?php
307 echo esc( $hub['error'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes.
308 echo ' ';
309 esc_html_e( 'Until the network has added this site, the switcher stays as it was.', 'desktop-mode' );
310 ?>
311 </os-notice>
312 <?php endif; ?>
313 <div class="os-network__actions">
314 <os-button variant="ghost" os-action="sync"><?php esc_html_e( 'Sync now', 'desktop-mode' ); ?></os-button>
315 <os-button
316 variant="ghost"
317 os-action="leave"
318 os-confirm="<?php esc_attr_e( 'Leave the network? The site switcher stops listing its sites here.', 'desktop-mode' ); ?>"
319 os-confirm-label="<?php esc_attr_e( 'Leave', 'desktop-mode' ); ?>"
320 ><?php esc_html_e( 'Leave network', 'desktop-mode' ); ?></os-button>
321 </div>
322 </header>
323 <?php if ( null !== $hub['list'] ) : ?>
324 <ul class="os-network__sites">
325 <?php
326 foreach ( $hub['list']['sites'] as $site ) {
327 site_row(
328 array(
329 'id' => $site['id'],
330 'name' => $site['name'],
331 'url' => $site['url'],
332 'kind' => $site['kind'],
333 'status' => 'paired',
334 'error' => '',
335 ),
336 false,
337 $current
338 );
339 }
340 ?>
341 </ul>
342 <?php endif; ?>
343 </section>
344 <?php
345 }
346
347 /**
348 * Neither role yet: join a network, or start one here.
349 *
350 * @param State $state State.
351 * @param Os $os Host.
352 */
353 function unpaired_view( State $state, Os $os ) {
354 $identity = \openstation_network_identity();
355 ?>
356 <section class="os-network__section">
357 <h2 class="os-network__title"><?php esc_html_e( 'Join a network', 'desktop-mode' ); ?></h2>
358 <p class="os-network__lede">
359 <?php esc_html_e( 'Enter the address of the OpenStation network this site belongs to. Its administrator adds this site there by this address:', 'desktop-mode' ); ?>
360 <code class="os-network__code"><?php echo esc( $identity['url'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></code>
361 </p>
362 <?php address_form( $state, __( 'Network address', 'desktop-mode' ), 'join', __( 'Join', 'desktop-mode' ) ); ?>
363 </section>
364 <section class="os-network__section">
365 <h3 class="os-network__subtitle"><?php esc_html_e( 'Or start one here', 'desktop-mode' ); ?></h3>
366 <p class="os-network__lede"><?php esc_html_e( 'Add the first external site and this site becomes the hub: every site added here shows the same site switcher.', 'desktop-mode' ); ?></p>
367 <?php address_form( $state, __( 'Site address', 'desktop-mode' ), 'add', __( 'Add external site', 'desktop-mode' ) ); ?>
368 </section>
369 <?php
370 }
371
372 /**
373 * The accounts on other installs this user linked to theirs: a switch
374 * from any of them logs this user in here. Nothing when there are none.
375 *
376 * @param Os $os Host.
377 */
378 function links_section( Os $os ) {
379 $links = \openstation_network_linked_accounts( (int) $os->auth->user_id() );
380 if ( array() === $links ) {
381 return;
382 }
383 ?>
384 <section class="os-network__section">
385 <h3 class="os-network__subtitle"><?php esc_html_e( 'Linked accounts', 'desktop-mode' ); ?></h3>
386 <p class="os-network__lede"><?php esc_html_e( 'A switch from any of these accounts logs you in here as you. Unlink one and that switch lands on the login screen again.', 'desktop-mode' ); ?></p>
387 <ul class="os-network__sites">
388 <?php foreach ( $links as $key => $link ) : ?>
389 <li class="os-network__site" os-key="<?php echo esc( $key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>">
390 <div class="os-network__site-main">
391 <strong class="os-network__site-name"><?php echo esc( '' !== $link['name'] ? $link['name'] : $key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></strong>
392 <span class="os-network__site-url"><?php echo esc( trim( $link['email'] . ' · ' . $link['site'], ' ·' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></span>
393 </div>
394 <div class="os-network__site-side">
395 <?php
396 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built by tag(); see above.
397 echo tag(
398 'os-button',
399 array(
400 'variant' => 'ghost',
401 'os-action' => 'unlink',
402 'os-arg-key' => $key,
403 ),
404 esc( __( 'Unlink', 'desktop-mode' ) )
405 );
406 ?>
407 </div>
408 </li>
409 <?php endforeach; ?>
410 </ul>
411 </section>
412 <?php
413 }
414
415 /**
416 * The body.
417 *
418 * @param State $state State.
419 * @param Os $os Host.
420 */
421 function render( State $state, Os $os ) {
422 $mode = mode( $os );
423 echo '<div class="os-network os-network--' . esc( $mode ) . '">'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes.
424 notices( $state );
425 if ( 'hub' === $mode ) {
426 hub_view( $state, $os );
427 } elseif ( 'member' === $mode ) {
428 member_view( $state, $os );
429 } else {
430 unpaired_view( $state, $os );
431 }
432 links_section( $os );
433 echo '</div>';
434 }
435
436 return App::define( APP_ID )
437 ->title( __( 'Network', 'desktop-mode' ) )
438 ->icon( ICON )
439 ->size( 760, 560 )
440 ->min_size( 520, 400 )
441 // Every shell of the network offers it: managing the network is the
442 // super admin's job wherever they stand, and each row is a way to
443 // another site.
444 ->admin( 'any' )
445 ->can( __NAMESPACE__ . '\\can_use' )
446 ->state(
447 array(
448 'url' => '',
449 'notice' => '',
450 'error' => '',
451 )
452 )
453 ->action(
454 'dismiss',
455 static function ( State $state ) {
456 $state->set( 'error', '' )->set( 'notice', '' );
457 }
458 )
459 ->action(
460 'add',
461 static function ( State $state, Os $os ) {
462 $member = \openstation_network_add_member( (string) $state->get( 'url' ) );
463 outcome(
464 $state,
465 $member,
466 is_wp_error( $member )
467 ? ''
468 /* translators: %s: site name. */
469 : sprintf( __( '%s is in the network. It is in the site switcher now, and on that site once it joins from its Network window.', 'desktop-mode' ), $member['name'] )
470 );
471 if ( ! is_wp_error( $member ) ) {
472 $state->set( 'url', '' );
473 // The switcher's rows come from the menu payload; a
474 // refresh is how they follow the registry without a
475 // reload. Same after every action below that changes them.
476 $os->refresh_menu();
477 }
478 }
479 )
480 ->action(
481 'remove',
482 static function ( State $state, Os $os, array $args ) {
483 $id = isset( $args['id'] ) ? sanitize_key( (string) $args['id'] ) : '';
484 $removed = \openstation_network_remove_member( $id );
485 outcome(
486 $state,
487 $removed ? true : new \WP_Error( 'openstation_network_unknown', __( 'That site is not in the network.', 'desktop-mode' ) ),
488 __( 'Removed from the network.', 'desktop-mode' )
489 );
490 if ( $removed ) {
491 $os->refresh_menu();
492 }
493 }
494 )
495 ->action(
496 'open',
497 static function ( State $state, Os $os, array $args ) {
498 $id = isset( $args['id'] ) && is_scalar( $args['id'] ) ? sanitize_text_field( (string) $args['id'] ) : '';
499 if ( '' !== $id ) {
500 // The shell takes it from here: the same hop a pick in
501 // the switcher takes, slide and login token included.
502 $os->effects->add( 'hop', array( 'site' => $id ) );
503 }
504 }
505 )
506 ->action(
507 'unlink',
508 static function ( State $state, Os $os, array $args ) {
509 $key = isset( $args['key'] ) && is_scalar( $args['key'] ) ? sanitize_text_field( (string) $args['key'] ) : '';
510 outcome(
511 $state,
512 \openstation_network_unlink( (int) $os->auth->user_id(), $key ) ? true : new \WP_Error( 'openstation_network_unknown', __( 'That account is not linked.', 'desktop-mode' ) ),
513 __( 'Unlinked. A switch from that account lands on the login screen now.', 'desktop-mode' )
514 );
515 }
516 )
517 ->action(
518 'check',
519 static function ( State $state ) {
520 \openstation_network_check_members();
521 outcome( $state, true, __( 'Every site was checked.', 'desktop-mode' ) );
522 }
523 )
524 ->action(
525 'join',
526 static function ( State $state, Os $os ) {
527 $hub = \openstation_network_join( (string) $state->get( 'url' ) );
528 outcome(
529 $state,
530 $hub,
531 is_wp_error( $hub )
532 ? ''
533 : ( '' === $hub['error']
534 /* translators: %s: network name. */
535 ? sprintf( __( 'This site belongs to %s. The site switcher shows the network now.', 'desktop-mode' ), $hub['name'] )
536 /* translators: %s: network name. */
537 : sprintf( __( 'Pinned %s. It has not added this site yet; sync once it has.', 'desktop-mode' ), $hub['name'] ) )
538 );
539 if ( ! is_wp_error( $hub ) ) {
540 $state->set( 'url', '' );
541 $os->refresh_menu();
542 }
543 }
544 )
545 ->action(
546 'leave',
547 static function ( State $state, Os $os ) {
548 \openstation_network_leave();
549 outcome( $state, true, __( 'Left the network.', 'desktop-mode' ) );
550 $os->refresh_menu();
551 }
552 )
553 ->action(
554 'sync',
555 static function ( State $state, Os $os ) {
556 $list = \openstation_network_refresh_list();
557 outcome( $state, $list, __( 'Site list synced.', 'desktop-mode' ) );
558 if ( ! is_wp_error( $list ) ) {
559 $os->refresh_menu();
560 }
561 }
562 )
563 ->view( __NAMESPACE__ . '\\render' );
564