PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.0
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 / desktop-files / openers.php

openers.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.0.0, at includes/desktop-files/openers.php

359 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — file-opener registry.
4 *
5 * An "opener" is the equivalent of a default-app association in a
6 * desktop OS: it answers the question "what should happen when the
7 * user double-clicks a `post` file?" with a discrete choice — open
8 * Gutenberg, open the Classic Editor, open someone-else's modal,
9 * etc. Multiple openers can register for the same file-type slug;
10 * the user picks their preferred one in OS Settings → File
11 * Associations (Phase 5), and the JS side resolves on every
12 * double-click using this chain:
13 *
14 * 1. The user's per-type override (`desktop_mode_file_associations`
15 * user meta).
16 * 2. The opener marked `is_default => true` for the type.
17 * 3. The first registered opener for the type (sort order).
18 * 4. No-op (the file simply can't be opened).
19 *
20 * The PHP side is metadata-only — opener handlers (URL builders,
21 * JS callbacks) live on the JS side because closures don't
22 * serialize across the shell payload. The JS module mirrors the
23 * registration surface and ships a `handler` field with the
24 * actual logic; the PHP entry feeds the OS Settings UI and
25 * validates user-meta choices against the known set.
26 *
27 * @package OpenStation
28 */
29
30 defined( 'ABSPATH' ) || exit;
31
32 /**
33 * User-meta key holding `{ type => opener_id, … }`.
34 *
35 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
36 * persisted or externally-visible identifier, so renaming it would
37 * orphan data already written by live installs (or break a live
38 * URL). The mismatch between this constant's name and its value is
39 * deliberate — it is NOT a half-finished rename.
40 */
41 define( 'OPENSTATION_FILE_ASSOCIATIONS_META', 'desktop_mode_file_associations' );
42
43 /**
44 * Internal static-store registry. Same pattern as the file-type
45 * and wallpaper registries.
46 *
47 * @internal
48 */
49 function openstation_file_opener_registry( $id = '', $entry = null ) {
50 static $store = array();
51
52 if ( '' === (string) $id ) {
53 return $store;
54 }
55 if ( null !== $entry ) {
56 $store[ (string) $id ] = $entry;
57 }
58 return isset( $store[ (string) $id ] ) ? $store[ (string) $id ] : null;
59 }
60
61 /**
62 * Registers a file-opener.
63 *
64 * @param string $id Opener id, e.g. `'gutenberg'`. Unique across
65 * openers; collisions overwrite (late wins).
66 * @param array $args {
67 * @type string $label Required. Picker label.
68 * @type string[] $types Required. File-type slugs this
69 * opener handles (`['post']`,
70 * `['post','page']`, etc.).
71 * @type bool $is_default Whether this opener is the
72 * ship-time default for ALL its
73 * types. The first matching
74 * default wins (registration
75 * order). Default false.
76 * @type int $sort Sort order in pickers. Default 100.
77 * @type string $script Optional handle the shell loads
78 * on activation so JS-side handlers
79 * defined in a plugin bundle appear
80 * without a page reload (Phase 5+).
81 * @type string[] $capabilities Gate: ALL caps must match.
82 * }
83 * @return true|WP_Error
84 */
85 function openstation_register_file_opener( $id, $args = array() ) {
86 $id = (string) $id;
87 if ( '' === $id ) {
88 return openstation_registration_error(
89 'openstation_missing_id',
90 __( 'Opener id is required.', 'desktop-mode' )
91 );
92 }
93
94 $defaults = array(
95 'label' => '',
96 'types' => array(),
97 'is_default' => false,
98 'sort' => 100,
99 'script' => '',
100 'capabilities' => array(),
101 );
102 $args = wp_parse_args( $args, $defaults );
103
104 foreach ( (array) $args['capabilities'] as $cap ) {
105 if ( ! current_user_can( (string) $cap ) ) {
106 return openstation_registration_error(
107 'openstation_capability_denied',
108 sprintf(
109 /* translators: %s: capability slug. */
110 __( 'Current user lacks the %s capability required to register this opener.', 'desktop-mode' ),
111 (string) $cap
112 ),
113 array(
114 'capability' => (string) $cap,
115 'id' => $id,
116 )
117 );
118 }
119 }
120
121 if ( '' === (string) $args['label'] ) {
122 return openstation_registration_error(
123 'openstation_missing_label',
124 __( 'Opener registration requires a non-empty `label`.', 'desktop-mode' ),
125 array( 'id' => $id )
126 );
127 }
128
129 $types = array_values( array_filter( array_map( 'strval', (array) $args['types'] ) ) );
130 if ( empty( $types ) ) {
131 return openstation_registration_error(
132 'openstation_missing_types',
133 __( 'Opener registration requires at least one file `type`.', 'desktop-mode' ),
134 array( 'id' => $id )
135 );
136 }
137
138 $entry = array(
139 'id' => $id,
140 'label' => (string) $args['label'],
141 'types' => $types,
142 'is_default' => (bool) $args['is_default'],
143 'sort' => (int) $args['sort'],
144 'script' => (string) $args['script'],
145 );
146 openstation_file_opener_registry( $id, $entry );
147
148 /**
149 * Fires after a file opener is successfully registered. Does
150 * NOT fire on `WP_Error` returns.
151 *
152 * @param string $id Opener id.
153 * @param array $entry Stored registry entry.
154 */
155 do_action( 'openstation_file_opener_registered', $id, $entry );
156
157 return true;
158 }
159
160 /**
161 * Returns every registered opener, sorted by `sort` then label.
162 *
163 * @return array[]
164 */
165 function openstation_get_file_openers() {
166 $registry = openstation_file_opener_registry();
167 if ( ! is_array( $registry ) || empty( $registry ) ) {
168 return array();
169 }
170
171 /**
172 * Filters the opener registry before consumers see it. Plugins
173 * can hide built-ins, swap labels, or rearrange sort order.
174 *
175 * @param array[] $registry Registered openers keyed by id.
176 */
177 $registry = apply_filters( 'openstation_file_openers', $registry );
178 if ( ! is_array( $registry ) ) {
179 return array();
180 }
181
182 $entries = array_values( $registry );
183 usort(
184 $entries,
185 static function ( $a, $b ) {
186 $sa = isset( $a['sort'] ) ? (int) $a['sort'] : 100;
187 $sb = isset( $b['sort'] ) ? (int) $b['sort'] : 100;
188 if ( $sa !== $sb ) {
189 return $sa - $sb;
190 }
191 return strcmp(
192 isset( $a['label'] ) ? (string) $a['label'] : '',
193 isset( $b['label'] ) ? (string) $b['label'] : ''
194 );
195 }
196 );
197 return $entries;
198 }
199
200 /**
201 * Returns openers that handle a given file type.
202 *
203 * @param string $type File-type slug.
204 * @return array[]
205 */
206 function openstation_get_file_openers_for_type( $type ) {
207 $type = (string) $type;
208 if ( '' === $type ) {
209 return array();
210 }
211 return array_values(
212 array_filter(
213 openstation_get_file_openers(),
214 static function ( $entry ) use ( $type ) {
215 return in_array( $type, (array) $entry['types'], true );
216 }
217 )
218 );
219 }
220
221 /**
222 * Resolves the opener id that should open a `(type, ref)` tuple
223 * for `$user_id`. Returns the id only — the JS side owns
224 * dispatch.
225 *
226 * Resolution chain:
227 * 1. User-meta override (per type).
228 * 2. `is_default` opener for the type.
229 * 3. First opener for the type (already sort-ordered).
230 *
231 * @param string $type File-type slug.
232 * @param int $user_id Viewer.
233 * @return string Opener id, or empty string when nothing matches.
234 */
235 function openstation_resolve_file_opener_id( $type, $user_id ) {
236 $candidates = openstation_get_file_openers_for_type( $type );
237 if ( empty( $candidates ) ) {
238 return '';
239 }
240 $by_id = array();
241 foreach ( $candidates as $entry ) {
242 $by_id[ $entry['id'] ] = $entry;
243 }
244
245 // 1. User override.
246 $override = '';
247 if ( $user_id > 0 ) {
248 $assoc = get_user_meta( (int) $user_id, OPENSTATION_FILE_ASSOCIATIONS_META, true );
249 if ( is_array( $assoc ) && isset( $assoc[ $type ] ) ) {
250 $override = (string) $assoc[ $type ];
251 }
252 }
253 if ( '' !== $override && isset( $by_id[ $override ] ) ) {
254 $resolved = $override;
255 } else {
256 // 2. Default opener.
257 $resolved = '';
258 foreach ( $candidates as $entry ) {
259 if ( ! empty( $entry['is_default'] ) ) {
260 $resolved = (string) $entry['id'];
261 break;
262 }
263 }
264 // 3. Fall back to the first registered opener.
265 if ( '' === $resolved ) {
266 $resolved = (string) $candidates[0]['id'];
267 }
268 }
269
270 /**
271 * Filters the resolved opener id for a `(type, user_id)`
272 * tuple. Plugins can override the user's choice — useful for
273 * role-based forced associations or for AB-testing a new
274 * editor before promoting it to default.
275 *
276 * @param string $resolved Resolved opener id.
277 * @param string $type File-type slug.
278 * @param int $user_id Viewer.
279 */
280 return (string) apply_filters( 'openstation_resolve_file_opener', $resolved, $type, $user_id );
281 }
282
283 /**
284 * Builds the openers payload sent to the shell.
285 *
286 * @return array[]
287 */
288 function openstation_build_file_openers_payload() {
289 $entries = openstation_get_file_openers();
290 if ( empty( $entries ) ) {
291 return array();
292 }
293 $out = array();
294 foreach ( $entries as $entry ) {
295 $handle = isset( $entry['script'] ) ? (string) $entry['script'] : '';
296 $payload = '' !== $handle
297 ? openstation_resolve_script_payload( $handle )
298 : array(
299 'url' => '',
300 'before' => array(),
301 'after' => array(),
302 'l10n' => array(),
303 'translations' => '',
304 );
305 $out[] = array(
306 'id' => (string) $entry['id'],
307 'label' => (string) $entry['label'],
308 'types' => array_values( (array) $entry['types'] ),
309 'isDefault' => (bool) $entry['is_default'],
310 'sort' => (int) $entry['sort'],
311 'scriptUrl' => $payload['url'],
312 'scriptHandle' => $handle,
313 'scriptBefore' => $payload['before'],
314 'scriptAfter' => $payload['after'],
315 'scriptL10n' => $payload['l10n'],
316 'scriptTranslations' => $payload['translations'],
317 );
318 }
319 return $out;
320 }
321
322 /**
323 * Returns the current user's `{ type => opener_id }` association
324 * map, sanitized against the registered openers (entries pointing
325 * at unknown ids are dropped from the returned map but kept in
326 * meta — a deactivated plugin that comes back later resumes its
327 * choice).
328 *
329 * @param int $user_id Viewer.
330 * @return array<string,string>
331 */
332 function openstation_get_user_file_associations( $user_id ) {
333 if ( $user_id <= 0 ) {
334 return array();
335 }
336 $raw = get_user_meta( (int) $user_id, OPENSTATION_FILE_ASSOCIATIONS_META, true );
337 if ( ! is_array( $raw ) ) {
338 return array();
339 }
340 $openers = openstation_get_file_openers();
341 $known = array();
342 foreach ( $openers as $entry ) {
343 $known[ (string) $entry['id'] ] = (array) $entry['types'];
344 }
345 $out = array();
346 foreach ( $raw as $type => $opener_id ) {
347 $type = (string) $type;
348 $opener_id = (string) $opener_id;
349 if ( ! isset( $known[ $opener_id ] ) ) {
350 continue;
351 }
352 if ( ! in_array( $type, $known[ $opener_id ], true ) ) {
353 continue;
354 }
355 $out[ $type ] = $opener_id;
356 }
357 return $out;
358 }
359