PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / php / REST_API / Cloud / Cloud_Snippets_REST_Controller.php

Cloud_Snippets_REST_Controller.php in Code Snippets 4.0.0-beta.2, at php/REST_API/Cloud/Cloud_Snippets_REST_Controller.php

391 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\REST_API\Cloud;
4
5 use Code_Snippets\Admin\Menus\Manage\Manage_Menu;
6 use Code_Snippets\Controller\Cloud_Search_Controller;
7 use Code_Snippets\Model\Cloud_Snippets;
8 use Code_Snippets\REST_API\REST_Collection_Controller;
9 use WP_Error;
10 use WP_REST_Request;
11 use WP_REST_Response;
12 use WP_REST_Server;
13 use function Code_Snippets\code_snippets;
14 use function Code_Snippets\get_snippets;
15
16 /**
17 * Allows fetching cloud snippets through the WordPress REST API.
18 *
19 * @package Code_Snippets
20 */
21 final class Cloud_Snippets_REST_Controller extends REST_Collection_Controller {
22
23 /**
24 * Current API version.
25 */
26 public const VERSION = 1;
27
28 /**
29 * The base of this controller's route.
30 */
31 public const BASE_ROUTE = 'cloud/snippets';
32
33 /**
34 * Search methods supported by the cloud API.
35 */
36 private const SEARCH_METHODS = [ 'term', 'codevault', 'ai' ];
37
38 /**
39 * Search controller instance.
40 *
41 * @var Cloud_Search_Controller
42 */
43 private Cloud_Search_Controller $search_controller;
44
45 /**
46 * Class constructor.
47 *
48 * @param Cloud_Search_Controller $search_controller Cloud search controller.
49 */
50 public function __construct( Cloud_Search_Controller $search_controller ) {
51 parent::__construct();
52 $this->search_controller = $search_controller;
53 }
54
55 /**
56 * Check the request from Cloud API is valid
57 *
58 * @param WP_REST_Request $request Full data about the request.
59 *
60 * @return bool
61 */
62 public function permission_callback( WP_REST_Request $request ): bool {
63 return code_snippets()->current_user_can() && $this->search_controller->verify_rest_request( $request );
64 }
65
66 /**
67 * Common filter args shared across search and featured endpoints.
68 *
69 * Each filter accepts a single numeric ID (e.g. category=12). The cloud API
70 * resolves IDs to the underlying name/slug.
71 *
72 * @return array<string, array<string, mixed>>
73 */
74 private function get_filter_args(): array {
75 return [
76 'category' => [
77 'description' => esc_html__( 'Filter by category ID.', 'code-snippets' ),
78 'type' => 'string',
79 'default' => '',
80 ],
81 'type' => [
82 'description' => esc_html__( 'Filter by language/type ID.', 'code-snippets' ),
83 'type' => 'string',
84 'default' => '',
85 ],
86 'status' => [
87 'description' => esc_html__( 'Filter by status ID.', 'code-snippets' ),
88 'type' => 'string',
89 'default' => '',
90 ],
91 ];
92 }
93
94 /**
95 * Extract filter values from a request.
96 *
97 * @param WP_REST_Request $request Request object.
98 *
99 * @return array<string, string>
100 */
101 private function extract_filters( WP_REST_Request $request ): array {
102 $filters = [];
103
104 foreach ( [ 'category', 'type', 'status' ] as $filter ) {
105 if ( $request->has_param( $filter ) ) {
106 $filters[ $filter ] = $request->get_param( $filter ) ?? '';
107 }
108 }
109
110 return $filters;
111 }
112
113 /**
114 * Register REST routes.
115 */
116 public function register_routes() {
117 $collection_args = $this->get_collection_params();
118 $filter_args = $this->get_filter_args();
119
120 register_rest_route(
121 $this->namespace,
122 $this->rest_base,
123 [
124 [
125 'methods' => WP_REST_Server::READABLE,
126 'callback' => [ $this, 'get_items' ],
127 'permission_callback' => [ $this, 'get_items_permissions_check' ],
128 'args' => array_merge(
129 [
130 'query' => [
131 'description' => esc_html__( 'Search query.', 'code-snippets' ),
132 'type' => 'string',
133 'required' => true,
134 ],
135 'searchByCodevault' => [
136 'description' => esc_html__( 'Treat the search query as the name of a CodeVault instead of a search term.', 'code-snippets' ),
137 'type' => 'boolean',
138 'default' => false,
139 ],
140 'searchMethod' => [
141 'description' => esc_html__( 'Optional search method. Set to "ai" to match snippets by natural-language intent.', 'code-snippets' ),
142 'type' => 'string',
143 'enum' => self::SEARCH_METHODS,
144 'default' => 'term',
145 ],
146 'page' => $collection_args['page'],
147 'per_page' => $collection_args['per_page'],
148 ],
149 $filter_args
150 ),
151 ],
152 'schema' => [ $this, 'get_item_schema' ],
153 ]
154 );
155
156 register_rest_route(
157 $this->namespace,
158 $this->rest_base . '/featured',
159 [
160 [
161 'methods' => WP_REST_Server::READABLE,
162 'callback' => [ $this, 'get_featured_items' ],
163 'permission_callback' => [ $this, 'get_items_permissions_check' ],
164 'args' => array_merge(
165 $filter_args,
166 [
167 'page' => $collection_args['page'],
168 'per_page' => $collection_args['per_page'],
169 ]
170 ),
171 ],
172 'schema' => [ $this, 'get_item_schema' ],
173 ]
174 );
175
176 register_rest_route(
177 $this->namespace,
178 $this->rest_base . '/(?P<id>\d+)/download',
179 [
180 [
181 'methods' => WP_REST_Server::CREATABLE,
182 'callback' => [ $this, 'create_item' ],
183 'permission_callback' => [ $this, 'create_item_permissions_check' ],
184 'args' => [
185 'id' => [
186 'description' => esc_html__( 'Cloud snippet ID.', 'code-snippets' ),
187 'type' => 'number',
188 'required' => true,
189 ],
190 ],
191 ],
192 ]
193 );
194 }
195
196 /**
197 * Augment the standard controller collection query params for cloud searches.
198 *
199 * The per_page default is intentionally removed rather than set here: routes are
200 * registered once, so a schema default would capture a single user's Screen
201 * Options value. Callbacks resolve the per-user default at request time instead.
202 *
203 * @return array Query parameters for the collection.
204 */
205 public function get_collection_params(): array {
206 $params = parent::get_collection_params();
207 unset( $params['per_page']['default'] );
208 return $params;
209 }
210
211 /**
212 * Record which of the given cloud snippets have already been downloaded to this site.
213 *
214 * Downloading stores the remote identifier on the local snippet, so the local
215 * snippets are the only record of the link once the browser has been reloaded.
216 *
217 * @param Cloud_Snippets $snippets Cloud snippets as retrieved from the cloud API.
218 *
219 * @return Cloud_Snippets The same collection, with local identifiers attached.
220 */
221 private function attach_local_ids( Cloud_Snippets $snippets ): Cloud_Snippets {
222 $local_ids = [];
223
224 foreach ( get_snippets() as $local_snippet ) {
225 if ( $local_snippet->cloud_id && ! $local_snippet->trashed ) {
226 $local_ids[ $local_snippet->cloud_id ] = $local_snippet->id;
227 }
228 }
229
230 foreach ( $snippets->snippets as $cloud_snippet ) {
231 $cloud_snippet->local_id = $local_ids[ $cloud_snippet->id ] ?? null;
232 }
233
234 return $snippets;
235 }
236
237 /**
238 * Determine the search method to use based on request parameters.
239 *
240 * @param WP_REST_Request $request The request object containing the search parameters.
241 *
242 * @return string The search method to use.
243 */
244 private function get_search_method( WP_REST_Request $request ): string {
245 $search_by_codevault = $request->get_param( 'searchByCodevault' );
246 if ( $search_by_codevault ) {
247 return 'codevault';
248 }
249
250 $method_param = $request->get_param( 'searchMethod' );
251 return in_array( $method_param, self::SEARCH_METHODS, true ) ? $method_param : 'term';
252 }
253
254 /**
255 * Retrieve cloud snippets using a search query.
256 *
257 * @param WP_REST_Request $request The request object containing the search parameters.
258 *
259 * @return WP_REST_Response|WP_Error
260 */
261 public function get_items( $request ) {
262 $query = $request->get_param( 'query' ) ?? '';
263 $page = max( 1, intval( $request->get_param( 'page' ) ) );
264 $per_page = intval( $request->get_param( 'per_page' ) ?? Manage_Menu::get_cloud_search_per_page() );
265 $filters = $this->extract_filters( $request );
266
267 $snippets = $this->search_controller
268 ->fetch_search_results( $this->get_search_method( $request ), $query, $page, $per_page, $filters );
269
270 return $snippets
271 ? rest_ensure_response( $this->attach_local_ids( $snippets )->to_rest_response() )
272 : new WP_Error(
273 'code_snippets_get_snippets_failure',
274 esc_html__( 'Could not fetch snippets.', 'code-snippets' ),
275 [ 'status' => 500 ]
276 );
277 }
278
279 /**
280 * Retrieve featured snippets from the cloud API.
281 *
282 * @param WP_REST_Request $request The request object.
283 *
284 * @return WP_REST_Response|WP_Error
285 */
286 public function get_featured_items( WP_REST_Request $request ) {
287 $page = max( 1, intval( $request->get_param( 'page' ) ) );
288 $per_page = intval( $request->get_param( 'per_page' ) ?? Manage_Menu::get_cloud_search_per_page() );
289 $filters = $this->extract_filters( $request );
290
291 $snippets = $this->search_controller->get_featured_snippets( $page, $per_page, $filters );
292
293 return $snippets
294 ? rest_ensure_response( $this->attach_local_ids( $snippets )->to_rest_response() )
295 : new WP_Error(
296 'code_snippets_featured_snippets_failure',
297 esc_html__( 'Could not fetch featured snippets.', 'code-snippets' ),
298 [ 'status' => 500 ]
299 );
300 }
301
302 /**
303 * Download a single cloud snippet.
304 *
305 * @param WP_REST_Request $request The request object containing the search parameters.
306 *
307 * @return WP_REST_Response|WP_Error
308 */
309 public function create_item( $request ) {
310 $id = intval( $request->get_param( 'id' ) );
311 $cloud_snippet = $this->search_controller->get_cloud_snippet( $id );
312
313 if ( ! $cloud_snippet ) {
314 return new WP_Error(
315 'code_snippets_cloud_snippet_not_found',
316 esc_html__( 'Cloud snippet not found.', 'code-snippets' ),
317 [ 'status' => 404 ]
318 );
319 }
320
321 $local_snippet = $this->search_controller->download_snippet_from_cloud( $cloud_snippet );
322
323 return $local_snippet
324 ? rest_ensure_response(
325 [
326 'success' => true,
327 'snippet_id' => $local_snippet->id,
328 ]
329 )
330 : new WP_Error(
331 'code_snippets_cloud_snippet_download_failed',
332 esc_html__( 'Failed to create new snippet.', 'code-snippets' ),
333 [ 'status' => 500 ]
334 );
335 }
336
337 /**
338 * Retrieves the item's schema, conforming to JSON Schema.
339 *
340 * @return array
341 */
342 public function get_item_schema(): array {
343 if ( $this->schema ) {
344 return $this->schema;
345 }
346
347 $this->schema = [
348 '$schema' => 'http://json-schema.org/draft-04/schema#',
349 'title' => 'cloud snippet',
350 'type' => 'object',
351 'properties' => [
352 'id' => [
353 'description' => esc_html__( 'Cloud snippet identifier.', 'code-snippets' ),
354 'type' => 'string',
355 ],
356 'name' => [
357 'description' => esc_html__( 'Title of cloud snippet.', 'code-snippets' ),
358 'type' => 'string',
359 ],
360 'description' => [
361 'description' => esc_html__( 'Descriptive text associated with snippet.', 'code-snippets' ),
362 'type' => 'string',
363 ],
364 'code' => [
365 'description' => esc_html__( 'Executable snippet code.', 'code-snippets' ),
366 'type' => 'string',
367 ],
368 'scope' => [
369 'description' => esc_html__( 'Context in which the snippet is executable.', 'code-snippets' ),
370 'type' => 'string',
371 ],
372 'created' => [
373 'description' => esc_html__( 'Date and time when the snippet was last created, in ISO format.', 'code-snippets' ),
374 'type' => 'string',
375 ],
376 'revision' => [
377 'description' => esc_html__( 'Snippet revision number.', 'code-snippets' ),
378 'type' => 'integer',
379 ],
380 'active' => [
381 'description' => esc_html__( 'Whether the snippet should be active on the receiving site. Defaults to false to preserve install-as-inactive behaviour; set true for the self-heal redeploy path.', 'code-snippets' ),
382 'type' => 'boolean',
383 'default' => false,
384 ],
385 ],
386 ];
387
388 return $this->schema;
389 }
390 }
391