PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.3
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.3
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 / includes / comments-window / window.php

window.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.3, at includes/comments-window/window.php

427 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Native Comments Window: registration, template, REST fields.
4 *
5 * Mirrors the structure of the Users/Posts windows adapted for the
6 * comment collection — `/wp/v2/comments` rather than `/wp/v2/posts`.
7 * The surface is a two-pane conversation view: a Pending/All/Spam/
8 * Trash/Mine tab set over a rail of conversations, and the selected
9 * thread's full nested reply chain with a docked composer.
10 *
11 * @package OpenStation
12 */
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Echoes the native Comments window's template body.
18 */
19 function openstation_comments_window_render_template() {
20 ob_start();
21 ?>
22 <div class="os-comments os-comments--conversation" data-os-comments-root>
23 <?php
24 /*
25 * Tab strip filters the thread rail (Pending / All / Spam /
26 * Trash / Mine). `<os-tabs>` owns roving `tabindex` and the
27 * `aria-selected` mirror; the bundle only listens for
28 * `os-tab-change`. There are no `<os-tabpanel>` siblings —
29 * one pane is repainted in place rather than swapped.
30 */
31 ?>
32 <os-tabs class="os-comments__tabrow" value="pending"
33 label="<?php esc_attr_e( 'Comment status', 'desktop-mode' ); ?>"
34 data-os-comments-tabs>
35 <os-tab value="pending"><?php esc_html_e( 'Pending', 'desktop-mode' ); ?></os-tab>
36 <os-tab value="all"><?php esc_html_e( 'All', 'desktop-mode' ); ?></os-tab>
37 <os-tab value="spam"><?php esc_html_e( 'Spam', 'desktop-mode' ); ?></os-tab>
38 <os-tab value="trash"><?php esc_html_e( 'Trash', 'desktop-mode' ); ?></os-tab>
39 <os-tab value="mine"><?php esc_html_e( 'Mine', 'desktop-mode' ); ?></os-tab>
40 </os-tabs>
41
42 <div class="os-comments__split">
43 <?php /* Left rail: search + list of conversations (top-level comments). */ ?>
44 <aside class="os-comments__rail" aria-label="<?php esc_attr_e( 'Conversations', 'desktop-mode' ); ?>">
45 <div class="os-comments__search">
46 <os-text-field data-os-comments-search
47 placeholder="<?php esc_attr_e( 'Search comments…', 'desktop-mode' ); ?>"></os-text-field>
48 </div>
49 <div class="os-comments__list" role="list"
50 aria-label="<?php esc_attr_e( 'Conversations', 'desktop-mode' ); ?>"
51 data-os-comments-list></div>
52 </aside>
53
54 <?php
55 /*
56 * Right pane: the selected conversation thread + composer.
57 * Deliberately NOT a live region — the whole pane is
58 * replaced on every selection and after every moderation
59 * action, so announcing it would read the entire thread
60 * aloud each time. The small status node below carries the
61 * announcements instead.
62 */
63 ?>
64 <section class="os-comments__convo" data-os-comments-convo></section>
65 </div>
66
67 <?php /* Single polite live region for action results ("Approved", "Reply sent"). */ ?>
68 <div class="os-comments__status screen-reader-text" role="status" aria-live="polite"
69 data-os-comments-status></div>
70 </div>
71 <?php
72 $html = (string) ob_get_clean();
73
74 /**
75 * Filter the native Comments window's template HTML.
76 *
77 * @param string $html Default template HTML.
78 */
79 $filtered = (string) apply_filters( 'openstation_comments_window_template_html', $html );
80
81 if ( function_exists( 'openstation_kses_native_window_template' ) ) {
82 echo openstation_kses_native_window_template( $filtered ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper kses-escapes.
83 } else {
84 echo wp_kses( $filtered, wp_kses_allowed_html( 'post' ) );
85 }
86 }
87
88 /**
89 * Register the native Comments window on `init` (priority 20).
90 */
91 function openstation_comments_window_register_window() {
92 if ( ! openstation_comments_window_user_can_register() ) {
93 return;
94 }
95
96 $viewer_id = (int) get_current_user_id();
97
98 $window_args = array(
99 'title' => __( 'Comments', 'desktop-mode' ),
100 'icon' => 'dashicons-admin-comments',
101 'template' => 'openstation_comments_window_render_template',
102 'script' => 'os-comments-window',
103 'styles' => array( 'os-comments-window' ),
104 'width' => 1180,
105 'height' => 760,
106 'min_width' => 760,
107 'min_height' => 480,
108 'placement' => 'none',
109 'config' => array(
110 'mode' => 'comments',
111 'restRoot' => esc_url_raw( rest_url() ),
112 'restNonce' => wp_create_nonce( 'wp_rest' ),
113 'commentsUrl' => esc_url_raw( rest_url( 'wp/v2/comments' ) ),
114 'currentUserId' => $viewer_id,
115 'defaultPerPage' => 20,
116 'queryArgs' => openstation_comments_window_default_query_args(),
117
118 // Capability flags surfaced to the JS — UI hides actions
119 // the viewer can't perform. Server still re-checks every
120 // mutation, so a tampered flag here changes nothing
121 // security-wise.
122 'canModerate' => current_user_can( 'moderate_comments' ),
123 'canEditComments' => current_user_can( 'edit_posts' ),
124
125 // Bulk + helper REST routes — the JS bundle reads these so
126 // a rename or namespace move stays in one place.
127 'bulkUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/bulk' ) ),
128 'replyUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/reply' ) ),
129 'insightsUrlBase' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/insights/' ) ),
130 'countsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/counts' ) ),
131 'aiSettingsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/ai-settings' ) ),
132 // Surface the current state so the OS Settings UI can
133 // branch on it without a separate fetch.
134 // Cap-gated mirror of what the REST endpoint would return.
135 'aiModeration' => array(
136 'enabled' => openstation_comments_ai_is_enabled(),
137 'providerConfigured' => openstation_comments_ai_provider_configured(),
138 'canManage' => current_user_can( 'manage_options' ),
139 ),
140
141 'replyEditor' => (string) apply_filters(
142 /**
143 * Filter the reply editor implementation the bundle should mount.
144 *
145 * - `'rich'` — built-in contenteditable rich editor (default).
146 * - `'gutenberg'` — full @wordpress/block-editor (follow-up surface).
147 * - `'plain'` — plain textarea, no toolbar.
148 *
149 * @param string $editor Editor flavor slug.
150 * @param int $viewer_id Current user id.
151 */
152 'openstation_comments_window_reply_editor',
153 'rich',
154 $viewer_id
155 ),
156 ),
157 );
158
159 /**
160 * Filter the args used to register the native Comments window.
161 *
162 * @param array $window_args Args passed to `openstation_register_window()`.
163 */
164 $window_args = (array) apply_filters( 'openstation_comments_window_args', $window_args );
165
166 $registered = openstation_register_window( 'desktop-mode-comments', $window_args );
167 if ( is_wp_error( $registered ) ) {
168 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
169 error_log( '[openstation] Native Comments window registration failed: ' . $registered->get_error_message() );
170 }
171 }
172 add_action( 'init', 'openstation_comments_window_register_window', 20 );
173
174 /**
175 * Default REST query args for the Comments window.
176 *
177 * @return array
178 */
179 function openstation_comments_window_default_query_args() {
180 // `context=edit` on `wp/v2/comments` requires `moderate_comments`
181 // — sending it as an author-without-moderate-cap 401s the entire
182 // list. Stick to `view` (every authenticated user can read it) and
183 // fall back to the REST `edit` context per-row only when the user
184 // opens the inline-edit affordance on a row they're allowed to
185 // edit.
186 $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
187 $args = array(
188
189 /*
190 * Exactly the fields the conversation view renders — no more.
191 * Every `openstation_*` field is a computed REST field, and
192 * `openstation_replies_count` runs its own `get_comments()`
193 * COUNT per row, so an over-broad `_fields` is a per-row query
194 * multiplier. The scoring fields (`spam_score`, `link_count`,
195 * `akismet`, `ai_verdict`) are NOT requested here: nothing in
196 * the conversation UI reads them, and computing them costs a
197 * meta read (or worse) per row. Re-add them via the filter
198 * below if a plugin surfaces them.
199 */
200 '_fields' =>
201 'id,post,parent,author,author_name,author_avatar_urls,'
202 . 'date_gmt,content,status,'
203 . 'openstation_post_title,openstation_post_link,'
204 . 'openstation_can_edit,openstation_can_moderate,'
205 . 'openstation_replies_count',
206 'context' => $context,
207 'per_page' => 20,
208 // 'hold' = pending. Use the wp/v2 status names where they differ
209 // from core's: 'hold' / 'approve' / 'spam' / 'trash'.
210 'status' => 'hold',
211 );
212
213 /**
214 * Filter the default outbound REST query args for the Comments window.
215 *
216 * @param array $args Default args.
217 */
218 return (array) apply_filters( 'openstation_comments_window_query_args', $args );
219 }
220
221 /**
222 * Register the Comments-window REST fields on the `comment` resource.
223 *
224 * Fields are computed lazily — none of them runs unless the bundle
225 * explicitly requests them via `_fields`.
226 */
227 function openstation_comments_window_register_rest_fields() {
228 register_rest_field(
229 'comment',
230 'openstation_post_title',
231 array(
232 'get_callback' => static function ( $row ) {
233 $post_id = isset( $row['post'] ) ? (int) $row['post'] : 0;
234 if ( $post_id <= 0 ) {
235 return '';
236 }
237 $post = get_post( $post_id );
238 return $post ? (string) get_the_title( $post ) : '';
239 },
240 'schema' => array(
241 'description' => __( 'Title of the post the comment is attached to.', 'desktop-mode' ),
242 'type' => 'string',
243 'context' => array( 'view', 'edit' ),
244 'readonly' => true,
245 ),
246 )
247 );
248
249 register_rest_field(
250 'comment',
251 'openstation_post_link',
252 array(
253 'get_callback' => static function ( $row ) {
254 $post_id = isset( $row['post'] ) ? (int) $row['post'] : 0;
255 if ( $post_id <= 0 ) {
256 return '';
257 }
258 return (string) get_permalink( $post_id );
259 },
260 'schema' => array(
261 'description' => __( 'Permalink of the post the comment is attached to.', 'desktop-mode' ),
262 'type' => 'string',
263 'format' => 'uri',
264 'context' => array( 'view', 'edit' ),
265 'readonly' => true,
266 ),
267 )
268 );
269
270 register_rest_field(
271 'comment',
272 'openstation_spam_score',
273 array(
274 'get_callback' => static function ( $row ) {
275 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
276 if ( $id <= 0 ) {
277 return 0;
278 }
279 return (int) openstation_comments_window_spam_score( $id );
280 },
281 'schema' => array(
282 'description' => __( 'Spam confidence score, 0–100.', 'desktop-mode' ),
283 'type' => 'integer',
284 'minimum' => 0,
285 'maximum' => 100,
286 'context' => array( 'view', 'edit' ),
287 'readonly' => true,
288 ),
289 )
290 );
291
292 register_rest_field(
293 'comment',
294 'openstation_link_count',
295 array(
296 'get_callback' => static function ( $row ) {
297 $content = isset( $row['content']['raw'] )
298 ? (string) $row['content']['raw']
299 : ( isset( $row['content']['rendered'] ) ? (string) $row['content']['rendered'] : '' );
300 return (int) preg_match_all( '#https?://#i', $content );
301 },
302 'schema' => array(
303 'description' => __( 'Number of links in the comment.', 'desktop-mode' ),
304 'type' => 'integer',
305 'minimum' => 0,
306 'context' => array( 'view', 'edit' ),
307 'readonly' => true,
308 ),
309 )
310 );
311
312 register_rest_field(
313 'comment',
314 'openstation_can_edit',
315 array(
316 'get_callback' => static function ( $row ) {
317 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
318 return $id > 0 && current_user_can( 'edit_comment', $id );
319 },
320 'schema' => array(
321 'description' => __( 'Whether the requester can edit this comment.', 'desktop-mode' ),
322 'type' => 'boolean',
323 'context' => array( 'view', 'edit' ),
324 'readonly' => true,
325 ),
326 )
327 );
328
329 register_rest_field(
330 'comment',
331 'openstation_can_moderate',
332 array(
333 'get_callback' => static function () {
334 return current_user_can( 'moderate_comments' );
335 },
336 'schema' => array(
337 'description' => __( 'Whether the requester can moderate comments globally.', 'desktop-mode' ),
338 'type' => 'boolean',
339 'context' => array( 'view', 'edit' ),
340 'readonly' => true,
341 ),
342 )
343 );
344
345 register_rest_field(
346 'comment',
347 'openstation_replies_count',
348 array(
349 'get_callback' => static function ( $row ) {
350 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
351 if ( $id <= 0 ) {
352 return 0;
353 }
354 return (int) get_comments(
355 array(
356 'parent' => $id,
357 'count' => true,
358 'status' => 'all',
359 )
360 );
361 },
362 'schema' => array(
363 'description' => __( 'Direct-reply count for this comment.', 'desktop-mode' ),
364 'type' => 'integer',
365 'minimum' => 0,
366 'context' => array( 'view', 'edit' ),
367 'readonly' => true,
368 ),
369 )
370 );
371
372 register_rest_field(
373 'comment',
374 'openstation_ai_verdict',
375 array(
376 'get_callback' => static function ( $row ) {
377 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
378 if ( $id <= 0 || ! function_exists( 'openstation_ai_get_meta' ) ) {
379 return null;
380 }
381 $meta = openstation_ai_get_meta( 'comment', $id );
382 if ( ! is_array( $meta ) ) {
383 return null;
384 }
385 return array(
386 'spam' => ! empty( $meta['spam'] ),
387 'harmful' => ! empty( $meta['harmful'] ),
388 'topic' => isset( $meta['topic'] ) ? (string) $meta['topic'] : '',
389 'summary' => isset( $meta['ai_summary'] ) ? (string) $meta['ai_summary'] : '',
390 'analyzedAt' => isset( $meta['analyzed_at'] ) ? (int) $meta['analyzed_at'] : 0,
391 );
392 },
393 'schema' => array(
394 'description' => __( 'AI moderation verdict for this comment, when analyzed (null otherwise).', 'desktop-mode' ),
395 'type' => array( 'object', 'null' ),
396 'context' => array( 'view', 'edit' ),
397 'readonly' => true,
398 ),
399 )
400 );
401
402 register_rest_field(
403 'comment',
404 'openstation_akismet',
405 array(
406 'get_callback' => static function ( $row ) {
407 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
408 if ( $id <= 0 ) {
409 return null;
410 }
411 $result = (string) get_comment_meta( $id, 'akismet_result', true );
412 if ( '' === $result ) {
413 return null;
414 }
415 return $result; // 'true' | 'false' | 'pending'.
416 },
417 'schema' => array(
418 'description' => __( 'Akismet verdict if the plugin is installed (null otherwise).', 'desktop-mode' ),
419 'type' => array( 'string', 'null' ),
420 'context' => array( 'view', 'edit' ),
421 'readonly' => true,
422 ),
423 )
424 );
425 }
426 add_action( 'rest_api_init', 'openstation_comments_window_register_rest_fields' );
427