RestReviewController::get_item_schema()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 6
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Controllers\Api\Version1;
4
5
use GeminiLabs\SiteReviews\Controllers\Api\Version1\Response\Prepare;
6
use GeminiLabs\SiteReviews\Controllers\Api\Version1\Schema\ReviewParameters;
7
use GeminiLabs\SiteReviews\Controllers\Api\Version1\Schema\ReviewSchema;
8
use GeminiLabs\SiteReviews\Helpers\Arr;
9
use GeminiLabs\SiteReviews\Review;
10
use GeminiLabs\SiteReviews\Reviews;
11
use GeminiLabs\SiteReviews\Shortcodes\SiteReviewsShortcode;
12
13
class RestReviewController extends \WP_REST_Controller
14
{
15
    public function __construct()
16
    {
17
        $this->namespace = glsr()->id.'/v1';
18
        $this->rest_base = 'reviews';
19
    }
20
21
    /**
22
     * @return bool
23
     */
24
    public function check_read_permission(Review $review)
25
    {
26
        return $review->is_approved || glsr()->can('read_post', $review->ID);
27
    }
28
29
    /**
30
     * @param \WP_REST_Request $request
31
     *
32
     * @return \WP_REST_Response|\WP_Error
33
     */
34
    public function create_item($request)
35
    {
36
        $review = glsr_create_review($request->get_params());
37
        if (false === $review || !$review->isValid()) {
38
            $error = _x('Review creation failed, please check the Site Reviews console log for more details.', 'admin-text', 'site-reviews');
39
            return new \WP_Error('rest_review_create_item', $error, ['status' => 500]);
40
        }
41
        if ($request['_rendered']) {
42
            $response = $this->renderedItems($request);
43
        } else {
44
            $data = $this->prepare_item_for_response($review, $request);
45
            $response = rest_ensure_response($data);
46
        }
47
        $response->set_status(201);
48
        $response->header('Location', rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $review->ID)));
49
        return $response;
50
    }
51
52
    /**
53
     * @param \WP_REST_Request $request
54
     *
55
     * @return true|\WP_Error
56
     */
57
    public function create_item_permissions_check($request)
58
    {
59
        if (!empty($request['id'])) {
60
            $error = _x('Cannot create existing review.', 'admin-text', 'site-reviews');
61
            return new \WP_Error('rest_review_exists', $error, ['status' => 400]);
62
        }
63
        if (!empty($request['author']) && get_current_user_id() !== $request['author'] && !glsr()->can('edit_others_posts')) {
64
            $error = _x('Sorry, you are not allowed to create reviews as this user.', 'admin-text', 'site-reviews');
65
            return new \WP_Error('rest_cannot_edit_others', $error, ['status' => rest_authorization_required_code()]);
66
        }
67
        if (!glsr()->can('create_posts')) {
68
            $error = _x('Sorry, you are not allowed to create reviews as this user.', 'admin-text', 'site-reviews');
69
            return new \WP_Error('rest_cannot_create', $error, ['status' => rest_authorization_required_code()]);
70
        }
71
        if (!$this->check_assign_terms_permission($request)) {
72
            $error = _x('Sorry, you are not allowed to assign the provided terms.', 'admin-text', 'site-reviews');
73
            return new \WP_Error('rest_cannot_assign_term', $error, ['status' => rest_authorization_required_code()]);
74
        }
75
        return true;
76
    }
77
78
    /**
79
     * @param \WP_REST_Request $request
80
     *
81
     * @return \WP_REST_Response|\WP_Error
82
     */
83
    public function delete_item($request)
84
    {
85
        $request->set_param('context', 'edit');
86
        if ((bool) $request['force']) {
87
            return $this->forceDeleteItem($request);
88
        }
89
        if (EMPTY_TRASH_DAYS < 1) {
90
            $error = sprintf(_x('The review does not support trashing. Set "%s" to delete.', 'admin-text', 'site-reviews'), 'force=true');
91
            return new \WP_Error('rest_trash_not_supported', $error, ['status' => 501]);
92
        }
93
        if ('trash' === get_post_status($request['id'])) {
94
            $error = _x('The review has already been deleted.', 'admin-text', 'site-reviews');
95
            return new \WP_Error('rest_already_trashed', $error, ['status' => 410]);
96
        }
97
        if (!wp_trash_post($request['id'])) {
98
            $error = _x('The review cannot be deleted.', 'admin-text', 'site-reviews');
99
            return new \WP_Error('rest_cannot_delete', $error, ['status' => 500]);
100
        }
101
        $review = glsr_get_review($request['id']);
102
        return $this->prepare_item_for_response($review, $request);
103
    }
