PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / trunk
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin vtrunk
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 / includes / network / member.php

member.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin trunk, at includes/network/member.php

274 lines 9.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — A member of a network: the hub it belongs to, and the
4 * network's list as last fetched, which is what its switcher shows.
5 *
6 * Joining is one URL: the member fetches the hub's identity, pins its
7 * key, then asks for the list with a signed request. The hub answers
8 * only once the member has been added there, so the two steps can
9 * happen in either order; until the hub knows this site, the member
10 * shows it is waiting. The list is cached and refreshed in the
11 * background when older than an hour, never on the request that
12 * paints the shell.
13 *
14 * @package OpenStation
15 */
16
17 defined( 'ABSPATH' ) || exit;
18
19 /** The hub this install belongs to, with the list as last fetched. */
20 const OPENSTATION_NETWORK_HUB_OPTION = 'openstation_network_hub';
21
22 /** How old a cached list may be before a refresh is scheduled. */
23 const OPENSTATION_NETWORK_LIST_TTL = HOUR_IN_SECONDS;
24
25 /**
26 * How soon a member WITHOUT a list asks again: it joined before the
27 * hub added it, or the hub was unreachable. The two steps of pairing
28 * happen in either order, and this is what makes the second one land
29 * without anyone pressing Sync.
30 */
31 const OPENSTATION_NETWORK_RETRY = 5 * MINUTE_IN_SECONDS;
32
33 /** The cron hook that refreshes the list. */
34 const OPENSTATION_NETWORK_REFRESH_HOOK = 'openstation_network_refresh_list';
35
36 /**
37 * The hub this install belongs to, or null.
38 *
39 * @return array<string,mixed>|null `url`, `name`, `shellUrl`, `publicKey`, `joined`, `list`, `fetched` (the last list), `tried` (the last attempt, either way), `error`.
40 */
41 function openstation_network_hub() {
42 $hub = openstation_network_option_get( OPENSTATION_NETWORK_HUB_OPTION );
43 if ( ! is_array( $hub ) || empty( $hub['url'] ) || empty( $hub['publicKey'] ) ) {
44 return null;
45 }
46 return array(
47 'url' => (string) $hub['url'],
48 'name' => isset( $hub['name'] ) ? (string) $hub['name'] : (string) $hub['url'],
49 'shellUrl' => isset( $hub['shellUrl'] ) ? (string) $hub['shellUrl'] : '',
50 'publicKey' => (string) $hub['publicKey'],
51 'joined' => isset( $hub['joined'] ) ? (int) $hub['joined'] : 0,
52 'list' => isset( $hub['list'] ) && is_array( $hub['list'] ) ? $hub['list'] : null,
53 'fetched' => isset( $hub['fetched'] ) ? (int) $hub['fetched'] : 0,
54 'tried' => isset( $hub['tried'] ) ? (int) $hub['tried'] : 0,
55 'error' => isset( $hub['error'] ) ? (string) $hub['error'] : '',
56 );
57 }
58
59 /**
60 * Whether this install belongs to a network.
61 *
62 * @return bool
63 */
64 function openstation_network_is_member() {
65 return null !== openstation_network_hub();
66 }
67
68 /**
69 * Join a network: pin the hub, then ask it for the list.
70 *
71 * @param string $url The hub's URL, as typed.
72 * @return array<string,mixed>|WP_Error The hub as stored, or why not.
73 */
74 function openstation_network_join( $url ) {
75 if ( is_multisite() ) {
76 return new WP_Error( 'openstation_network_is_network', __( 'A network cannot join another network yet.', 'desktop-mode' ) );
77 }
78 $url = esc_url_raw( trim( (string) $url ) );
79 if ( '' === $url || ! wp_parse_url( $url, PHP_URL_HOST ) ) {
80 return new WP_Error( 'openstation_network_bad_url', __( 'Enter the network\'s address, starting with https://.', 'desktop-mode' ) );
81 }
82 if ( openstation_network_url_is_self( $url ) ) {
83 return new WP_Error( 'openstation_network_self', __( 'That is this site itself.', 'desktop-mode' ) );
84 }
85 $identity = openstation_network_fetch_identity( $url );
86 if ( is_wp_error( $identity ) ) {
87 return $identity;
88 }
89 openstation_network_option_set(
90 OPENSTATION_NETWORK_HUB_OPTION,
91 array(
92 'url' => $identity['url'],
93 'name' => $identity['name'],
94 'shellUrl' => $identity['shellUrl'],
95 'publicKey' => $identity['publicKey'],
96 'joined' => time(),
97 'list' => null,
98 'fetched' => 0,
99 'error' => '',
100 )
101 );
102 openstation_network_refresh_list();
103 return openstation_network_hub();
104 }
105
106 /**
107 * Leave the network: forget the hub and its list.
108 *
109 * @return bool
110 */
111 function openstation_network_leave() {
112 wp_clear_scheduled_hook( OPENSTATION_NETWORK_REFRESH_HOOK );
113 return openstation_network_option_delete( OPENSTATION_NETWORK_HUB_OPTION );
114 }
115
116 /**
117 * Fetch the list from the hub with a signed request and cache it. A
118 * failure is recorded on the hub entry, so the app can say why, and
119 * the last good list stays in place.
120 *
121 * @return array<string,mixed>|WP_Error The list, or the error.
122 */
123 function openstation_network_refresh_list() {
124 $hub = openstation_network_hub();
125 if ( null === $hub ) {
126 return new WP_Error( 'openstation_network_no_hub', __( 'This site does not belong to a network.', 'desktop-mode' ) );
127 }
128 $route = '/desktop-mode/v1/network';
129 $list = openstation_network_remote_get( $hub['url'], $route, openstation_network_signed_headers( 'GET', $route ) );
130 if ( ! is_wp_error( $list ) && ( empty( $list['sites'] ) || ! is_array( $list['sites'] ) ) ) {
131 $list = new WP_Error( 'openstation_network_bad_list', __( 'The network answered without a site list.', 'desktop-mode' ) );
132 }
133 $stored = openstation_network_option_get( OPENSTATION_NETWORK_HUB_OPTION );
134 if ( ! is_array( $stored ) ) {
135 $stored = array();
136 }
137 $stored['tried'] = time();
138 if ( is_wp_error( $list ) ) {
139 $stored['error'] = $list->get_error_message();
140 openstation_network_option_set( OPENSTATION_NETWORK_HUB_OPTION, $stored );
141 return $list;
142 }
143 $sites = array();
144 foreach ( $list['sites'] as $site ) {
145 if ( ! is_array( $site ) || empty( $site['id'] ) || empty( $site['shellUrl'] ) ) {
146 continue;
147 }
148 $sites[] = array(
149 'id' => (string) $site['id'],
150 'name' => isset( $site['name'] ) ? sanitize_text_field( (string) $site['name'] ) : (string) $site['id'],
151 'shellUrl' => esc_url_raw( (string) $site['shellUrl'] ),
152 'url' => isset( $site['url'] ) ? esc_url_raw( (string) $site['url'] ) : '',
153 'publicKey' => isset( $site['publicKey'] ) && openstation_network_is_public_key( $site['publicKey'] ) ? (string) $site['publicKey'] : '',
154 'kind' => isset( $site['kind'] ) && 'member' === $site['kind'] ? 'member' : 'local',
155 );
156 }
157 $admin = null;
158 if ( ! empty( $list['networkAdmin'] ) && is_array( $list['networkAdmin'] ) && ! empty( $list['networkAdmin']['shellUrl'] ) ) {
159 $rows = array();
160 if ( ! empty( $list['networkAdmin']['rows'] ) && is_array( $list['networkAdmin']['rows'] ) ) {
161 foreach ( $list['networkAdmin']['rows'] as $row ) {
162 if ( is_array( $row ) && ! empty( $row['title'] ) && ! empty( $row['url'] ) ) {
163 $rows[] = array(
164 'title' => sanitize_text_field( (string) $row['title'] ),
165 'url' => esc_url_raw( (string) $row['url'] ),
166 );
167 }
168 }
169 }
170 $admin = array(
171 'url' => isset( $list['networkAdmin']['url'] ) ? esc_url_raw( (string) $list['networkAdmin']['url'] ) : '',
172 'shellUrl' => esc_url_raw( (string) $list['networkAdmin']['shellUrl'] ),
173 'rows' => $rows,
174 );
175 }
176 $stored['list'] = array(
177 'name' => isset( $list['name'] ) ? sanitize_text_field( (string) $list['name'] ) : $hub['name'],
178 'networkAdmin' => $admin,
179 'sites' => $sites,
180 );
181 $stored['name'] = $stored['list']['name'];
182 $stored['fetched'] = time();
183 $stored['error'] = '';
184 openstation_network_option_set( OPENSTATION_NETWORK_HUB_OPTION, $stored );
185 return $stored['list'];
186 }
187 add_action( OPENSTATION_NETWORK_REFRESH_HOOK, 'openstation_network_refresh_list' );
188
189 /**
190 * Schedule a background refresh when the cached list is stale, or when
191 * there is none yet. Never fetches on the request that paints the shell.
192 *
193 * A member with a list refreshes it hourly. One without asks again
194 * every few minutes: it joined before the hub added it, or the hub was
195 * unreachable, and the switcher should appear once the hub answers,
196 * without anyone pressing Sync.
197 */
198 function openstation_network_schedule_refresh() {
199 $hub = openstation_network_hub();
200 if ( null === $hub ) {
201 return;
202 }
203 $due = null === $hub['list']
204 ? $hub['tried'] + OPENSTATION_NETWORK_RETRY
205 : $hub['fetched'] + OPENSTATION_NETWORK_LIST_TTL;
206 if ( time() < $due ) {
207 return;
208 }
209 if ( ! wp_next_scheduled( OPENSTATION_NETWORK_REFRESH_HOOK ) ) {
210 wp_schedule_single_event( time(), OPENSTATION_NETWORK_REFRESH_HOOK );
211 }
212 }
213
214 /**
215 * The multisite block a member's shell boots with, built from the
216 * cached list: the network's sites, this site current, the hub's
217 * Network Admin for administrators. Null when this site is not a
218 * member or has no list yet.
219 *
220 * @return array<string,mixed>|null
221 */
222 function openstation_network_member_payload() {
223 $hub = openstation_network_hub();
224 if ( null === $hub ) {
225 return null;
226 }
227 // Before the list check: a member still waiting for the hub is the
228 // one that most needs to ask again.
229 openstation_network_schedule_refresh();
230 if ( null === $hub['list'] ) {
231 return null;
232 }
233
234 $me = openstation_network_public_key();
235 $current = '';
236 $sites = array();
237 foreach ( $hub['list']['sites'] as $site ) {
238 $mine = '' !== $site['publicKey'] && hash_equals( $site['publicKey'], $me );
239 if ( $mine ) {
240 $current = $site['id'];
241 }
242 $sites[] = array(
243 'id' => $site['id'],
244 'name' => $site['name'],
245 'shellUrl' => $site['shellUrl'],
246 'kind' => $site['kind'],
247 // Every entry but this install's own is another install: the
248 // hub's sites and the other members alike.
249 'foreign' => ! $mine,
250 );
251 }
252 if ( '' === $current ) {
253 // Listed by the hub or not, this site is where the user stands.
254 $current = 'member:self';
255 $sites[] = array(
256 'id' => $current,
257 'name' => (string) get_bloginfo( 'name' ),
258 'shellUrl' => esc_url_raw( admin_url( 'admin.php?page=' . OPENSTATION_SHELL_PAGE_SLUG ) ),
259 'kind' => 'member',
260 'foreign' => false,
261 );
262 }
263 $admin = $hub['list']['networkAdmin'];
264 if ( is_array( $admin ) ) {
265 $admin['foreign'] = true;
266 }
267 return array(
268 'isNetworkAdmin' => false,
269 'networkAdmin' => ( $admin && current_user_can( 'manage_options' ) ) ? $admin : null,
270 'current' => $current,
271 'sites' => $sites,
272 );
273 }
274