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
← All changes | includes/content-graph/rest.php +148 -82 0.9.11.1.10 View file →
@@ -1,17 +1,19 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Content Graph: REST routes.
3 + * OpenStation — Content Graph: REST routes.
4 4 *
5 5 * Three endpoints under `desktop-mode/v1/content-graph`:
6 6 *
7 7 * GET /post-types
8 8 * Lists the types eligible for the graph (`slug`, `label`, `icon`,
9 - * `count`).
9 + * `count`, `taxonomies`).
10 10 *
11 11 * GET /nodes?types=post,page,...
12 - * Returns the full `{ nodes, edges, stats }` tuple. Cached server-
13 - * side, see graph-builder.php.
12 + * Returns the full `{ nodes, edges, groups, stats }` tuple. Cached
13 + * server-side, see graph-builder.php. `types` omitted means every
14 + * registered type; `types=` (present but empty) means none — the
15 + * shell sends the latter when every toolbar chip is off.
14 16 *
15 17 * GET /post/<id>
16 18 * Returns the side-panel detail bundle for one post:
17 19 * { post: {...}, author, contributors, comments, categories,
@@ -16,10 +18,9 @@
16 18 * Returns the side-panel detail bundle for one post:
17 19 * { post: {...}, author, contributors, comments, categories,
18 20 * attached_media, revisions }.
19 21 *
20 - * @package WPDesktopMode
21 - * @since 0.8.2
22 + * @package OpenStation
22 23 */
23 24
24 25 defined( 'ABSPATH' ) || exit;
25 26
@@ -25,29 +26,25 @@
25 26
26 27 /**
27 28 * Capability check shared across every endpoint.
28 29 *
29 - * @since 0.8.2
30 - *
31 30 * @return bool
32 31 */
33 -function desktop_mode_content_graph_rest_permission() {
34 - return desktop_mode_content_graph_user_can_use();
32 +function openstation_content_graph_rest_permission() {
33 + return openstation_content_graph_user_can_use();
35 34 }
36 35
37 36 /**
38 37 * Register the routes.
39 - *
40 - * @since 0.8.2
41 38 */
42 -function desktop_mode_content_graph_register_routes() {
39 +function openstation_content_graph_register_routes() {
43 40 register_rest_route(
44 41 'desktop-mode/v1',
45 42 '/content-graph/post-types',
46 43 array(
47 44 'methods' => WP_REST_Server::READABLE,
48 - 'callback' => 'desktop_mode_content_graph_rest_post_types',
49 - 'permission_callback' => 'desktop_mode_content_graph_rest_permission',
45 + 'callback' => 'openstation_content_graph_rest_post_types',
46 + 'permission_callback' => 'openstation_content_graph_rest_permission',
50 47 )
51 48 );
52 49 register_rest_route(
53 50 'desktop-mode/v1',
@@ -53,15 +50,20 @@
53 50 'desktop-mode/v1',
54 51 '/content-graph/nodes',
55 52 array(
56 53 'methods' => WP_REST_Server::READABLE,
57 - 'callback' => 'desktop_mode_content_graph_rest_nodes',
58 - 'permission_callback' => 'desktop_mode_content_graph_rest_permission',
54 + 'callback' => 'openstation_content_graph_rest_nodes',
55 + 'permission_callback' => 'openstation_content_graph_rest_permission',
59 56 'args' => array(
57 + // Deliberately no `default`: the dispatcher copies a
58 + // registered default into the request before the
59 + // callback runs, and `WP_REST_Request::has_param()`
60 + // sees it, so a default here would make an omitted
61 + // parameter indistinguishable from an explicitly empty
62 + // one — and the callback tells those two apart.
60 63 'types' => array(
61 - 'description' => 'Comma-separated list of post type slugs to include.',
64 + 'description' => 'Comma-separated list of post type slugs to include. Omit for every registered type; pass an empty value for none.',
62 65 'type' => 'string',
63 - 'default' => '',
64 66 ),
65 67 ),
66 68 )
67 69 );
@@ -69,10 +71,10 @@
69 71 'desktop-mode/v1',
70 72 '/content-graph/post/(?P<id>\d+)',
71 73 array(
72 74 'methods' => WP_REST_Server::READABLE,
73 - 'callback' => 'desktop_mode_content_graph_rest_post_detail',
74 - 'permission_callback' => 'desktop_mode_content_graph_rest_permission',
75 + 'callback' => 'openstation_content_graph_rest_post_detail',
76 + 'permission_callback' => 'openstation_content_graph_rest_permission',
75 77 'args' => array(
76 78 'id' => array(
77 79 'type' => 'integer',
78 80 'required' => true,
@@ -80,23 +82,25 @@
80 82 ),
81 83 )
82 84 );
83 85 }
84 -add_action( 'rest_api_init', 'desktop_mode_content_graph_register_routes' );
86 +add_action( 'rest_api_init', 'openstation_content_graph_register_routes' );
85 87
86 88 /**
87 89 * GET /post-types
88 90 *
89 - * @since 0.8.2
90 - *
91 91 * @return WP_REST_Response
92 92 */
93 -function desktop_mode_content_graph_rest_post_types() {
94 - $types = desktop_mode_content_graph_post_types();
93 +function openstation_content_graph_rest_post_types() {
94 + $types = openstation_content_graph_post_types();
95 95 $out = array();
96 96 foreach ( $types as $entry ) {
97 - $slug = isset( $entry['slug'] ) ? (string) $entry['slug'] : '';
98 - $counts = $slug ? wp_count_posts( $slug ) : null;
97 + $slug = isset( $entry['slug'] ) ? (string) $entry['slug'] : '';
98 + // 'readable' scopes the private bucket to posts the current
99 + // user can actually read (others' private posts require the
100 + // type's read_private_posts capability), keeping the filter-bar
101 + // counts consistent with the rows /nodes returns.
102 + $counts = $slug ? wp_count_posts( $slug, 'readable' ) : null;
99 103 $count = 0;
100 104 if ( $counts && isset( $counts->publish ) ) {
101 105 $count = (int) $counts->publish;
102 106 if ( isset( $counts->private ) ) {
@@ -103,12 +107,13 @@
103 107 $count += (int) $counts->private;
104 108 }
105 109 }
106 110 $out[] = array(
107 - 'slug' => $slug,
108 - 'label' => isset( $entry['label'] ) ? (string) $entry['label'] : $slug,
109 - 'icon' => isset( $entry['icon'] ) ? (string) $entry['icon'] : 'dashicons-admin-post',
110 - 'count' => $count,
111 + 'slug' => $slug,
112 + 'label' => isset( $entry['label'] ) ? (string) $entry['label'] : $slug,
113 + 'icon' => isset( $entry['icon'] ) ? (string) $entry['icon'] : 'dashicons-admin-post',
114 + 'count' => $count,
115 + 'taxonomies' => $entry['taxonomies'],
111 116 );
112 117 }
113 118 return rest_ensure_response( $out );
114 119 }
@@ -115,35 +120,97 @@
115 120
116 121 /**
117 122 * GET /nodes
118 123 *
119 - * @since 0.8.2
124 + * An omitted `types` parameter selects every registered type; a
125 + * present-but-empty one selects none. The route registers no default
126 + * for the parameter so the two stay distinguishable through a real
127 + * dispatch (see the route registration above).
120 128 *
121 129 * @param WP_REST_Request $request
122 130 * @return WP_REST_Response
123 131 */
124 -function desktop_mode_content_graph_rest_nodes( WP_REST_Request $request ) {
125 - $raw = (string) $request->get_param( 'types' );
126 - $types = '' === $raw
127 - ? wp_list_pluck( desktop_mode_content_graph_post_types(), 'slug' )
128 - : array_map( 'trim', explode( ',', $raw ) );
129 - return rest_ensure_response( desktop_mode_content_graph_build( (array) $types ) );
132 +function openstation_content_graph_rest_nodes( WP_REST_Request $request ) {
133 + $raw = $request->get_param( 'types' );
134 + $types = $request->has_param( 'types' )
135 + ? array_map( 'trim', explode( ',', (string) $raw ) )
136 + : wp_list_pluck( openstation_content_graph_post_types(), 'slug' );
137 + $payload = openstation_content_graph_build( (array) $types );
138 + return rest_ensure_response( openstation_content_graph_filter_payload_for_user( $payload ) );
130 139 }
131 140
132 141 /**
142 + * Strip revision-derived data the current user may not see from a
143 + * graph payload before it goes out.
144 + *
145 + * Revision authorship is edit-level data in core (wp/v2 exposes a
146 + * post's revisions only behind `edit_post`), so each node's
147 + * `contributor_ids` — distinct revision authors — are emptied for
148 + * posts the user cannot `edit_post`. Authors-catalog entries that
149 + * were referenced only via stripped contributor ids are removed too.
150 + * This runs at response time, not build time, because the cached
151 + * payload is shared across users of the same privilege tier.
152 + *
153 + * @param array $payload Payload from `openstation_content_graph_build()`.
154 + * @return array
155 + */
156 +function openstation_content_graph_filter_payload_for_user( array $payload ) {
157 + if ( empty( $payload['nodes'] ) || ! is_array( $payload['nodes'] ) ) {
158 + return $payload;
159 + }
160 +
161 + // Bulk-warm the post cache for the cap checks — only nodes that
162 + // actually carry contributor ids need an edit_post decision.
163 + $check_ids = array();
164 + foreach ( $payload['nodes'] as $node ) {
165 + if ( ! empty( $node['contributor_ids'] ) && ! empty( $node['id'] ) ) {
166 + $check_ids[] = (int) $node['id'];
167 + }
168 + }
169 + if ( ! empty( $check_ids ) && function_exists( '_prime_post_caches' ) ) {
170 + _prime_post_caches( $check_ids, false, false );
171 + }
172 +
173 + $referenced = array();
174 + foreach ( $payload['nodes'] as $i => $node ) {
175 + $id = isset( $node['id'] ) ? (int) $node['id'] : 0;
176 + $contribs = isset( $node['contributor_ids'] ) && is_array( $node['contributor_ids'] )
177 + ? $node['contributor_ids']
178 + : array();
179 + if ( ! empty( $contribs ) && ! current_user_can( 'edit_post', $id ) ) {
180 + $contribs = array();
181 + $payload['nodes'][ $i ]['contributor_ids'] = array();
182 + }
183 + $author_id = isset( $node['author_id'] ) ? (int) $node['author_id'] : 0;
184 + if ( $author_id > 0 ) {
185 + $referenced[ $author_id ] = true;
186 + }
187 + foreach ( $contribs as $cid ) {
188 + if ( (int) $cid > 0 ) {
189 + $referenced[ (int) $cid ] = true;
190 + }
191 + }
192 + }
193 +
194 + if ( isset( $payload['groups']['authors'] ) && is_array( $payload['groups']['authors'] ) ) {
195 + $payload['groups']['authors'] = array_intersect_key( $payload['groups']['authors'], $referenced );
196 + }
197 +
198 + return $payload;
199 +}
200 +
201 +/**
133 202 * GET /post/<id>
134 203 *
135 - * @since 0.8.2
136 - *
137 204 * @param WP_REST_Request $request
138 205 * @return WP_REST_Response|WP_Error
139 206 */
140 -function desktop_mode_content_graph_rest_post_detail( WP_REST_Request $request ) {
207 +function openstation_content_graph_rest_post_detail( WP_REST_Request $request ) {
141 208 $id = (int) $request['id'];
142 209 $post = $id > 0 ? get_post( $id ) : null;
143 210 if ( ! $post ) {
144 211 return new WP_Error(
145 - 'desktop_mode_content_graph_post_not_found',
212 + 'openstation_content_graph_post_not_found',
146 213 __( 'Post not found.', 'desktop-mode' ),
147 214 array( 'status' => 404 )
148 215 );
149 216 }
@@ -148,20 +215,25 @@
148 215 );
149 216 }
150 217 if ( ! current_user_can( 'read_post', $id ) ) {
151 218 return new WP_Error(
152 - 'desktop_mode_content_graph_forbidden',
219 + 'openstation_content_graph_forbidden',
153 220 __( 'Insufficient permissions.', 'desktop-mode' ),
154 221 array( 'status' => 403 )
155 222 );
156 223 }
157 224
158 - $author = desktop_mode_content_graph_format_user( (int) $post->post_author );
159 - $contributors = desktop_mode_content_graph_collect_contributors( $post );
160 - $comments = desktop_mode_content_graph_collect_comments( $post );
161 - $categories = desktop_mode_content_graph_collect_terms( $post );
162 - $attached = desktop_mode_content_graph_collect_attached_media( $post );
163 - $revisions = desktop_mode_content_graph_collect_revisions( $post );
225 + // Revision history (and the identities of who edited the post) is
226 + // edit-level data in core — wp/v2 only exposes revisions behind
227 + // edit_post. Mirror that: readers get comment-author contributors
228 + // only, no revision list.
229 + $can_edit = current_user_can( 'edit_post', $post->ID );
230 + $author = openstation_content_graph_format_user( (int) $post->post_author );
231 + $contributors = openstation_content_graph_collect_contributors( $post, $can_edit );
232 + $comments = openstation_content_graph_collect_comments( $post );
233 + $categories = openstation_content_graph_collect_terms( $post );
234 + $attached = openstation_content_graph_collect_attached_media( $post );
235 + $revisions = $can_edit ? openstation_content_graph_collect_revisions( $post ) : array();
164 236
165 237 return rest_ensure_response(
166 238 array(
167 239 'post' => array(
@@ -187,14 +259,12 @@
187 259
188 260 /**
189 261 * Format a user record for the side panel.
190 262 *
191 - * @since 0.8.2
192 - *
193 263 * @param int $user_id
194 264 * @return array|null
195 265 */
196 -function desktop_mode_content_graph_format_user( $user_id ) {
266 +function openstation_content_graph_format_user( $user_id ) {
197 267 $user_id = (int) $user_id;
198 268 if ( $user_id <= 0 ) {
199 269 return null;
200 270 }
@@ -215,27 +285,31 @@
215 285 * Collect contributors: distinct revision authors (excluding the
216 286 * current author) plus distinct comment authors who have a
217 287 * registered user account.
218 288 *
219 - * @since 0.8.2
220 - *
221 289 * @param WP_Post $post
290 + * @param bool $include_revision_authors Whether to include revision
291 + * authors. Pass false for users who cannot `edit_post`
292 + * the post — revision authorship is edit-level data;
293 + * approved comment authors are public either way.
222 294 * @return array[]
223 295 */
224 -function desktop_mode_content_graph_collect_contributors( WP_Post $post ) {
296 +function openstation_content_graph_collect_contributors( WP_Post $post, $include_revision_authors = true ) {
225 297 $author_id = (int) $post->post_author;
226 298 $ids = array();
227 - $revs = wp_get_post_revisions(
228 - $post->ID,
229 - array(
230 - 'posts_per_page' => 100,
231 - 'fields' => 'ids',
232 - )
233 - );
234 - foreach ( (array) $revs as $rev_id ) {
235 - $rev = get_post( $rev_id );
236 - if ( $rev && (int) $rev->post_author > 0 && (int) $rev->post_author !== $author_id ) {
237 - $ids[ (int) $rev->post_author ] = true;
299 + if ( $include_revision_authors ) {
300 + $revs = wp_get_post_revisions(
301 + $post->ID,
302 + array(
303 + 'posts_per_page' => 100,
304 + 'fields' => 'ids',
305 + )
306 + );
307 + foreach ( (array) $revs as $rev_id ) {
308 + $rev = get_post( $rev_id );
309 + if ( $rev && (int) $rev->post_author > 0 && (int) $rev->post_author !== $author_id ) {
310 + $ids[ (int) $rev->post_author ] = true;
311 + }
238 312 }
239 313 }
240 314 $comment_users = get_comments(
241 315 array(
@@ -251,9 +325,9 @@
251 325 }
252 326 }
253 327 $out = array();
254 328 foreach ( array_keys( $ids ) as $uid ) {
255 - $entry = desktop_mode_content_graph_format_user( $uid );
329 + $entry = openstation_content_graph_format_user( $uid );
256 330 if ( $entry ) {
257 331 $out[] = $entry;
258 332 }
259 333 }
@@ -262,14 +336,12 @@
262 336
263 337 /**
264 338 * Collect approved comments (most recent first, capped at 50).
265 339 *
266 - * @since 0.8.2
267 - *
268 340 * @param WP_Post $post
269 341 * @return array[]
270 342 */
271 -function desktop_mode_content_graph_collect_comments( WP_Post $post ) {
343 +function openstation_content_graph_collect_comments( WP_Post $post ) {
272 344 $comments = get_comments(
273 345 array(
274 346 'post_id' => $post->ID,
275 347 'status' => 'approve',
@@ -277,9 +349,9 @@
277 349 'orderby' => 'comment_date_gmt',
278 350 'order' => 'DESC',
279 351 )
280 352 );
281 - $out = array();
353 + $out = array();
282 354 foreach ( $comments as $comment ) {
283 355 $out[] = array(
284 356 'id' => (int) $comment->comment_ID,
285 357 'author' => (string) $comment->comment_author,
@@ -295,14 +367,12 @@
295 367 /**
296 368 * Collect every taxonomy term attached to the post (categories, tags,
297 369 * and any custom taxonomy registered for the post type).
298 370 *
299 - * @since 0.8.2
300 - *
301 371 * @param WP_Post $post
302 372 * @return array[]
303 373 */
304 -function desktop_mode_content_graph_collect_terms( WP_Post $post ) {
374 +function openstation_content_graph_collect_terms( WP_Post $post ) {
305 375 $taxes = get_object_taxonomies( $post->post_type, 'objects' );
306 376 $out = array();
307 377 foreach ( $taxes as $tax ) {
308 378 if ( ! $tax->public && ! $tax->show_ui ) {
@@ -330,14 +400,12 @@
330 400 /**
331 401 * Collect attached media (anything with this post as its `post_parent`)
332 402 * plus any media referenced from a `wp:image` block. Returns up to 50.
333 403 *
334 - * @since 0.8.2
335 - *
336 404 * @param WP_Post $post
337 405 * @return array[]
338 406 */
339 -function desktop_mode_content_graph_collect_attached_media( WP_Post $post ) {
407 +function openstation_content_graph_collect_attached_media( WP_Post $post ) {
340 408 $attachments = get_attached_media( '', $post );
341 409 $out = array();
342 410 foreach ( $attachments as $att ) {
343 411 $out[] = array(
@@ -356,14 +424,12 @@
356 424
357 425 /**
358 426 * Collect post revisions (most recent first, capped at 30).
359 427 *
360 - * @since 0.8.2
361 - *
362 428 * @param WP_Post $post
363 429 * @return array[]
364 430 */
365 -function desktop_mode_content_graph_collect_revisions( WP_Post $post ) {
431 +function openstation_content_graph_collect_revisions( WP_Post $post ) {
366 432 $revs = wp_get_post_revisions(
367 433 $post->ID,
368 434 array(
369 435 'posts_per_page' => 30,
@@ -373,9 +439,9 @@
373 439 foreach ( $revs as $rev ) {
374 440 $out[] = array(
375 441 'id' => (int) $rev->ID,
376 442 'date' => mysql2date( 'c', $rev->post_date_gmt, false ),
377 - 'author' => desktop_mode_content_graph_format_user( (int) $rev->post_author ),
443 + 'author' => openstation_content_graph_format_user( (int) $rev->post_author ),
378 444 'edit_url' => (string) admin_url( 'revision.php?revision=' . (int) $rev->ID ),
379 445 );
380 446 }
381 447 return $out;