104
105
    /**
106
     * @param \WP_REST_Request $request
107
     *
108
     * @return true|\WP_Error
109
     */
110
    public function delete_item_permissions_check($request)
111
    {
112
        $review = glsr_get_review($request['id']);
113
        if (!$review->isValid()) {
114
            $error = _x('Invalid review ID.', 'admin-text', 'site-reviews');
115
            return new \WP_Error('rest_review_invalid_id', $error, ['status' => 404]);
116
        }
117
        if (!glsr()->can('delete_post', $review->ID)) {
118
            $error = _x('Sorry, you are not allowed to delete this review.', 'admin-text', 'site-reviews');
119
            return new \WP_Error('rest_cannot_delete', $error, ['status' => rest_authorization_required_code()]);
120
        }
121
        return true;
122
    }
123
124
    /**
125
     * @return array
126
     */
127
    public function get_collection_params()
128
    {
129
        $params = glsr(ReviewParameters::class)->parameters();
130
        $params['context'] = $this->get_context_param(['default' => 'view']);
131
        return apply_filters('rest_review_collection_params', $params, glsr()->post_type);
132
    }
133
134
    /**
135
     * @param \WP_REST_Request $request
136
     *
137
     * @return \WP_REST_Response|\WP_Error
138
     */
139
    public function get_item($request)
140
    {
141
        if ($request['_rendered']) {
142
            return $this->renderedItem($request);
143
        }
144
        $review = glsr_get_review($request['id']);
145
        $data = $this->prepare_item_for_response($review, $request);
146
        return rest_ensure_response($data);
147
    }
148
149
    /**
150
     * @param \WP_REST_Request $request
151
     *
152
     * @return true|\WP_Error
153
     */
154
    public function get_item_permissions_check($request)
155
    {
156
        $review = glsr_get_review($request['id']);
157
        if (!$review->isValid()) {
158
            $error = _x('Invalid review ID.', 'admin-text', 'site-reviews');
159
            return new \WP_Error('rest_review_invalid_id', $error, ['status' => 404]);
160
        }
161
        if (!is_user_logged_in() || !$this->check_read_permission($review)) {
162
            $error = _x('Sorry, you are not allowed to view this review.', 'admin-text', 'site-reviews');
163
            return new \WP_Error('rest_cannot_view', $error, ['status' => rest_authorization_required_code()]);
164
        }
165
        return true;
166
    }
167
168
    /**
169
     * @return array
170
     */
171
    public function get_item_schema()
172
    {
173
        if (empty($this->schema)) {
174
            $this->schema = glsr(ReviewSchema::class)->schema();
175
        }
176
        return $this->add_additional_fields_schema($this->schema);
177
    }
178
179
    /**
180
     * @param \WP_REST_Request $request
181
     *
182
     * @return \WP_REST_Response|\WP_Error
183
     */
184
    public function get_items($request)
185
    {
186
        if ($request['_rendered']) {
187
            return $this->renderedItems($request);
188
        }
189
        $results = glsr_get_reviews($this->normalizedArgs($request));
190
        $reviews = [];
191
        foreach ($results->reviews as $review) {
192
            if ($this->check_read_permission($review)) {
193
                $data = $this->prepare_item_for_response($review, $request);
194
                $reviews[] = $this->prepare_response_for_collection($data);
195
            }
196
        }
197
        if ($results->args['page'] > $results->max_num_pages && $results->total > 0) {
198
            $error = _x('The page number requested is larger than the number of pages available.', 'admin-text', 'site-reviews');
199
            return new \WP_Error('rest_invalid_page_number', $error, ['status' => 400]);
200
        }
201
        $response = rest_ensure_response($reviews);
202
        if (is_wp_error($response)) { // @phpstan-ignore-line
203
            return $response;
204
        }
205
        return $this->prepareResponse($response, $request, $results);
206
    }
207
208
    /**
209
     * @param \WP_REST_Request $request
210
     *
211
     * @return true|\WP_Error
212
     */
213
    public function get_items_permissions_check($request)
214
    {
215
        if (!is_user_logged_in()) {
216
            $error = _x('Sorry, you do not have permission to access reviews.', 'admin-text', 'site-reviews');
217
        }
218
        if ('edit' === $request['context'] && !glsr()->can('edit_posts')) {
219
            $error = _x('Sorry, you are not allowed to edit reviews.', 'admin-text', 'site-reviews');
220
        }
221
        if (isset($error)) {
222
            return new \WP_Error('rest_forbidden_context', $error, [
223
                'status' => rest_authorization_required_code(),
224
            ]);
225
        }
226
        return true;
227
    }
