PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
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 0.8.6 All 33 releases
desktop-mode / includes / games / rest.php

rest.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.9, at includes/games/rest.php

574 lines 16.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Games REST routes.
4 *
5 * Namespace `desktop-mode/v1`:
6 *
7 * GET /games/(?P<game>[a-z0-9_\-]+)/scores Leaderboard (paged).
8 * POST /games/(?P<game>[a-z0-9_\-]+)/scores Submit own score.
9 * GET /games/challenges Challenges involving me.
10 * POST /games/challenges Send a challenge.
11 * POST /games/challenges/(?P<id>\d+)/accept Accept (recipient).
12 * POST /games/challenges/(?P<id>\d+)/decline Decline (recipient).
13 * POST /games/challenges/(?P<id>\d+)/complete Report the run (recipient).
14 * GET /games/users/search Opponent-picker autocomplete.
15 * GET /games/playtime My per-game play-time totals.
16 * POST /games/(?P<game>[a-z0-9_\-]+)/playtime Record own play time.
17 *
18 * Permission: every route requires a logged-in user with desktop
19 * mode enabled and the `read` capability (subscribers play games
20 * too). Unknown game ids 404. Challenge routes additionally verify
21 * party membership; sending gates through the
22 * `openstation_games_can_challenge` filter.
23 *
24 * @package OpenStation
25 */
26
27 defined( 'ABSPATH' ) || exit;
28
29 /**
30 * Base permission gate shared by every games route.
31 */
32 function openstation_games_rest_permission() {
33 if ( ! is_user_logged_in() ) {
34 return new WP_Error( 'openstation_games_unauthenticated', __( 'You must be logged in.', 'desktop-mode' ), array( 'status' => 401 ) );
35 }
36 if ( function_exists( 'openstation_is_enabled' ) && ! openstation_is_enabled( get_current_user_id() ) ) {
37 return new WP_Error( 'openstation_games_disabled', __( 'OpenStation is not enabled for this user.', 'desktop-mode' ), array( 'status' => 403 ) );
38 }
39 if ( ! current_user_can( 'read' ) ) {
40 return new WP_Error( 'openstation_games_forbidden', __( 'You cannot use desktop games.', 'desktop-mode' ), array( 'status' => 403 ) );
41 }
42
43 /**
44 * Filters the base games REST permission verdict. Return a
45 * `WP_Error` (or `false`) to lock the whole surface down below
46 * the default logged-in + `read` gate.
47 *
48 * @param true|false|WP_Error $allowed Default `true`.
49 * @param int $user_id Current user.
50 */
51 $allowed = apply_filters( 'openstation_games_rest_permission', true, get_current_user_id() );
52 if ( is_wp_error( $allowed ) ) {
53 return $allowed;
54 }
55 if ( true !== $allowed ) {
56 return new WP_Error( 'openstation_games_forbidden', __( 'You cannot use desktop games.', 'desktop-mode' ), array( 'status' => 403 ) );
57 }
58 return true;
59 }
60
61 /**
62 * Register the routes.
63 */
64 function openstation_games_register_rest_routes() {
65 $ns = 'desktop-mode/v1';
66
67 register_rest_route(
68 $ns,
69 '/games/(?P<game>[a-z0-9_\-]+)/scores',
70 array(
71 array(
72 'methods' => WP_REST_Server::READABLE,
73 'permission_callback' => 'openstation_games_rest_permission',
74 'callback' => 'openstation_games_rest_list_scores',
75 'args' => array(
76 'page' => array(
77 'type' => 'integer',
78 'default' => 1,
79 'minimum' => 1,
80 ),
81 'per_page' => array(
82 'type' => 'integer',
83 'default' => 25,
84 'minimum' => 1,
85 'maximum' => 100,
86 ),
87 'orderby' => array(
88 'type' => 'string',
89 'default' => 'score',
90 'enum' => array( 'score', 'created' ),
91 ),
92 'order' => array(
93 'type' => 'string',
94 'default' => 'desc',
95 'enum' => array( 'asc', 'desc' ),
96 ),
97 'user_id' => array(
98 'type' => 'integer',
99 'default' => 0,
100 ),
101 ),
102 ),
103 array(
104 'methods' => WP_REST_Server::CREATABLE,
105 'permission_callback' => 'openstation_games_rest_permission',
106 'callback' => 'openstation_games_rest_submit_score',
107 'args' => array(
108 'score' => array(
109 'type' => 'integer',
110 'required' => true,
111 'minimum' => 0,
112 ),
113 'meta' => array(
114 'type' => 'object',
115 'default' => array(),
116 ),
117 ),
118 ),
119 )
120 );
121
122 register_rest_route(
123 $ns,
124 '/games/challenges',
125 array(
126 array(
127 'methods' => WP_REST_Server::READABLE,
128 'permission_callback' => 'openstation_games_rest_permission',
129 'callback' => 'openstation_games_rest_list_challenges',
130 'args' => array(
131 'box' => array(
132 'type' => 'string',
133 'default' => 'all',
134 'enum' => array( 'incoming', 'outgoing', 'all' ),
135 ),
136 'state' => array(
137 'type' => 'string',
138 'default' => '',
139 'enum' => array( '', 'pending', 'accepted', 'declined', 'completed' ),
140 ),
141 ),
142 ),
143 array(
144 'methods' => WP_REST_Server::CREATABLE,
145 'permission_callback' => 'openstation_games_rest_permission',
146 'callback' => 'openstation_games_rest_create_challenge',
147 'args' => array(
148 'game' => array(
149 'type' => 'string',
150 'required' => true,
151 ),
152 'recipient_id' => array(
153 'type' => 'integer',
154 'required' => true,
155 ),
156 'score' => array(
157 'type' => 'integer',
158 'required' => true,
159 'minimum' => 0,
160 ),
161 'meta' => array(
162 'type' => 'object',
163 'default' => array(),
164 ),
165 ),
166 ),
167 )
168 );
169
170 register_rest_route(
171 $ns,
172 '/games/challenges/(?P<id>\d+)/accept',
173 array(
174 'methods' => WP_REST_Server::CREATABLE,
175 'permission_callback' => 'openstation_games_rest_permission',
176 'callback' => 'openstation_games_rest_accept_challenge',
177 )
178 );
179
180 register_rest_route(
181 $ns,
182 '/games/challenges/(?P<id>\d+)/decline',
183 array(
184 'methods' => WP_REST_Server::CREATABLE,
185 'permission_callback' => 'openstation_games_rest_permission',
186 'callback' => 'openstation_games_rest_decline_challenge',
187 )
188 );
189
190 register_rest_route(
191 $ns,
192 '/games/challenges/(?P<id>\d+)/complete',
193 array(
194 'methods' => WP_REST_Server::CREATABLE,
195 'permission_callback' => 'openstation_games_rest_permission',
196 'callback' => 'openstation_games_rest_complete_challenge',
197 'args' => array(
198 'score' => array(
199 'type' => 'integer',
200 'required' => true,
201 'minimum' => 0,
202 ),
203 'meta' => array(
204 'type' => 'object',
205 'default' => array(),
206 ),
207 ),
208 )
209 );
210
211 register_rest_route(
212 $ns,
213 '/games/playtime',
214 array(
215 'methods' => WP_REST_Server::READABLE,
216 'permission_callback' => 'openstation_games_rest_permission',
217 'callback' => 'openstation_games_rest_get_playtime',
218 )
219 );
220
221 register_rest_route(
222 $ns,
223 '/games/(?P<game>[a-z0-9_\-]+)/playtime',
224 array(
225 'methods' => WP_REST_Server::CREATABLE,
226 'permission_callback' => 'openstation_games_rest_permission',
227 'callback' => 'openstation_games_rest_record_playtime',
228 'args' => array(
229 'seconds' => array(
230 'type' => 'integer',
231 'required' => true,
232 'minimum' => 1,
233 ),
234 ),
235 )
236 );
237
238 register_rest_route(
239 $ns,
240 '/games/users/search',
241 array(
242 'methods' => WP_REST_Server::READABLE,
243 'permission_callback' => 'openstation_games_rest_permission',
244 'callback' => 'openstation_games_rest_search_users',
245 'args' => array(
246 'q' => array(
247 'type' => 'string',
248 'default' => '',
249 ),
250 'exclude' => array(
251 'type' => 'string',
252 'default' => '',
253 ),
254 ),
255 )
256 );
257 }
258 add_action( 'rest_api_init', 'openstation_games_register_rest_routes' );
259
260 /**
261 * Resolve + validate the `game` path param. 404s unknown ids so the
262 * scores surface doesn't leak which games exist server-side.
263 *
264 * @internal
265 *
266 * @param WP_REST_Request $req Request.
267 * @return string|WP_Error The sanitized game id.
268 */
269 function openstation_games_rest_resolve_game( WP_REST_Request $req ) {
270 $game = sanitize_key( (string) $req->get_param( 'game' ) );
271 if ( '' === $game || ! openstation_games_is_registered( $game ) ) {
272 return new WP_Error(
273 'openstation_unknown_game',
274 __( 'Unknown game.', 'desktop-mode' ),
275 array( 'status' => 404 )
276 );
277 }
278 return $game;
279 }
280
281 /**
282 * GET /games/{game}/scores
283 */
284 function openstation_games_rest_list_scores( WP_REST_Request $req ) {
285 $game = openstation_games_rest_resolve_game( $req );
286 if ( is_wp_error( $game ) ) {
287 return $game;
288 }
289 $result = openstation_games_get_scores(
290 $game,
291 array(
292 'page' => (int) $req->get_param( 'page' ),
293 'per_page' => (int) $req->get_param( 'per_page' ),
294 'orderby' => (string) $req->get_param( 'orderby' ),
295 'order' => (string) $req->get_param( 'order' ),
296 'user_id' => (int) $req->get_param( 'user_id' ),
297 )
298 );
299 return rest_ensure_response(
300 array(
301 'scores' => $result['rows'],
302 'total' => $result['total'],
303 )
304 );
305 }
306
307 /**
308 * POST /games/{game}/scores — always records for the current user;
309 * there is no way to submit a score on someone else's behalf.
310 */
311 function openstation_games_rest_submit_score( WP_REST_Request $req ) {
312 $game = openstation_games_rest_resolve_game( $req );
313 if ( is_wp_error( $game ) ) {
314 return $game;
315 }
316 $id = openstation_games_save_score(
317 $game,
318 get_current_user_id(),
319 (int) $req->get_param( 'score' ),
320 (array) $req->get_param( 'meta' )
321 );
322 if ( is_wp_error( $id ) ) {
323 return $id;
324 }
325 return rest_ensure_response( array( 'id' => $id ) );
326 }
327
328 /**
329 * GET /games/playtime — the current user's `game id => seconds` map.
330 */
331 function openstation_games_rest_get_playtime() {
332 $user_id = get_current_user_id();
333 // Day maps are cast per-game so empty buckets JSON-encode as `{}`.
334 $daily = array();
335 foreach ( openstation_games_get_playtime_daily( $user_id ) as $game => $days ) {
336 $daily[ $game ] = (object) $days;
337 }
338 return rest_ensure_response(
339 array(
340 'playtime' => (object) openstation_games_get_playtime( $user_id ),
341 'daily' => (object) $daily,
342 'today' => openstation_games_playtime_today_key(),
343 )
344 );
345 }
346
347 /**
348 * POST /games/{game}/playtime — always records for the current user;
349 * there is no way to record play time on someone else's behalf.
350 */
351 function openstation_games_rest_record_playtime( WP_REST_Request $req ) {
352 $game = openstation_games_rest_resolve_game( $req );
353 if ( is_wp_error( $game ) ) {
354 return $game;
355 }
356 $total = openstation_games_add_playtime(
357 $game,
358 get_current_user_id(),
359 (int) $req->get_param( 'seconds' )
360 );
361 if ( is_wp_error( $total ) ) {
362 return $total;
363 }
364 return rest_ensure_response( array( 'total' => $total ) );
365 }
366
367 /**
368 * GET /games/challenges — challenges involving the current user.
369 */
370 function openstation_games_rest_list_challenges( WP_REST_Request $req ) {
371 $user_id = get_current_user_id();
372 $box = (string) $req->get_param( 'box' );
373 $state = (string) $req->get_param( 'state' );
374
375 $rows = openstation_games_get_challenges_for_user( $user_id, 0, 100 );
376 $out = array();
377 foreach ( $rows as $row ) {
378 if ( 'incoming' === $box && (int) $row['recipient_id'] !== $user_id ) {
379 continue;
380 }
381 if ( 'outgoing' === $box && (int) $row['challenger_id'] !== $user_id ) {
382 continue;
383 }
384 if ( '' !== $state && $row['state'] !== $state ) {
385 continue;
386 }
387 $out[] = openstation_games_shape_challenge( $row );
388 }
389 // Newest change first for the inbox view.
390 $out = array_reverse( $out );
391 return rest_ensure_response( array( 'challenges' => $out ) );
392 }
393
394 /**
395 * POST /games/challenges
396 */
397 function openstation_games_rest_create_challenge( WP_REST_Request $req ) {
398 $challenger_id = get_current_user_id();
399 $recipient_id = (int) $req->get_param( 'recipient_id' );
400 $game = sanitize_key( (string) $req->get_param( 'game' ) );
401
402 if ( ! openstation_games_is_registered( $game ) ) {
403 return new WP_Error(
404 'openstation_unknown_game',
405 __( 'Unknown game.', 'desktop-mode' ),
406 array( 'status' => 404 )
407 );
408 }
409
410 /**
411 * Filters whether a user may challenge another user. Return
412 * `false` (or a `WP_Error`) to block — e.g. respecting a
413 * do-not-disturb setting or a per-role policy.
414 *
415 * @param bool|WP_Error $allowed Default `true`.
416 * @param int $challenger_id Sender.
417 * @param int $recipient_id Receiver.
418 * @param string $game Game id.
419 */
420 $allowed = apply_filters( 'openstation_games_can_challenge', true, $challenger_id, $recipient_id, $game );
421 if ( is_wp_error( $allowed ) ) {
422 return $allowed;
423 }
424 if ( true !== $allowed ) {
425 return new WP_Error(
426 'openstation_challenge_blocked',
427 __( 'You cannot challenge this user.', 'desktop-mode' ),
428 array( 'status' => 403 )
429 );
430 }
431
432 $id = openstation_games_create_challenge(
433 $game,
434 $challenger_id,
435 $recipient_id,
436 (int) $req->get_param( 'score' ),
437 (array) $req->get_param( 'meta' )
438 );
439 if ( is_wp_error( $id ) ) {
440 return $id;
441 }
442 $row = openstation_games_get_challenge( $id );
443 return rest_ensure_response( array( 'challenge' => openstation_games_shape_challenge( $row ) ) );
444 }
445
446 /**
447 * Load a challenge and verify the current user is its recipient.
448 *
449 * @internal
450 *
451 * @param WP_REST_Request $req Request.
452 * @return array|WP_Error The raw challenge row.
453 */
454 function openstation_games_rest_resolve_recipient_challenge( WP_REST_Request $req ) {
455 $row = openstation_games_get_challenge( (int) $req->get_param( 'id' ) );
456 if ( ! $row ) {
457 return new WP_Error(
458 'openstation_challenge_not_found',
459 __( 'Challenge not found.', 'desktop-mode' ),
460 array( 'status' => 404 )
461 );
462 }
463 if ( get_current_user_id() !== (int) $row['recipient_id'] ) {
464 return new WP_Error(
465 'openstation_challenge_forbidden',
466 __( 'Only the challenged user can act on this challenge.', 'desktop-mode' ),
467 array( 'status' => 403 )
468 );
469 }
470 return $row;
471 }
472
473 /**
474 * POST /games/challenges/{id}/accept
475 */
476 function openstation_games_rest_accept_challenge( WP_REST_Request $req ) {
477 $row = openstation_games_rest_resolve_recipient_challenge( $req );
478 if ( is_wp_error( $row ) ) {
479 return $row;
480 }
481 $result = openstation_games_set_challenge_state( (int) $row['id'], 'accepted' );
482 if ( is_wp_error( $result ) ) {
483 return $result;
484 }
485 $updated = openstation_games_get_challenge( (int) $row['id'] );
486 return rest_ensure_response( array( 'challenge' => openstation_games_shape_challenge( $updated ) ) );
487 }
488
489 /**
490 * POST /games/challenges/{id}/decline
491 */
492 function openstation_games_rest_decline_challenge( WP_REST_Request $req ) {
493 $row = openstation_games_rest_resolve_recipient_challenge( $req );
494 if ( is_wp_error( $row ) ) {
495 return $row;
496 }
497 $result = openstation_games_set_challenge_state( (int) $row['id'], 'declined' );
498 if ( is_wp_error( $result ) ) {
499 return $result;
500 }
501 $updated = openstation_games_get_challenge( (int) $row['id'] );
502 return rest_ensure_response( array( 'challenge' => openstation_games_shape_challenge( $updated ) ) );
503 }
504
505 /**
506 * POST /games/challenges/{id}/complete
507 */
508 function openstation_games_rest_complete_challenge( WP_REST_Request $req ) {
509 $row = openstation_games_rest_resolve_recipient_challenge( $req );
510 if ( is_wp_error( $row ) ) {
511 return $row;
512 }
513 $updated = openstation_games_complete_challenge(
514 (int) $row['id'],
515 (int) $req->get_param( 'score' ),
516 (array) $req->get_param( 'meta' )
517 );
518 if ( is_wp_error( $updated ) ) {
519 return $updated;
520 }
521 return rest_ensure_response( array( 'challenge' => openstation_games_shape_challenge( $updated ) ) );
522 }
523
524 /**
525 * GET /games/users/search?q=<>&exclude=<csv> — autocomplete for the
526 * opponent picker. Thin sibling of the folder-share picker, gated on
527 * `read` instead of `edit_posts` so subscribers can be challenged.
528 */
529 function openstation_games_rest_search_users( WP_REST_Request $req ) {
530 $q = trim( (string) $req->get_param( 'q' ) );
531 $exclude = array_filter( array_map( 'intval', explode( ',', (string) $req->get_param( 'exclude' ) ) ) );
532
533 // Always exclude the current viewer — self-challenges are
534 // rejected at create time anyway.
535 $exclude[] = (int) get_current_user_id();
536 $exclude = array_values( array_unique( array_filter( $exclude ) ) );
537
538 $args = array(
539 'number' => 20,
540 'orderby' => 'display_name',
541 'order' => 'ASC',
542 'exclude' => $exclude,
543 'fields' => 'all',
544 );
545 if ( '' !== $q ) {
546 $args['search'] = '*' . $q . '*';
547 $args['search_columns'] = array( 'user_login', 'user_email', 'display_name', 'user_nicename' );
548 }
549
550 /**
551 * Filter the WP_User_Query args used by the opponent picker.
552 *
553 * @param array $args Default args.
554 * @param array $req Request params (`q`, `exclude`).
555 */
556 $args = (array) apply_filters( 'openstation_games_user_query_args', $args, $req->get_params() );
557
558 $query = new WP_User_Query( $args );
559 $users = $query->get_results();
560 $out = array();
561 foreach ( (array) $users as $user ) {
562 if ( ! user_can( $user, 'read' ) ) {
563 continue;
564 }
565 $out[] = array(
566 'id' => (int) $user->ID,
567 'name' => (string) $user->display_name,
568 'slug' => (string) $user->user_nicename,
569 'avatarUrl' => get_avatar_url( $user->ID, array( 'size' => 48 ) ),
570 );
571 }
572 return rest_ensure_response( array( 'users' => $out ) );
573 }
574