PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.1
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 / rest.php

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

383 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Native Comments Window: REST mutation + helper routes.
4 *
5 * Four endpoints under `desktop-mode/v1`:
6 *
7 * - POST /comments/bulk { ids: int[], action: 'approve'|'unapprove'|'spam'|'unspam'|'trash'|'untrash' }
8 * - POST /comments/reply { parent: int, content: string }
9 * - GET /comments/insights/<email>
10 * - GET /comments/counts
11 *
12 * SECURITY POSTURE
13 * ================
14 *
15 * 1. `permission_callback` — broad cap gate
16 * (`moderate_comments`, `edit_posts`).
17 * 2. Per-target re-validation inside the callback —
18 * `current_user_can( 'edit_comment', $id )` per row.
19 *
20 * @package WPDesktopMode
21 * @since 0.19.0
22 */
23
24 defined( 'ABSPATH' ) || exit;
25
26 /**
27 * Allowed bulk actions, mapped to the function that performs them on a single id.
28 *
29 * Each callback returns true on success, false on a soft failure (the
30 * row is skipped) and throws nothing — the bulk endpoint logs misses
31 * but never aborts the batch on a single bad row.
32 *
33 * @since 0.19.0
34 *
35 * @return array<string,callable>
36 */
37 function desktop_mode_comments_window_bulk_action_map() {
38 return array(
39 'approve' => static function ( $id ) {
40 return false !== wp_set_comment_status( $id, 'approve' );
41 },
42 'unapprove' => static function ( $id ) {
43 return false !== wp_set_comment_status( $id, 'hold' );
44 },
45 'spam' => static function ( $id ) {
46 return false !== wp_spam_comment( $id );
47 },
48 'unspam' => static function ( $id ) {
49 return false !== wp_unspam_comment( $id );
50 },
51 'trash' => static function ( $id ) {
52 return false !== wp_trash_comment( $id );
53 },
54 'untrash' => static function ( $id ) {
55 return false !== wp_untrash_comment( $id );
56 },
57 );
58 }
59
60 /**
61 * Register all routes.
62 *
63 * @since 0.19.0
64 */
65 function desktop_mode_comments_window_register_rest_routes() {
66 register_rest_route(
67 'desktop-mode/v1',
68 '/comments/bulk',
69 array(
70 'methods' => WP_REST_Server::CREATABLE,
71 'callback' => 'desktop_mode_comments_window_rest_bulk',
72 'permission_callback' => static function () {
73 return current_user_can( 'moderate_comments' );
74 },
75 'args' => array(
76 'ids' => array(
77 'required' => true,
78 'type' => 'array',
79 'items' => array( 'type' => 'integer' ),
80 ),
81 'action' => array(
82 'required' => true,
83 'type' => 'string',
84 'enum' => array_keys( desktop_mode_comments_window_bulk_action_map() ),
85 ),
86 ),
87 )
88 );
89
90 register_rest_route(
91 'desktop-mode/v1',
92 '/comments/reply',
93 array(
94 'methods' => WP_REST_Server::CREATABLE,
95 'callback' => 'desktop_mode_comments_window_rest_reply',
96 'permission_callback' => static function () {
97 return current_user_can( 'edit_posts' );
98 },
99 'args' => array(
100 'parent' => array(
101 'required' => true,
102 'type' => 'integer',
103 ),
104 'content' => array(
105 'required' => true,
106 'type' => 'string',
107 ),
108 ),
109 )
110 );
111
112 register_rest_route(
113 'desktop-mode/v1',
114 '/comments/insights/(?P<email>[^/]+)',
115 array(
116 'methods' => WP_REST_Server::READABLE,
117 'callback' => 'desktop_mode_comments_window_rest_insights',
118 'permission_callback' => static function () {
119 return current_user_can( 'moderate_comments' );
120 },
121 'args' => array(
122 'email' => array(
123 'required' => true,
124 'type' => 'string',
125 ),
126 ),
127 )
128 );
129
130 register_rest_route(
131 'desktop-mode/v1',
132 '/comments/counts',
133 array(
134 'methods' => WP_REST_Server::READABLE,
135 'callback' => 'desktop_mode_comments_window_rest_counts',
136 'permission_callback' => static function () {
137 return current_user_can( 'edit_posts' );
138 },
139 )
140 );
141 }
142 add_action( 'rest_api_init', 'desktop_mode_comments_window_register_rest_routes' );
143
144 /**
145 * Bulk moderation handler.
146 *
147 * @since 0.19.0
148 *
149 * @param WP_REST_Request $request Request.
150 * @return WP_REST_Response|WP_Error
151 */
152 function desktop_mode_comments_window_rest_bulk( WP_REST_Request $request ) {
153 $ids = array_values( array_filter( array_map( 'intval', (array) $request['ids'] ) ) );
154 $action = (string) $request['action'];
155 $map = desktop_mode_comments_window_bulk_action_map();
156
157 if ( ! isset( $map[ $action ] ) ) {
158 return new WP_Error(
159 'desktop_mode_comments_invalid_action',
160 __( 'Unknown bulk action.', 'desktop-mode' ),
161 array( 'status' => 400 )
162 );
163 }
164
165 $cb = $map[ $action ];
166 $processed = array();
167 $skipped = array();
168
169 foreach ( $ids as $id ) {
170 if ( ! current_user_can( 'edit_comment', $id ) ) {
171 $skipped[] = $id;
172 continue;
173 }
174 if ( $cb( $id ) ) {
175 $processed[] = $id;
176 } else {
177 $skipped[] = $id;
178 }
179 }
180
181 /**
182 * Fires after a Comments-window bulk action runs.
183 *
184 * @since 0.19.0
185 *
186 * @param string $action Action slug.
187 * @param int[] $processed Ids successfully acted on.
188 * @param int[] $skipped Ids skipped (cap fail or soft error).
189 */
190 do_action(
191 'desktop_mode_comments_window_after_bulk',
192 $action,
193 $processed,
194 $skipped
195 );
196
197 return new WP_REST_Response(
198 array(
199 'action' => $action,
200 'processed' => $processed,
201 'skipped' => $skipped,
202 'counts' => desktop_mode_comments_window_counts(),
203 ),
204 200
205 );
206 }
207
208 /**
209 * Inline-reply handler. Wraps `wp_new_comment` with sane defaults so
210 * the client only needs `{ parent, content }`.
211 *
212 * @since 0.19.0
213 *
214 * @param WP_REST_Request $request Request.
215 * @return WP_REST_Response|WP_Error
216 */
217 function desktop_mode_comments_window_rest_reply( WP_REST_Request $request ) {
218 $parent_id = (int) $request['parent'];
219 $content = (string) $request['content'];
220
221 $parent = get_comment( $parent_id );
222 if ( ! $parent instanceof WP_Comment ) {
223 return new WP_Error(
224 'desktop_mode_comments_no_parent',
225 __( 'Parent comment not found.', 'desktop-mode' ),
226 array( 'status' => 404 )
227 );
228 }
229
230 if ( '' === trim( wp_strip_all_tags( $content ) ) ) {
231 return new WP_Error(
232 'desktop_mode_comments_empty_reply',
233 __( 'Reply cannot be empty.', 'desktop-mode' ),
234 array( 'status' => 400 )
235 );
236 }
237
238 $user = wp_get_current_user();
239 if ( ! $user || ! $user->ID ) {
240 return new WP_Error(
241 'desktop_mode_comments_unauthenticated',
242 __( 'You must be logged in to reply.', 'desktop-mode' ),
243 array( 'status' => 401 )
244 );
245 }
246
247 $comment_data = array(
248 'comment_post_ID' => (int) $parent->comment_post_ID,
249 'comment_parent' => $parent_id,
250 'user_id' => (int) $user->ID,
251 'comment_author' => (string) $user->display_name,
252 'comment_author_email' => (string) $user->user_email,
253 'comment_author_url' => (string) $user->user_url,
254 'comment_content' => $content,
255 'comment_approved' => 1,
256 'comment_type' => 'comment',
257 );
258
259 $new_id = wp_new_comment( wp_slash( $comment_data ), true );
260 if ( is_wp_error( $new_id ) ) {
261 return $new_id;
262 }
263
264 $new = get_comment( $new_id );
265 return new WP_REST_Response(
266 array(
267 'id' => (int) $new_id,
268 'parent' => $parent_id,
269 'content' => $new ? (string) $new->comment_content : $content,
270 'date_gmt' => $new ? (string) $new->comment_date_gmt : '',
271 'author' => $user->display_name,
272 'avatarUrl' => (string) get_avatar_url( (int) $user->ID, array( 'size' => 96 ) ),
273 ),
274 201
275 );
276 }
277
278 /**
279 * Author insights endpoint — drives the side drawer.
280 *
281 * Returns total/approved/pending/spam counts, oldest/newest comment
282 * timestamps, the linked user id (if the email matches a registered
283 * user), and a 0–100 reliability score.
284 *
285 * @since 0.19.0
286 *
287 * @param WP_REST_Request $request Request.
288 * @return WP_REST_Response|WP_Error
289 */
290 function desktop_mode_comments_window_rest_insights( WP_REST_Request $request ) {
291 $email = strtolower( urldecode( (string) $request['email'] ) );
292 if ( '' === $email || ! is_email( $email ) ) {
293 return new WP_Error(
294 'desktop_mode_comments_invalid_email',
295 __( 'Invalid author email.', 'desktop-mode' ),
296 array( 'status' => 400 )
297 );
298 }
299
300 $counts_by_status = array();
301 foreach ( array( 'approve', 'hold', 'spam', 'trash' ) as $status ) {
302 $counts_by_status[ $status ] = (int) get_comments(
303 array(
304 'author_email' => $email,
305 'status' => $status,
306 'count' => true,
307 )
308 );
309 }
310 $total = array_sum( $counts_by_status );
311
312 // Sample the oldest + newest record without loading every row.
313 $oldest = get_comments(
314 array(
315 'author_email' => $email,
316 'status' => 'all',
317 'orderby' => 'comment_date_gmt',
318 'order' => 'ASC',
319 'number' => 1,
320 )
321 );
322 $newest = get_comments(
323 array(
324 'author_email' => $email,
325 'status' => 'all',
326 'orderby' => 'comment_date_gmt',
327 'order' => 'DESC',
328 'number' => 1,
329 )
330 );
331
332 $user = get_user_by( 'email', $email );
333 $reliability = 100;
334 if ( $total > 0 ) {
335 $bad = $counts_by_status['spam'] + $counts_by_status['trash'];
336 $reliability = (int) round( max( 0, min( 100, 100 - ( $bad / $total ) * 100 ) ) );
337 }
338
339 return new WP_REST_Response(
340 array(
341 'email' => $email,
342 'total' => $total,
343 'counts' => $counts_by_status,
344 'oldest' => isset( $oldest[0] ) ? (string) $oldest[0]->comment_date_gmt : null,
345 'newest' => isset( $newest[0] ) ? (string) $newest[0]->comment_date_gmt : null,
346 'userId' => $user ? (int) $user->ID : 0,
347 'userName' => $user ? (string) $user->display_name : '',
348 'reliability' => $reliability,
349 'avatarUrl' => (string) get_avatar_url( $email, array( 'size' => 96 ) ),
350 ),
351 200
352 );
353 }
354
355 /**
356 * Per-status counts. Used by the dock badge + the "N new" pill.
357 *
358 * @since 0.19.0
359 *
360 * @return WP_REST_Response
361 */
362 function desktop_mode_comments_window_rest_counts() {
363 return new WP_REST_Response( desktop_mode_comments_window_counts(), 200 );
364 }
365
366 /**
367 * Internal helper — current comment counts as a flat array.
368 *
369 * @since 0.19.0
370 *
371 * @return array<string,int>
372 */
373 function desktop_mode_comments_window_counts() {
374 $counts = wp_count_comments();
375 return array(
376 'pending' => (int) $counts->moderated,
377 'approved' => (int) $counts->approved,
378 'spam' => (int) $counts->spam,
379 'trash' => (int) $counts->trash,
380 'total' => (int) $counts->total_comments,
381 );
382 }
383