228
229
    /**
230
     * @param Review           $review
231
     * @param \WP_REST_Request $request
232
     *
233
     * @return \WP_REST_Response
234
     */
235
    public function prepare_item_for_response($review, $request)
236
    {
237
        $fields = $this->get_fields_for_response($request);
238
        $prepare = new Prepare($fields, $review, $request);
239
        glsr()->store('api', true); // load all review fields!
240
        foreach ($fields as $field) {
241
            call_user_func([$prepare, $field]);
242
        }
243
        glsr()->discard('api');
244
        $data = $prepare->item();
245
        $data = $this->add_additional_fields_to_object($data, $request);
246
        $data = $this->filter_response_by_context($data, Arr::get($request, 'context', 'view'));
247
        $response = rest_ensure_response($data);
248
        $links = $this->prepareLinks($review);
249
        $response->add_links($links);
250
        if ($self = Arr::get($links, 'self.href')) {
251
            $actions = $this->getAvailableActions($review, $request);
252
            foreach ($actions as $rel) {
253
                $response->add_link($rel, $self);
254
            }
255
        }
256
        return $response; // @todo filter this, i.e. "rest_prepare_{glsr()->post_type}"
257
    }
258
259
    /**
260
     * @return void
261
     */
262
    public function register_routes()
263
    {
264
        register_rest_route($this->namespace, "/{$this->rest_base}", [
265
            [
266
                'args' => $this->get_collection_params(),
267
                'callback' => [$this, 'get_items'],
268
                'methods' => \WP_REST_Server::READABLE,
269
                'permission_callback' => [$this, 'get_items_permissions_check'],
270
            ], [
271
                'args' => $this->get_endpoint_args_for_item_schema(\WP_REST_Server::CREATABLE),
272
                'callback' => [$this, 'create_item'],
273
                'methods' => \WP_REST_Server::CREATABLE,
274
                'permission_callback' => [$this, 'create_item_permissions_check'],
275
            ],
276
            'schema' => [$this, 'get_public_item_schema'],
277
        ]);
278
        register_rest_route($this->namespace, "/{$this->rest_base}".'/(?P<id>[\d]+)', [
279
            [
280
                'args' => [
281
                    'context' => $this->get_context_param(['default' => 'view']),
282
                ],
283
                'callback' => [$this, 'get_item'],
284
                'methods' => \WP_REST_Server::READABLE,
285
                'permission_callback' => [$this, 'get_item_permissions_check'],
286
            ],
287
            [
288
                'args' => $this->get_endpoint_args_for_item_schema(\WP_REST_Server::EDITABLE),
289
                'callback' => [$this, 'update_item'],
290
                'methods' => \WP_REST_Server::EDITABLE,
291
                'permission_callback' => [$this, 'update_item_permissions_check'],
292
            ],
293
            [
294
                'args' => [
295
                    'force' => [
296
                        'default' => false,
297
                        'description' => _x('Whether to bypass Trash and force deletion.', 'admin-text', 'site-reviews'),
298
                        'type' => 'boolean',
299
                    ],
300
                ],
301
                'callback' => [$this, 'delete_item'],
302
                'methods' => \WP_REST_Server::DELETABLE,
303
                'permission_callback' => [$this, 'delete_item_permissions_check'],
304
            ],
305
            'args' => [
306
                'id' => [
307
                    'description' => _x('Unique identifier for the object.', 'admin-text', 'site-reviews'),
308
                    'type' => 'integer',
309
                ],
310
            ],
311
            'schema' => [$this, 'get_public_item_schema'],
312
        ]);
313
    }
314
315
    /**
316
     * @param \WP_REST_Request $request
317
     *
318
     * @return \WP_REST_Response|\WP_Error
319
     */
320
    public function update_item($request)
321
    {
322
        $review = glsr_update_review($request['id'], $request->get_params());
323
        if (!$review) {
324
            $error = _x('Review update failed, please check the Site Reviews console log for more details.', 'admin-text', 'site-reviews');
325
            return new \WP_Error('rest_update_review', $error, ['status' => 500]);
326
        }
327
        $request->set_param('context', 'edit');
328
        if ($request['_rendered']) {
329
            return $this->renderedItem($request);
330
        }
331
        $data = $this->prepare_item_for_response($review, $request);
332
        $response = rest_ensure_response($data);
333
        return $response;
334
    }
