PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 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 All 35 releases
desktop-mode / includes / framework / app / class-runtime.php

class-runtime.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.11, at includes/framework/app/class-runtime.php

259 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation App Framework — dispatch runtime.
4 *
5 * The whole request cycle of a window, host-agnostic:
6 *
7 * request { action, state, args, client }
8 * → rebuild State from the app's declared defaults
9 * → run the action (or `mount` / the built-in `set`)
10 * → render the view
11 * response { state, html, effects }
12 *
13 * The host (a REST route on WordPress, anything on a bare PHP host)
14 * only has to move those two arrays over the wire. A failure comes
15 * back as `array( 'ok' => false, 'error' => <code>, 'status' => <http> )`
16 * with an English `message` the host may translate.
17 *
18 * @package OpenStation
19 */
20
21 namespace OpenStation\App;
22
23 use OpenStation\App;
24
25 // Direct access, unless a standalone host is booting on bare PHP.
26 if ( ! defined( 'ABSPATH' ) ) {
27 defined( 'OPENSTATION_STANDALONE' ) || exit;
28 }
29
30 /**
31 * Runs dispatches against the registry.
32 */
33 final class Runtime {
34
35 /** First render of a window. Runs the app's `mount` hook, if any. */
36 const ACTION_MOUNT = 'mount';
37
38 /**
39 * The reopen lifecycle action — an open window asked to open
40 * again, from a URL naming another of its tabs.
41 */
42 const ACTION_REOPEN = 'reopen';
43
44 /** Built-in: the client changed a bound key; nothing to run, just re-render. */
45 const ACTION_SET = 'set';
46
47 /**
48 * Built-in: recompute `data()` and re-render, nothing else. Both
49 * first apps declared an empty action just to get this; declaring
50 * a `refresh` handler still works and wins, for the app that also
51 * wants to reset something on the way.
52 */
53 const ACTION_REFRESH = 'refresh';
54
55 /**
56 * @var Registry
57 */
58 private $registry;
59
60 public function __construct( Registry $registry ) {
61 $this->registry = $registry;
62 }
63
64 /**
65 * The registry this runtime dispatches into.
66 *
67 * @return Registry
68 */
69 public function registry() {
70 return $this->registry;
71 }
72
73 /**
74 * Run one dispatch.
75 *
76 * @param string $app_id App id.
77 * @param array<string,mixed> $request `action` (string), `state` (array), `args` (array), `client` (array).
78 * @param Os $os Host handle for the acting user.
79 * @return array<string,mixed> `ok`, then `state` / `html` / `effects` on success or
80 * `error` / `message` / `status` on failure.
81 */
82 public function dispatch( $app_id, array $request, Os $os ) {
83 $app = $this->registry->get( $app_id );
84 if ( ! $app ) {
85 return self::failure( 'not_found', 'Unknown app.', 404 );
86 }
87 if ( ! $app->allows( $os ) ) {
88 return self::failure( 'forbidden', 'You are not allowed to use this window.', 403 );
89 }
90
91 $action = isset( $request['action'] ) ? strtolower( (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $request['action'] ) ) : '';
92 $args = isset( $request['args'] ) && is_array( $request['args'] ) ? $request['args'] : array();
93 $view = isset( $request['view'] ) ? strtolower( (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $request['view'] ) ) : 'main';
94 if ( '' === $view ) {
95 $view = 'main';
96 }
97 if ( ! $app->has_view( $view ) ) {
98 return self::failure( 'unknown_view', sprintf( 'Unknown view "%s".', $view ), 400 );
99 }
100 $state = new State(
101 $app->defaults(),
102 isset( $request['state'] ) && is_array( $request['state'] ) ? $request['state'] : array()
103 );
104 $os->begin(
105 isset( $request['client'] ) && is_array( $request['client'] ) ? $request['client'] : array(),
106 isset( $request['params'] ) && is_array( $request['params'] ) ? $request['params'] : array(),
107 $app->id(),
108 $view
109 );
110
111 // A window that declares a menu lands on the tab the opener
112 // asked for — the dock row that was picked — on the first
113 // render and again whenever it is reopened from another row.
114 // Generic, so no app has to remember to wire it.
115 if ( self::ACTION_MOUNT === $action || self::ACTION_REOPEN === $action ) {
116 self::apply_menu_tab( $app, $state, $os );
117 }
118
119 try {
120 if ( self::ACTION_MOUNT === $action ) {
121 $app->run_mount( $state, $os );
122 } elseif ( self::ACTION_REOPEN === $action && ! $app->has_action( $action ) ) {
123 // The tab above WAS the reopen. An app that wants more
124 // declares the action and gets it as well.
125 $state->get( 'tab' );
126 } elseif ( self::ACTION_SET === $action ) {
127 // State already carries the bound value.
128 $app->run_action( self::ACTION_SET, $state, $os, $args, false );
129 } elseif ( $app->has_action( $action ) ) {
130 $app->run_action( $action, $state, $os, $args );
131 } elseif ( self::ACTION_REFRESH !== $action ) {
132 // A bare `refresh` (no declared handler) falls through on
133 // purpose: recomputing `data()` below IS the action, and
134 // declaring an empty handler to get it is boilerplate.
135 return self::failure( 'unknown_action', sprintf( 'Unknown action "%s".', $action ), 400 );
136 }
137
138 $data = $app->has_data() ? $app->compute_data( $state, $os ) : null;
139 $html = $app->render( $state, $os, $view );
140 } catch ( \Throwable $e ) {
141 return self::failure( 'action_failed', $e->getMessage(), 500 );
142 }
143
144 $response = array(
145 'ok' => true,
146 'state' => $state->all(),
147 'html' => $html,
148 'effects' => $os->effects->all(),
149 );
150 if ( null !== $data ) {
151 $response['data'] = $data;
152 }
153
154 /**
155 * Filter a dispatch response before it leaves the runtime.
156 *
157 * @param array<string,mixed> $response `ok`, `state`, `html`, `effects`.
158 * @param string $app_id App id.
159 * @param string $action Action that ran.
160 * @param State $state Final state.
161 */
162 $filtered = $os->filter( 'openstation_app_response', $response, $app->id(), $action, $state );
163
164 return is_array( $filtered ) ? $filtered : $response;
165 }
166
167 /**
168 * Render an app straight from a state array — no action, no
169 * request cycle. What a host calls to get "the whole window" as
170 * a value: the manifest plus the body it would paint.
171 *
172 * @param string $app_id App id.
173 * @param array<string,mixed> $state State values (partial; defaults fill the rest).
174 * @param Os $os Host handle.
175 * @return array<string,mixed> `manifest`, `state`, `html`, `effects` — or a failure array.
176 */
177 public function describe( $app_id, array $state, Os $os ) {
178 $app = $this->registry->get( $app_id );
179 if ( ! $app ) {
180 return self::failure( 'not_found', 'Unknown app.', 404 );
181 }
182 if ( ! $app->allows( $os ) ) {
183 return self::failure( 'forbidden', 'You are not allowed to use this window.', 403 );
184 }
185 $os->begin( array(), array(), $app->id(), 'main' );
186 $window_state = new State( $app->defaults(), $state );
187 try {
188 $app->run_mount( $window_state, $os );
189 $data = $app->has_data() ? $app->compute_data( $window_state, $os ) : null;
190 $html = $app->render( $window_state, $os );
191 $tabs = array();
192 foreach ( $app->tabs() as $tab ) {
193 $os->view = $tab['value'];
194 $tabs[ $tab['value'] ] = $app->render( new State( $app->defaults(), $state ), $os, $tab['value'] );
195 }
196 } catch ( \Throwable $e ) {
197 return self::failure( 'action_failed', $e->getMessage(), 500 );
198 }
199 return array(
200 'ok' => true,
201 'manifest' => $app->manifest(),
202 'state' => $window_state->all(),
203 'html' => $html,
204 'data' => $data,
205 'tabs' => $tabs,
206 'effects' => $os->effects->all(),
207 );
208 }
209
210 /**
211 * Land on the tab the opener named.
212 *
213 * The dock's rows for a window that declares a menu carry
214 * `os_tab=<id>`, which the shell passes as the window's `tab`
215 * open-time param. An id the window does not have is ignored
216 * rather than corrected: the value comes from a URL, and a
217 * window landing somewhere unexpected is worse than one landing
218 * where it always does.
219 *
220 * @param App $app The app.
221 * @param State $state State to write to.
222 * @param Os $os Host handle, carrying the params.
223 * @return void
224 */
225 private static function apply_menu_tab( App $app, State $state, Os $os ) {
226 $tabs = $app->menu_tabs();
227 if ( ! $tabs ) {
228 return;
229 }
230 $wanted = (string) $os->param( 'tab', '' );
231 if ( '' === $wanted ) {
232 return;
233 }
234 foreach ( $tabs as $tab ) {
235 if ( $tab['id'] === $wanted ) {
236 $state->set( 'tab', $wanted );
237 return;
238 }
239 }
240 }
241
242 /**
243 * Shape a failure.
244 *
245 * @param string $code Machine code.
246 * @param string $message English message.
247 * @param int $status HTTP status the host should use.
248 * @return array<string,mixed>
249 */
250 private static function failure( $code, $message, $status ) {
251 return array(
252 'ok' => false,
253 'error' => $code,
254 'message' => $message,
255 'status' => (int) $status,
256 );
257 }
258 }
259