PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.7
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.7, at apps/posts/parts/query.php

302 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 * 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 'perPage' => 20,
73 'search' => '',
74 // `''` is the "All" sentinel — sent as `status=any`.
75 'status' => '',
76 'orderby' => (string) $orderby,
77 'order' => (string) $order,
78 // Author user ids and tag term ids to filter by; empty = no filter.
79 'author' => array(),
80 'tag' => array(),
81 );
82 }
83
84 /**
85 * The REST `orderby` values a column click may set. Anything else
86 * (a plugin column core cannot sort by) falls back to the declared
87 * default rather than reaching `WP_Query` as a stray key.
88 *
89 * @return string[]
90 */
91 function openstation_posts_app_allowed_orderby() {
92 return array( 'date', 'title', 'author', 'modified', 'comment_count', 'menu_order' );
93 }
94
95 /**
96 * The static facts the client view reads through `ctx.extra`: the
97 * mode, the editor URLs and the declared default sort — what the
98 * client returns to when a column sort is cleared. The Pages app
99 * layers its own facts on top in `openstation_pages_app_config()`.
100 *
101 * @param string $mode `posts` | `pages`.
102 * @param string $orderby Default sort column.
103 * @param string $order Default direction.
104 * @return array<string,mixed>
105 */
106 function openstation_posts_app_config( $mode, $orderby = 'date', $order = 'desc' ) {
107 return array(
108 'mode' => 'pages' === $mode ? 'pages' : 'posts',
109 'editPostUrlBase' => esc_url_raw( admin_url( 'post.php' ) ),
110 'newPostUrl' => 'pages' === $mode
111 ? esc_url_raw( add_query_arg( 'post_type', 'page', admin_url( 'post-new.php' ) ) )
112 : esc_url_raw( admin_url( 'post-new.php' ) ),
113 'defaultOrderby' => (string) $orderby,
114 'defaultOrder' => 'asc' === $order ? 'asc' : 'desc',
115 );
116 }
117
118 /**
119 * Positive integers out of a state list.
120 *
121 * @param mixed $value State value.
122 * @return int[]
123 */
124 function openstation_posts_app_ids( $value ) {
125 $out = array();
126 foreach ( (array) $value as $id ) {
127 $id = (int) $id;
128 if ( $id > 0 ) {
129 $out[] = $id;
130 }
131 }
132 return $out;
133 }
134
135 /**
136 * The REST query for a state — the legacy bundle's `fetchPosts()`
137 * URL, as query params.
138 *
139 * @param array<string,mixed> $defaults Filtered default args.
140 * @param State $state Declared state.
141 * @return array<string,mixed>
142 */
143 function openstation_posts_app_query( array $defaults, State $state ) {
144 $query = array();
145 // Merge the PHP-declared defaults first so `_fields`, `_embed`,
146 // and a custom `post_type` from the filter all flow through.
147 foreach ( $defaults as $key => $value ) {
148 if ( is_string( $value ) && '' !== $value ) {
149 $query[ (string) $key ] = $value;
150 }
151 }
152 $query['page'] = max( 1, (int) $state->get( 'page' ) );
153 $query['per_page'] = max( 1, (int) $state->get( 'perPage' ) );
154 $search = trim( (string) $state->get( 'search' ) );
155 if ( '' !== $search ) {
156 $query['search'] = $search;
157 }
158 // `status` quirk: omitted, core's handler defaults to `publish`
159 // only — drafts / pending / scheduled / private silently vanish
160 // from "All". `status=any` makes the All segment mean every
161 // status the user can see (trash has its own segment).
162 $status = (string) $state->get( 'status' );
163 $query['status'] = '' !== $status ? $status : 'any';
164 $orderby = (string) $state->get( 'orderby' );
165 if ( '' !== $orderby ) {
166 $query['orderby'] = $orderby;
167 }
168 $order = (string) $state->get( 'order' );
169 if ( in_array( $order, array( 'asc', 'desc' ), true ) ) {
170 $query['order'] = $order;
171 }
172 // Both `author` and `tags` are registered as integer arrays —
173 // union (OR) semantics: "rows whose author / tag is ANY of these".
174 $author = openstation_posts_app_ids( $state->get( 'author' ) );
175 if ( array() !== $author ) {
176 $query['author'] = $author;
177 }
178 $tag = openstation_posts_app_ids( $state->get( 'tag' ) );
179 if ( array() !== $tag ) {
180 $query['tags'] = $tag;
181 }
182 return $query;
183 }
184
185 /**
186 * The client data: the current page of rows as the paged-list
187 * envelope (plus `error`), with the "page out of range → page 1"
188 * recovery the legacy bundle did client-side (the typical case is
189 * the user on page 7 changing per-page from 10 to 100).
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 $list = openstation_app_rest_page( $route, $query );
199 // A page past the end — Core's `rest_post_invalid_page_number`
200 // refusal, or an empty page on a controller that tolerates it —
201 // lands on page 1 silently rather than render an empty table. A
202 // refusal for any other reason (a capability, a bad argument) is
203 // never retried: the error reaches the client as it is.
204 if ( openstation_app_rest_page_is_out_of_range( $list ) ) {
205 $state->set( 'page', 1 );
206 $query['page'] = 1;
207 $list = openstation_app_rest_page( $route, $query );
208 }
209 if ( 0 === $list['total'] ) {
210 // The legacy pager read "No posts" off a zero page count; the
211 // envelope floors `pages` at 1, so hand the client the truth.
212 $list['pages'] = 0;
213 }
214 return array( 'list' => $list );
215 }
216
217 /**
218 * `filter` — a query change (status, search, per-page, column
219 * filters) replaces the result set from page 1.
220 *
221 * @param State $state Declared state.
222 * @return void
223 */
224 function openstation_posts_app_filter( State $state ) {
225 $state->set( 'page', 1 );
226 }
227
228 /**
229 * `page` — move to the requested page.
230 *
231 * @param State $state Declared state.
232 * @param array<string,mixed> $args `page`.
233 * @return void
234 */
235 function openstation_posts_app_page( State $state, array $args ) {
236 $state->set( 'page', max( 1, isset( $args['page'] ) ? (int) $args['page'] : 1 ) );
237 }
238
239 /**
240 * `sort` — a column header click; the client maps the column key to
241 * the REST `orderby` value, and the server only keeps one it knows:
242 * anything outside `openstation_posts_app_allowed_orderby()` is the
243 * app's declared default.
244 *
245 * @param State $state Declared state.
246 * @param array<string,mixed> $args `orderby`, `order`.
247 * @param string $default_orderby The app's default sort column.
248 * @param string $default_order The app's default direction.
249 * @return void
250 */
251 function openstation_posts_app_sort( State $state, array $args, $default_orderby = 'date', $default_order = 'desc' ) {
252 $orderby = isset( $args['orderby'] ) ? sanitize_key( (string) $args['orderby'] ) : '';
253 if ( ! in_array( $orderby, openstation_posts_app_allowed_orderby(), true ) ) {
254 $orderby = (string) $default_orderby;
255 }
256 $order = isset( $args['order'] ) ? strtolower( (string) $args['order'] ) : (string) $default_order;
257 $state->set( 'orderby', $orderby );
258 $state->set( 'order', 'asc' === $order ? 'asc' : 'desc' );
259 }
260
261 /**
262 * `trash` — move the selected rows to the trash. Rows already in the
263 * trash are skipped (a second delete would remove them for good),
264 * every id is checked against `delete_post`, the survivors are
265 * announced as one content change (what the Recycle Bin, WP Explorer
266 * and every other window showing the type repaint on), and failures
267 * become a toast.
268 *
269 * @param Os $os Host handle.
270 * @param array<string,mixed> $args `ids`.
271 * @param string $type Content type announced (`post` | `page`).
272 * @return void
273 */
274 function openstation_posts_app_trash( Os $os, array $args, $type ) {
275 $ids = openstation_posts_app_ids( isset( $args['ids'] ) ? $args['ids'] : array() );
276 $ok = array();
277 $failed = 0;
278 foreach ( $ids as $id ) {
279 $post = get_post( $id );
280 if ( ! $post || 'trash' === $post->post_status ) {
281 continue;
282 }
283 if ( ! $os->can( 'delete_post', $id ) || ! wp_trash_post( $id ) ) {
284 ++$failed;
285 continue;
286 }
287 $ok[] = $id;
288 }
289 if ( array() !== $ok ) {
290 $os->announce( $type, 'trashed', $ok );
291 }
292 if ( $failed > 0 ) {
293 $os->toast(
294 sprintf(
295 /* translators: %d: number of rows that could not be trashed. */
296 _n( '%d item could not be moved to the trash.', '%d items could not be moved to the trash.', $failed, 'desktop-mode' ),
297 $failed
298 )
299 );
300 }
301 }
302