335
336
    /**
337
     * @param \WP_REST_Request $request
338
     *
339
     * @return true|\WP_Error
340
     */
341
    public function update_item_permissions_check($request)
342
    {
343
        $review = glsr_get_review($request['id']);
344
        if (!$review->isValid()) {
345
            $error = _x('Invalid review ID.', 'admin-text', 'site-reviews');
346
            return new \WP_Error('rest_review_invalid_id', $error, ['status' => 404]);
347
        }
348
        if (!glsr()->can('edit_post', $review->ID)) {
349
            $error = _x('Sorry, you are not allowed to edit this review.', 'admin-text', 'site-reviews');
350
            return new \WP_Error('rest_cannot_edit', $error, ['status' => rest_authorization_required_code()]);
351
        }
352
        if (!empty($request['author']) && get_current_user_id() !== $request['author'] && !glsr()->can('edit_others_posts')) {
353
            $error = _x('Sorry, you are not allowed to update reviews as this user.', 'admin-text', 'site-reviews');
354
            return new \WP_Error('rest_cannot_edit_others', $error, ['status' => rest_authorization_required_code()]);
355
        }
356
        if (!$this->check_assign_terms_permission($request)) {
357
            $error = _x('Sorry, you are not allowed to assign the provided terms.', 'admin-text', 'site-reviews');
358
            return new \WP_Error('rest_cannot_assign_term', $error, ['status' => rest_authorization_required_code()]);
359
        }
360
        return true;
361
    }
362
363
    /**
364
     * @param \WP_REST_Request $request
365
     *
366
     * @return bool
367
     */
368
    protected function check_assign_terms_permission($request)
369
    {
370
        $terms = Arr::consolidate($request['assigned_terms']);
371
        foreach ($terms as $termId) {
372
            if (!get_term($termId, glsr()->taxonomy)) {
373
                continue; // Invalid terms will be rejected later
374
            }
375
            if (!current_user_can('assign_term', (int) $termId)) {
376
                return false;
377
            }
378
        }
379
        return true;
380
    }
381
382
    /**
383
     * @param \WP_REST_Request $request
384
     *
385
     * @return \WP_REST_Response|\WP_Error
386
     */
387
    public function forceDeleteItem($request)
388
    {
389
        $review = glsr_get_review($request['id']);
390
        $previous = $this->prepare_item_for_response($review, $request);
391
        $result = wp_delete_post($review->ID, true);
392
        if (false === $result) {
393
            $error = _x('The review cannot be deleted.', 'admin-text', 'site-reviews');
394
            return new \WP_Error('rest_cannot_delete', $error, ['status' => 500]);
395
        }
396
        return rest_ensure_response([
397
            'deleted' => true,
398
            'previous' => $previous->get_data(),
399
        ]);
400
    }
401
402
    /**
403
     * @return array
404
     */
405
    protected function getAvailableActions(Review $review, \WP_REST_Request $request)
406
    {
407
        if ('edit' !== $request['context']) {
408
            return [];
409
        }
410
        $rels = [];
411
        $taxonomy = get_taxonomy(glsr()->taxonomy);
412
        if (glsr()->can('publish_posts')) {
413
            $rels[] = 'https://api.w.org/action-publish';
414
        }
415
        if (glsr()->can('edit_others_posts')) {
416
            $rels[] = 'https://api.w.org/action-assign-author';
417
        }
418
        if (current_user_can($taxonomy->cap->edit_terms)) {
419
            $rels[] = 'https://api.w.org/action-create-'.glsr()->taxonomy;
420
        }
421
        if (current_user_can($taxonomy->cap->assign_terms)) {
422
            $rels[] = 'https://api.w.org/action-assign-'.glsr()->taxonomy;
423
        }
424
        return $rels;
425
    }
426
427
    /**
428
     * @return array
429
     */
430
    protected function normalizedArgs(\WP_REST_Request $request)
431
    {
432
        $args = [];
433
        $registered = $this->get_collection_params();
434
        foreach ($registered as $key => $params) {
435
            if (isset($request[$key])) {
436
                $args[$key] = $request[$key];
437
            }
438
        }
439
        if (empty($args['date'])) {
440
            $args['date'] = [
441
                'after' => $args['after'] ?? '',
442
                'before' => $args['before'] ?? '',
443
            ];
444
        }
445
        return glsr()->filterArray("rest-api/{$this->rest_base}/args", $args, $request);
446
    }
