PluginProbe
Solid Post Likes / trunk
Solid Post Likes vtrunk
trunk 1.0.8 1.1.0 1.2.0
solid-post-likes / controllers / SolidPostLikesRest.php

SolidPostLikesRest.php in Solid Post Likes trunk, at controllers/SolidPostLikesRest.php

148 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace OACS\SolidPostLikes\Controllers;
3
4 use OACS\SolidPostLikes\Controllers\SolidPostLikesChecker as Checker;
5 use OACS\SolidPostLikes\Controllers\SolidPostLikesStore as Store;
6
7 if ( ! defined( 'WPINC' ) ) { die; }
8
9 /**
10 * REST API (since 1.2.0).
11 *
12 * GET /wp-json/spl/v1/likes/<id> -> { id, type, count, liked, nonce }
13 * POST /wp-json/spl/v1/likes/<id>/toggle -> { status, count, liked }
14 *
15 * Reading is public (counts already render on every page). Toggling requires
16 * either an authenticated request (cookie + X-WP-Nonce, or an Application
17 * Password for headless/mobile use) or the plugin nonce from the GET response.
18 */
19 class SolidPostLikesRest
20 {
21 public function register_routes()
22 {
23 $id_args = array(
24 'id' => array(
25 'validate_callback' => function ( $value ) {
26 return is_numeric( $value ) && (int) $value > 0;
27 },
28 ),
29 'type' => array(
30 'type' => 'string',
31 'default' => 'post',
32 'enum' => array( 'post', 'comment' ),
33 ),
34 );
35
36 register_rest_route( 'spl/v1', '/likes/(?P<id>\d+)', array(
37 'methods' => 'GET',
38 'callback' => array( $this, 'get_item' ),
39 'permission_callback' => '__return_true',
40 'args' => $id_args,
41 ) );
42
43 register_rest_route( 'spl/v1', '/likes/(?P<id>\d+)/toggle', array(
44 'methods' => 'POST',
45 'callback' => array( $this, 'toggle_item' ),
46 'permission_callback' => array( $this, 'can_toggle' ),
47 'args' => $id_args,
48 ) );
49 }
50
51 public function can_toggle( $request )
52 {
53 if ( is_user_logged_in() ) {
54 return true;
55 }
56 $nonce = (string) $request['nonce'];
57 if ( $nonce && wp_verify_nonce( $nonce, 'oacs_spl_likes_nonce' ) ) {
58 return true;
59 }
60 return new \WP_Error(
61 'rest_forbidden',
62 __( 'Authenticate or supply the nonce from GET /spl/v1/likes/<id>.', 'solid-post-likes' ),
63 array( 'status' => 403 )
64 );
65 }
66
67 public function get_item( $request )
68 {
69 $object = $this->validate_object( $request );
70 if ( is_wp_error( $object ) ) {
71 return $object;
72 }
73 list( $object_id, $is_comment ) = $object;
74
75 $checker = new Checker;
76 $store = new Store;
77 $response = rest_ensure_response( array(
78 'id' => $object_id,
79 'type' => $is_comment ? 'comment' : 'post',
80 'count' => $store->display_count( $object_id, $is_comment ),
81 'liked' => $checker->oacs_spl_already_liked( $object_id, $is_comment ),
82 'nonce' => wp_create_nonce( 'oacs_spl_likes_nonce' ),
83 ) );
84 // 'liked' and 'nonce' are per-visitor — never let a proxy/CDN cache this.
85 $response->header( 'Cache-Control', 'no-store' );
86 return $response;
87 }
88
89 public function toggle_item( $request )
90 {
91 // Only logged-in users may like when the setting is active.
92 if ( ! is_user_logged_in() && get_option( '_oacs_spl_logged_in_only' ) ) {
93 return new \WP_Error(
94 'spl_login_required',
95 __( 'Log in to like', 'solid-post-likes' ),
96 array( 'status' => 401, 'login_url' => wp_login_url() )
97 );
98 }
99
100 $object = $this->validate_object( $request );
101 if ( is_wp_error( $object ) ) {
102 return $object;
103 }
104 list( $object_id, $is_comment ) = $object;
105
106 $store = new Store;
107 $result = $store->toggle( $object_id, $is_comment );
108 return rest_ensure_response( array(
109 'status' => $result['status'],
110 'count' => $result['count'],
111 'liked' => ( $result['status'] === 'liked' ),
112 ) );
113 }
114
115 /** @return array{0:int,1:int}|\WP_Error [object_id, is_comment] for an existing, readable object. */
116 private function validate_object( $request )
117 {
118 // Route capture only — a body/query 'id' must not override the URL.
119 $url_params = $request->get_url_params();
120 $object_id = isset( $url_params['id'] ) ? (int) $url_params['id'] : 0;
121 $is_comment = ( $request['type'] === 'comment' ) ? 1 : 0;
122
123 // The object must exist AND be readable by the requester — otherwise
124 // this endpoint is an existence/count oracle for drafts, private
125 // posts, trashed items and unapproved comments.
126 if ( $is_comment ) {
127 $comment = get_comment( $object_id );
128 $readable = $comment && (
129 '1' === $comment->comment_approved
130 || current_user_can( 'moderate_comments' )
131 );
132 } else {
133 $readable = is_string( get_post_status( $object_id ) ) && (
134 is_post_publicly_viewable( $object_id )
135 || current_user_can( 'read_post', $object_id )
136 );
137 }
138 if ( ! $readable ) {
139 return new \WP_Error(
140 'spl_not_found',
141 __( 'No such post or comment.', 'solid-post-likes' ),
142 array( 'status' => 404 )
143 );
144 }
145 return array( $object_id, $is_comment );
146 }
147 }
148