PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 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 All 36 releases
desktop-mode / apps / my-wordpress / parts / dossiers.php

dossiers.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.12, at apps/my-wordpress/parts/dossiers.php

457 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * My WordPress — navigate-into: the detail folder and its sub-lists.
4 *
5 * Part of the `my-wordpress` app: required by `my-wordpress.os.php`,
6 * same namespace, plain `.php` on purpose — only `*.os.php` files are
7 * app entries to the framework loader. This part owns what a post
8 * OPENS INTO: the relation folders with live counts, each relation's
9 * rows, the right-pane dossier behind a selected row (rendered
10 * through WP Explorer's own stats callbacks), the Edit… modal's
11 * choice lists, and the shared preview-action descriptors.
12 *
13 * @package OpenStation
14 */
15
16 namespace OpenStation\Apps\MyWordPress;
17
18 use OpenStation\App\Os;
19
20 // Direct access, unless a standalone host is booting on bare PHP.
21 if ( ! defined( 'ABSPATH' ) ) {
22 defined( 'OPENSTATION_STANDALONE' ) || exit;
23 }
24
25 /**
26 * The ids "Attached media" lists and counts: the post's attachments
27 * plus its featured image, each once.
28 *
29 * @param \WP_Post $post Post.
30 * @return int[]
31 */
32 function media_ids( \WP_Post $post ) {
33 $ids = get_children(
34 array(
35 'post_parent' => $post->ID,
36 'post_type' => 'attachment',
37 'fields' => 'ids',
38 )
39 );
40 $ids = array_map( 'intval', $ids );
41 if ( has_post_thumbnail( $post ) ) {
42 $ids[] = (int) get_post_thumbnail_id( $post );
43 }
44 return array_unique( $ids );
45 }
46
47 /**
48 * The detail FOLDER a post navigates into: the rendered article plus
49 * one folder tile per related surface — Author, Contributors,
50 * Comments, Categories, Tags, Attached media, Revisions — with live
51 * counts. WP Explorer's detail view, as data.
52 *
53 * @param Os $os Host handle.
54 * @param array<string,mixed> $section Section descriptor.
55 * @param int $id Post id.
56 * @return array<string,mixed>|null Null when the post vanished.
57 */
58 function folder( Os $os, array $section, $id ) {
59 $post = get_post( $id );
60 if ( ! $post || $post->post_type !== $section['post_type'] ) {
61 return null;
62 }
63
64 $contributors = function_exists( 'openstation_my_wordpress_post_contributors_payload' )
65 ? openstation_my_wordpress_post_contributors_payload( $id )
66 : array();
67 $categories = get_the_terms( $post, 'category' );
68 $categories = is_array( $categories ) ? $categories : array();
69 $tags = get_the_terms( $post, 'post_tag' );
70 $tags = is_array( $tags ) ? $tags : array();
71 $media_count = count( media_ids( $post ) );
72 $comments = (int) get_comments_number( $post );
73 $revisions = wp_revisions_enabled( $post ) ? count( wp_get_post_revisions( $id, array( 'fields' => 'ids' ) ) ) : 0;
74
75 $folders = array();
76 $folders[] = array(
77 'relation' => 'author',
78 'label' => __( 'Author', 'desktop-mode' ),
79 'icon' => 'dashicons-admin-users',
80 'count' => 1,
81 );
82 if ( array() !== $contributors ) {
83 $folders[] = array(
84 'relation' => 'contributors',
85 'label' => __( 'Contributors', 'desktop-mode' ),
86 'icon' => 'dashicons-groups',
87 'count' => count( $contributors ),
88 );
89 }
90 $folders[] = array(
91 'relation' => 'comments',
92 'label' => __( 'Comments', 'desktop-mode' ),
93 'icon' => 'dashicons-admin-comments',
94 'count' => $comments,
95 'disabled' => 0 === $comments && 'closed' === $post->comment_status,
96 );
97 if ( array() !== $categories ) {
98 $folders[] = array(
99 'relation' => 'categories',
100 'label' => __( 'Categories', 'desktop-mode' ),
101 'icon' => 'dashicons-category',
102 'count' => count( $categories ),
103 );
104 }
105 if ( array() !== $tags ) {
106 $folders[] = array(
107 'relation' => 'tags',
108 'label' => __( 'Tags', 'desktop-mode' ),
109 'icon' => 'dashicons-tag',
110 'count' => count( $tags ),
111 );
112 }
113 $folders[] = array(
114 'relation' => 'media',
115 'label' => __( 'Attached media', 'desktop-mode' ),
116 'icon' => 'dashicons-format-image',
117 'count' => $media_count,
118 );
119 $folders[] = array(
120 'relation' => 'revisions',
121 'label' => __( 'Revisions', 'desktop-mode' ),
122 'icon' => 'dashicons-backup',
123 'count' => $revisions,
124 );
125
126 return array(
127 'id' => $id,
128 'title' => '' !== $post->post_title ? openstation_plain_text_title( $post->post_title ) : __( '(no title)', 'desktop-mode' ),
129 'status' => (string) $post->post_status,
130 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying Core's own content pipeline, not declaring a hook.
131 'content' => (string) apply_filters( 'the_content', (string) $post->post_content ),
132 'folders' => $folders,
133 );
134 }
135
136 /**
137 * The rows inside one relation sub-folder. Uniform shape: `id`,
138 * `title`, `subtitle`, `icon` | `thumb`, `editUrl`.
139 *
140 * @param Os $os Host handle.
141 * @param array<string,mixed> $section Section descriptor.
142 * @param int $id Post id.
143 * @param string $relation Relation slug.
144 * @return array{label:string,rows:array[]}|null
145 */
146 function sub( Os $os, array $section, $id, $relation ) {
147 $post = get_post( $id );
148 if ( ! $post || $post->post_type !== $section['post_type'] ) {
149 return null;
150 }
151 $rows = array();
152 $label = '';
153
154 $user_row = static function ( $user_id, $name = '', $avatar = '' ) {
155 $user = get_userdata( (int) $user_id );
156 return array(
157 'id' => (int) $user_id,
158 'title' => openstation_plain_text_title( '' !== $name ? $name : ( $user ? (string) $user->display_name : sprintf( '#%d', $user_id ) ) ),
159 'subtitle' => $user ? (string) $user->user_email : '',
160 'thumb' => '' !== $avatar ? $avatar : (string) get_avatar_url( (int) $user_id, array( 'size' => 96 ) ),
161 'editUrl' => current_user_can( 'edit_user', (int) $user_id )
162 ? admin_url( 'user-edit.php?user_id=' . (int) $user_id )
163 : '',
164 );
165 };
166
167 switch ( $relation ) {
168 case 'author':
169 $label = __( 'Author', 'desktop-mode' );
170 $rows[] = $user_row( (int) $post->post_author );
171 break;
172 case 'contributors':
173 $label = __( 'Contributors', 'desktop-mode' );
174 if ( function_exists( 'openstation_my_wordpress_post_contributors_payload' ) ) {
175 foreach ( openstation_my_wordpress_post_contributors_payload( $id ) as $person ) {
176 $rows[] = $user_row( $person['userId'], $person['userName'], $person['userAvatarUrl'] );
177 }
178 }
179 break;
180 case 'comments':
181 $label = __( 'Comments', 'desktop-mode' );
182 foreach ( get_comments(
183 array(
184 'post_id' => $id,
185 'number' => 100,
186 )
187 ) as $comment ) {
188 $rows[] = array(
189 'id' => (int) $comment->comment_ID,
190 'title' => openstation_plain_text_title( $comment->comment_author ),
191 'subtitle' => openstation_plain_text_title( wp_trim_words( openstation_strip_all_tags( (string) $comment->comment_content ), 12 ) ),
192 'icon' => 'dashicons-admin-comments',
193 'editUrl' => current_user_can( 'edit_comment', (int) $comment->comment_ID )
194 ? admin_url( 'comment.php?action=editcomment&c=' . (int) $comment->comment_ID )
195 : '',
196 );
197 }
198 break;
199 case 'categories':
200 case 'tags':
201 $taxonomy = 'tags' === $relation ? 'post_tag' : 'category';
202 $label = 'tags' === $relation ? __( 'Tags', 'desktop-mode' ) : __( 'Categories', 'desktop-mode' );
203 $terms = get_the_terms( $post, $taxonomy );
204 foreach ( is_array( $terms ) ? $terms : array() as $term ) {
205 $rows[] = array(
206 'id' => (int) $term->term_id,
207 'title' => openstation_plain_text_title( $term->name ),
208 'subtitle' => sprintf(
209 /* translators: %s: entry count. */
210 _n( '%s entry', '%s entries', (int) $term->count, 'desktop-mode' ),
211 number_format_i18n( (int) $term->count )
212 ),
213 'icon' => 'tags' === $relation ? 'dashicons-tag' : 'dashicons-category',
214 'editUrl' => current_user_can( 'manage_categories' )
215 ? admin_url( 'term.php?taxonomy=' . $taxonomy . '&tag_ID=' . (int) $term->term_id )
216 : '',
217 );
218 }
219 break;
220 case 'media':
221 $label = __( 'Attached media', 'desktop-mode' );
222 foreach ( media_ids( $post ) as $media_id ) {
223 $media = get_post( $media_id );
224 if ( ! $media ) {
225 continue;
226 }
227 $rows[] = array(
228 'id' => $media_id,
229 'title' => '' !== $media->post_title ? openstation_plain_text_title( $media->post_title ) : sprintf( '#%d', $media_id ),
230 'subtitle' => (string) $media->post_mime_type,
231 'thumb' => (string) wp_get_attachment_image_url( $media_id, 'medium' ),
232 'icon' => 'dashicons-format-image',
233 'editUrl' => current_user_can( 'edit_post', $media_id )
234 ? admin_url( 'post.php?post=' . $media_id . '&action=edit' )
235 : '',
236 );
237 }
238 break;
239 case 'revisions':
240 $label = __( 'Revisions', 'desktop-mode' );
241 foreach ( wp_get_post_revisions( $id ) as $revision ) {
242 $rows[] = array(
243 'id' => (int) $revision->ID,
244 'title' => openstation_plain_text_title( wp_post_revision_title_expanded( $revision, false ) ), // Core leads with an avatar <img>; the client prints a title as text.
245 'subtitle' => openstation_plain_text_title( get_the_author_meta( 'display_name', (int) $revision->post_author ) ),
246 'icon' => 'dashicons-backup',
247 'editUrl' => current_user_can( 'edit_post', $id )
248 ? admin_url( 'revision.php?revision=' . (int) $revision->ID )
249 : '',
250 );
251 }
252 break;
253 default:
254 return null;
255 }
256
257 return array(
258 'label' => $label,
259 'rows' => $rows,
260 );
261 }
262
263 /**
264 * Invoke one of WP Explorer's stats REST callbacks in-process, with a
265 * synthetic request — the panes render the SAME payloads WP Explorer
266 * renders, filters (`openstation_my_wordpress_term_stats` and
267 * friends) included.
268 *
269 * @param string $callback Function name.
270 * @param array<string,mixed> $params Request params.
271 * @return array<string,mixed>|null Null when unavailable or refused.
272 */
273 function stats_payload( $callback, array $params ) {
274 if ( ! function_exists( $callback ) || ! class_exists( '\WP_REST_Request' ) ) {
275 return null;
276 }
277 $request = new \WP_REST_Request();
278 foreach ( $params as $key => $value ) {
279 $request->set_param( $key, $value );
280 }
281 $payload = call_user_func( $callback, $request );
282 return is_array( $payload ) ? $payload : null;
283 }
284
285 /**
286 * The right-pane dossier for one SELECTED sub-list row, per relation:
287 * the term-stats card for a category or tag (stat tiles, 12-month
288 * activity, first/last post, recent posts), the user dossier + stats
289 * for author/contributors, the comment dossier, the media dossier
290 * with its usage scan, a revision preview.
291 *
292 * @param Os $os Host handle.
293 * @param array<string,mixed> $section Section descriptor.
294 * @param int $post_id Post navigated into.
295 * @param string $relation Relation slug.
296 * @param int $row_id Selected row id.
297 * @return array<string,mixed>|null
298 */
299 function sub_detail( Os $os, array $section, $post_id, $relation, $row_id ) {
300 switch ( $relation ) {
301 case 'categories':
302 case 'tags':
303 $stats = stats_payload(
304 'openstation_my_wordpress_term_stats_callback',
305 array(
306 'taxonomy' => 'tags' === $relation ? 'post_tag' : 'category',
307 'id' => $row_id,
308 )
309 );
310 return $stats ? array(
311 'kind' => 'term',
312 'stats' => $stats,
313 ) : null;
314
315 case 'author':
316 case 'contributors':
317 $user_section = array(
318 'kind' => 'user',
319 'post_type' => '',
320 );
321 $dossier = detail( $os, $user_section, $row_id );
322 if ( ! $dossier ) {
323 return null;
324 }
325 return array(
326 'kind' => 'user',
327 'detail' => $dossier,
328 'stats' => stats_payload( 'openstation_my_wordpress_user_stats_callback', array( 'id' => $row_id ) ),
329 );
330
331 case 'comments':
332 $stats = stats_payload( 'openstation_my_wordpress_comment_stats_callback', array( 'id' => $row_id ) );
333 return $stats ? array(
334 'kind' => 'comment',
335 'stats' => $stats,
336 ) : null;
337
338 case 'media':
339 $media_section = array(
340 'kind' => 'media',
341 'post_type' => 'attachment',
342 'thumbnails' => true,
343 );
344 $dossier = detail( $os, $media_section, $row_id );
345 return $dossier ? array(
346 'kind' => 'media',
347 'detail' => $dossier,
348 ) : null;
349
350 case 'revisions':
351 $revision = wp_get_post_revision( $row_id );
352 if ( ! $revision || (int) $revision->post_parent !== (int) $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
353 return null;
354 }
355 return array(
356 'kind' => 'revision',
357 'title' => openstation_plain_text_title( wp_post_revision_title_expanded( $revision, false ) ), // Same as the row title: no avatar <img>.
358 'author' => openstation_plain_text_title( get_the_author_meta( 'display_name', (int) $revision->post_author ) ),
359 'date' => (string) get_the_date( '', $revision ) . ' ' . get_the_time( '', $revision ),
360 'content' => wp_kses_post( (string) apply_filters( 'the_content', (string) $revision->post_content ) ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core's own content pipeline.
361 );
362 }
363 return null;
364 }
365
366 /**
367 * The choices the Edit… modal offers: the site's authors, the
368 * category tree (in `<os-category-picker>`'s item shape, `parent`
369 * included) and the tag terms the token field suggests from. Only
370 * computed while a post-kind section is open.
371 *
372 * @return array{authors:array[],categories:array[],tags:array[]}
373 */
374 function edit_choices() {
375 $authors = array();
376 foreach ( get_users(
377 array(
378 'capability' => array( 'edit_posts' ),
379 'number' => 100,
380 'orderby' => 'display_name',
381 'fields' => array( 'ID', 'display_name' ),
382 )
383 ) as $user ) {
384 $authors[] = array(
385 'id' => (int) $user->ID,
386 'name' => openstation_plain_text_title( $user->display_name ),
387 );
388 }
389 $categories = array();
390 foreach ( get_terms(
391 array(
392 'taxonomy' => 'category',
393 'hide_empty' => false,
394 'number' => 100,
395 )
396 ) as $term ) {
397 if ( $term instanceof \WP_Term ) {
398 $categories[] = array(
399 'id' => (int) $term->term_id,
400 'name' => openstation_plain_text_title( $term->name ),
401 'parent' => (int) $term->parent,
402 );
403 }
404 }
405 $tags = array();
406 foreach ( get_terms(
407 array(
408 'taxonomy' => 'post_tag',
409 'hide_empty' => false,
410 'number' => 100,
411 )
412 ) as $term ) {
413 if ( $term instanceof \WP_Term ) {
414 $tags[] = array(
415 'id' => (int) $term->term_id,
416 'name' => openstation_plain_text_title( $term->name ),
417 );
418 }
419 }
420 return array(
421 'authors' => $authors,
422 'categories' => $categories,
423 'tags' => $tags,
424 );
425 }
426
427 /**
428 * The preview-action descriptors the acting user may see — the same
429 * `openstation_my_wordpress_preview_actions` pipeline WP Explorer
430 * collects, minus the fields the client does not need.
431 *
432 * @param Os $os Host handle.
433 * @return array<int,array<string,mixed>>
434 */
435 function preview_actions( Os $os ) {
436 if ( ! function_exists( 'openstation_my_wordpress_collect_preview_actions' ) ) {
437 return array();
438 }
439 $out = array();
440 foreach ( (array) openstation_my_wordpress_collect_preview_actions() as $action ) {
441 if ( ! is_array( $action ) || empty( $action['id'] ) ) {
442 continue;
443 }
444 if ( ! empty( $action['capability'] ) && ! $os->can( (string) $action['capability'] ) ) {
445 continue;
446 }
447 $out[] = array(
448 'id' => (string) $action['id'],
449 'label' => (string) ( $action['label'] ?? $action['id'] ),
450 'icon' => (string) ( $action['icon'] ?? '' ),
451 'sections' => array_map( 'strval', (array) ( $action['sections'] ?? array() ) ),
452 'mime' => (string) ( $action['mime'] ?? '' ),
453 );
454 }
455 return $out;
456 }
457