447
448
    /**
449
     * @return array
450
     */
451
    protected function prepareLinks(Review $review)
452
    {
453
        $base = $this->namespace.'/'.$this->rest_base;
454
        $revisions = wp_get_post_revisions($review->ID, ['fields' => 'ids']);
455
        $revisionCount = count($revisions);
456
        $links = [
457
            'self' => [
458
                'href' => rest_url(trailingslashit($base).$review->ID),
459
            ],
460
            'collection' => [
461
                'href' => rest_url($base),
462
            ],
463
            'about' => [
464
                'href' => rest_url('wp/v2/types/'.glsr()->post_type),
465
            ],
466
            'https://api.w.org/attachment' => [
467
                'href' => add_query_arg('parent', $review->ID, rest_url('wp/v2/media')),
468
            ],
469
            'https://api.w.org/term' => [
470
                'embeddable' => true,
471
                'href' => add_query_arg('post', $review->ID, rest_url('wp/v2/'.glsr()->taxonomy)),
472
                'taxonomy' => glsr()->taxonomy,
473
            ],
474
            'version-history' => [
475
                'count' => $revisionCount,
476
                'href' => rest_url(trailingslashit($base)."{$review->ID}/revisions"),
477
            ],
478
        ];
479
        if ($revisionCount > 0) {
480
            $lastRevision = array_shift($revisions);
481
            $links['predecessor-version'] = [
482
                'href' => rest_url(trailingslashit($base)."{$review->ID}/revisions/{$lastRevision}"),
483
                'id' => $lastRevision,
484
            ];
485
        }
486
        if (!empty($review->user_id)) {
487
            $links['author'] = [
488
                'embeddable' => true,
489
                'href' => rest_url("wp/v2/users/{$review->user_id}"),
490
            ];
491
        }
492
        if (post_type_supports(glsr()->post_type, 'comments')) {
493
            $links['replies'] = [
494
                'embeddable' => true,
495
                'href' => add_query_arg('post', $review->ID, rest_url('wp/v2/comments')),
496
            ];
497
        }
498
        return $links;
499
    }
500
501
    /**
502
     * @return \WP_REST_Response
503
     */
504
    protected function prepareResponse(\WP_REST_Response $response, \WP_REST_Request $request, Reviews $reviews)
505
    {
506
        $page = $reviews->args['page'];
507
        $ratings = glsr_get_ratings($this->normalizedArgs($request));
508
        $response->header('X-GLSR-Average', (string) $ratings->average);
509
        $response->header('X-GLSR-Ranking', (string) $ratings->ranking);
510
        $response->header('X-WP-Total', (string) $reviews->total);
511
        $response->header('X-WP-TotalPages', (string) $reviews->max_num_pages);
512
        $parameters = $request->get_query_params();
513
        $base = add_query_arg(urlencode_deep($parameters), rest_url(sprintf('%s/%s', $this->namespace, $this->rest_base)));
514
        if ($page > 1) {
515
            $prevPage = $page - 1;
516
            if ($prevPage > $reviews->max_num_pages) {
517
                $prevPage = $reviews->max_num_pages;
518
            }
519
            $prevLink = add_query_arg('page', $prevPage, $base);
520
            $response->link_header('prev', $prevLink);
521
        }
522
        if ($reviews->max_num_pages > $page) {
523
            $nextPage = $page + 1;
524
            $nextLink = add_query_arg('page', $nextPage, $base);
525
            $response->link_header('next', $nextLink);
526
        }
527
        return $response;
528
    }
529
530
    protected function renderedItem(\WP_REST_Request $request)
531
    {
532
        $args = $this->normalizedArgs($request);
533
        $args['hide'] = $request['_rendered_hide'] ?? '';
534
        $review = glsr_get_review($request['id']);
535
        return rest_ensure_response([
536
            'rendered' => (string) $review->build($args),
537
        ]);
538
    }
539
540
    protected function renderedItems(\WP_REST_Request $request)
541
    {
542
        $args = $this->normalizedArgs($request);
543
        $args['hide'] = $request['_rendered_hide'] ?? '';
544
        $html = glsr(SiteReviewsShortcode::class)
545
            ->normalize($args)
546
            ->buildReviewsHtml();
547
        $response = rest_ensure_response([
548
            'pagination' => $html->getPagination($wrap = false),
549
            'rendered' => $html->getReviews(),
550
        ]);
551
        return $this->prepareResponse($response, $request, $html->reviews);
552
    }
553
}
554