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 / apps / posts / parts / query.php

query.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.10, at apps/posts/parts/query.php

344 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Posts app — the list query, the data envelope and the server
4 * actions the Posts and Pages apps share.
5 *
6 * The list is the same `/wp/v2/posts` (or `/wp/v2/pages`) request the
7 * legacy bundle fetched from the browser, run in-process through
8 * `openstation_app_rest_page()`: the filterable default args
9 * (`_embed`, `_fields`, an optional `post_type`) merged with the
10 * state's page / per-page / search / status / sort / author / tag
11 * values. Every REST field a plugin registers and every
12 * `rest_post_query` filter keeps applying, and the row JSON the
13 * client view renders is byte-identical to what the old bundle got.
14 *
15 * Plain PHP part of `apps/posts/posts.os.php`; `apps/pages/pages.os.php`
16 * requires it too and hands in its own defaults, route and noun.
17 *
18 * @package OpenStation
19 */
20
21 defined( 'ABSPATH' ) || exit;
22
23 use OpenStation\App\Os;
24 use OpenStation\App\State;
25
26 /**
27 * Default REST query args the Posts window uses on every list fetch.
28 *
29 * Filterable so a plugin can flip the post type to a custom CPT (or
30 * a list of CPTs) without forking the app. The app merges these
31 * under the page/per-page/search/status/sort args it generates per
32 * request.
33 *
34 * @return array
35 */
36 function openstation_posts_window_default_query_args() {
37 $args = array(
38 // `_embed` pulls author + taxonomy + featured-media side-loads
39 // into `_embedded`, so the table can render avatars, term
40 // chips, and thumbnails without N extra round-trips per row.
41 '_embed' => 'author,wp:term,wp:featuredmedia',
42 // `openstation_lock` is the REST field registered by My WordPress'
43 // `lock.php` on every public post type — it tells us whether
44 // another user is currently editing the row, so the title cell
45 // can paint a small lock icon without an extra fetch.
46 '_fields' =>
47 'id,title,status,date,date_gmt,modified,modified_gmt,author,categories,tags,comment_status,excerpt,openstation_lock,_links,_embedded',
48 );
49
50 /**
51 * Filter the default outbound REST query args for the Posts window.
52 *
53 * Drop in a `'post_type' => 'product'` to point the window at a
54 * CPT, or extend `_fields` to ship more columns. The app merges
55 * these under pagination/search/status/sort args.
56 *
57 * @param array $args Default args.
58 */
59 return (array) apply_filters( 'openstation_posts_window_query_args', $args );
60 }
61
62 /**
63 * The declared state of a list app — the schema the client keeps.
64 *
65 * @param string $orderby Default sort column (`date` for posts, `menu_order` for pages).
66 * @param string $order Default direction.
67 * @return array<string,mixed>
68 */
69 function openstation_posts_app_state( $orderby = 'date', $order = 'desc' ) {
70 return array(
71 'page' => 1,
72 'feedAppend' => false,
73 'perPage' => 20,
74 'search' => '',
75 // `''` is the "All" sentinel — sent as `status=any`.
76 'status' => '',
77 'orderby' => (string) $orderby,
78 'order' => (string) $order,
79 // Author user ids and tag term ids to filter by; empty = no filter.
80 'author' => array(),
81 'tag' => array(),
82 );
83 }
84
85 /**
86 * The REST `orderby` values a column click may set. Anything else
87 * (a plugin column core cannot sort by) falls back to the declared
88 * default rather than reaching `WP_Query` as a stray key.
89 *
90 * @return string[]
91 */
92 function openstation_posts_app_allowed_orderby() {
93 return array( 'date', 'title', 'author', 'modified', 'comment_count', 'menu_order' );
94 }
95
96 /**
97 * The static facts the client view reads through `ctx.extra`: the
98 * mode, the editor URLs and the declared default sort — what the
99 * client returns to when a column sort is cleared. The Pages app
100 * layers its own facts on top in `openstation_pages_app_config()`.
101 *
102 * @param string $mode `posts` | `pages`.
103 * @param string $orderby Default sort column.
104 * @param string $order Default direction.
105 * @return array<string,mixed>
106 */
107 function openstation_posts_app_config( $mode, $orderby = 'date', $order = 'desc' ) {
108 return array(
109 'mode' => 'pages' === $mode ? 'pages' : 'posts',
110 'editPostUrlBase' => esc_url_raw( admin_url( 'post.php' ) ),
111 'newPostUrl' => 'pages' === $mode
112 ? esc_url_raw( add_query_arg( 'post_type', 'page', admin_url( 'post-new.php' ) ) )
113 : esc_url_raw( admin_url( 'post-new.php' ) ),
114 'defaultOrderby' => (string) $orderby,
115 'defaultOrder' => 'asc' === $order ? 'asc' : 'desc',
116 );
117 }
118
119 /**
120 * Positive integers out of a state list.
121 *
122 * @param mixed $value State value.
123 * @return int[]
124 */
125 function openstation_posts_app_ids( $value ) {
126 $out = array();
127 foreach ( (array) $value as $id ) {
128 $id = (int) $id;
129 if ( $id > 0 ) {
130 $out[] = $id;
131 }
132 }
133 return $out;
134 }
135
136 /**
137 * The REST query for a state — the legacy bundle's `fetchPosts()`
138 * URL, as query params.
139 *
140 * @param array<string,mixed> $defaults Filtered default args.
141 * @param State $state Declared state.
142 * @return array<string,mixed>
143 */
144 function openstation_posts_app_query( array $defaults, State $state ) {
145 $query = array();
146 // Merge the PHP-declared defaults first so `_fields`, `_embed`,
147 // and a custom `post_type` from the filter all flow through.
148 foreach ( $defaults as $key => $value ) {
149 if ( is_string( $value ) && '' !== $value ) {
150 $query[ (string) $key ] = $value;
151 }
152 }
153 $query['page'] = max( 1, (int) $state->get( 'page' ) );
154 $query['per_page'] = max( 1, (int) $state->get( 'perPage' ) );
155 $search = trim( (string) $state->get( 'search' ) );
156 if ( '' !== $search ) {
157 $query['search'] = $search;
158 }
159 // `status` quirk: omitted, core's handler defaults to `publish`
160 // only — drafts / pending / scheduled / private silently vanish
161 // from "All". `status=any` makes the All segment mean every
162 // status the user can see (trash has its own segment).
163 $status = (string) $state->get( 'status' );
164 $query['status'] = '' !== $status ? $status : 'any';
165 $orderby = (string) $state->get( 'orderby' );
166 if ( '' !== $orderby ) {
167 $query['orderby'] = $orderby;
168 }
169 $order = (string) $state->get( 'order' );
170 if ( in_array( $order, array( 'asc', 'desc' ), true ) ) {
171 $query['order'] = $order;
172 }
173 // Both `author` and `tags` are registered as integer arrays —
174 // union (OR) semantics: "rows whose author / tag is ANY of these".
175 $author = openstation_posts_app_ids( $state->get( 'author' ) );
176 if ( array() !== $author ) {
177 $query['author'] = $author;
178 }
179 $tag = openstation_posts_app_ids( $state->get( 'tag' ) );
180 if ( array() !== $tag ) {
181 $query['tags'] = $tag;
182 }
183 return $query;
184 }
185
186 /**
187 * The client data: a continuation page or a refreshed loaded prefix,
188 * with a query identity for rejecting stale responses. Out-of-range
189 * continuations recover to page one; failed refreshes retain paging.
190 *
191 * @param string $route `wp/v2/posts` | `wp/v2/pages`.
192 * @param array<string,mixed> $defaults Filtered default args.
193 * @param State $state Declared state.
194 * @return array<string,mixed> `list`.
195 */
196 function openstation_posts_app_data( $route, array $defaults, State $state ) {
197 $query = openstation_posts_app_query( $defaults, $state );
198 $append = (bool) $state->get( 'feedAppend' );
199 $state->set( 'feedAppend', false );
200 // Refresh the loaded prefix in one HTTP response. Core still enforces
201 // permissions, query filters and its 100-row bound for each internal batch.
202 if ( ! $append && $query['page'] > 1 ) {
203 $wanted = $query['page'] * $query['per_page'];
204 $refresh = $query;
205 $refresh['page'] = 1;
206 $refresh['per_page'] = min( 100, $wanted );
207 $list = openstation_app_rest_page( $route, $refresh );
208 $wanted = min( $wanted, $list['total'] );
209 $received = count( $list['items'] );
210 while ( empty( $list['error'] ) && $received < $wanted ) {
211 ++$refresh['page'];
212 $batch = openstation_app_rest_page( $route, $refresh );
213 if ( ! empty( $batch['error'] ) ) {
214 $list = $batch;
215 break;
216 }
217 if ( empty( $batch['items'] ) ) {
218 break;
219 }
220 $list['items'] = array_merge( $list['items'], $batch['items'] );
221 $received = count( $list['items'] );
222 }
223 $list['items'] = array_slice( $list['items'], 0, $wanted );
224 $list['perPage'] = $query['per_page'];
225 $list['pages'] = (int) ceil( $list['total'] / $query['per_page'] );
226 $list['page'] = empty( $list['error'] ) ? min( $query['page'], max( 1, $list['pages'] ) ) : $query['page'];
227 $state->set( 'page', $list['page'] );
228 $list['replace'] = true;
229 } else {
230 $list = openstation_app_rest_page( $route, $query );
231 }
232 // A page past the end — Core's `rest_post_invalid_page_number`
233 // refusal, or an empty page on a controller that tolerates it —
234 // lands on page 1 silently rather than render an empty table. A
235 // refusal for any other reason (a capability, a bad argument) is
236 // never retried: the error reaches the client as it is.
237 if ( openstation_app_rest_page_is_out_of_range( $list ) ) {
238 $state->set( 'page', 1 );
239 $query['page'] = 1;
240 $list = openstation_app_rest_page( $route, $query );
241 }
242 if ( 0 === $list['total'] ) {
243 // The legacy pager read "No posts" off a zero page count; the
244 // envelope floors `pages` at 1, so hand the client the truth.
245 $list['pages'] = 0;
246 }
247 $identity = array();
248 foreach ( array( 'search', 'status', 'orderby', 'order', 'author', 'tag', 'perPage' ) as $key ) {
249 $identity[ $key ] = $state->get( $key );
250 }
251 return array(
252 'list' => $list,
253 'query' => $identity,
254 );
255 }
256
257 /**
258 * `filter` — a query change (status, search, per-page, column
259 * filters) replaces the result set from page 1.
260 *
261 * @param State $state Declared state.
262 * @return void
263 */
264 function openstation_posts_app_filter( State $state ) {
265 $state->set( 'page', 1 );
266 }
267
268 /**
269 * `page` — move to the requested page.
270 *
271 * @param State $state Declared state.
272 * @param array<string,mixed> $args `page`.
273 * @return void
274 */
275 function openstation_posts_app_page( State $state, array $args ) {
276 $state->set( 'page', max( 1, isset( $args['page'] ) ? (int) $args['page'] : 1 ) );
277 $state->set( 'feedAppend', true );
278 }
279
280 /**
281 * `sort` — a column header click; the client maps the column key to
282 * the REST `orderby` value, and the server only keeps one it knows:
283 * anything outside `openstation_posts_app_allowed_orderby()` is the
284 * app's declared default.
285 *
286 * @param State $state Declared state.
287 * @param array<string,mixed> $args `orderby`, `order`.
288 * @param string $default_orderby The app's default sort column.
289 * @param string $default_order The app's default direction.
290 * @return void
291 */
292 function openstation_posts_app_sort( State $state, array $args, $default_orderby = 'date', $default_order = 'desc' ) {
293 $orderby = isset( $args['orderby'] ) ? sanitize_key( (string) $args['orderby'] ) : '';
294 if ( ! in_array( $orderby, openstation_posts_app_allowed_orderby(), true ) ) {
295 $orderby = (string) $default_orderby;
296 }
297 $order = isset( $args['order'] ) ? strtolower( (string) $args['order'] ) : (string) $default_order;
298 $state->set( 'page', 1 );
299 $state->set( 'orderby', $orderby );
300 $state->set( 'order', 'asc' === $order ? 'asc' : 'desc' );
301 }
302
303 /**
304 * `trash` — move the selected rows to the trash. Rows already in the
305 * trash are skipped (a second delete would remove them for good),
306 * every id is checked against `delete_post`, the survivors are
307 * announced as one content change (what the Recycle Bin, WP Explorer
308 * and every other window showing the type repaint on), and failures
309 * become a toast.
310 *
311 * @param Os $os Host handle.
312 * @param array<string,mixed> $args `ids`.
313 * @param string $type Content type announced (`post` | `page`).
314 * @return void
315 */
316 function openstation_posts_app_trash( Os $os, array $args, $type ) {
317 $ids = openstation_posts_app_ids( isset( $args['ids'] ) ? $args['ids'] : array() );
318 $ok = array();
319 $failed = 0;
320 foreach ( $ids as $id ) {
321 $post = get_post( $id );
322 if ( ! $post || 'trash' === $post->post_status ) {
323 continue;
324 }
325 if ( ! $os->can( 'delete_post', $id ) || ! wp_trash_post( $id ) ) {
326 ++$failed;
327 continue;
328 }
329 $ok[] = $id;
330 }
331 if ( array() !== $ok ) {
332 $os->announce( $type, 'trashed', $ok );
333 }
334 if ( $failed > 0 ) {
335 $os->toast(
336 sprintf(
337 /* translators: %d: number of rows that could not be trashed. */
338 _n( '%d item could not be moved to the trash.', '%d items could not be moved to the trash.', $failed, 'desktop-mode' ),
339 $failed
340 )
341 );
342 }
343 }
344