Test Failed
Push — main ( 6435b1...db043e )
by Paul
16:23 queued 07:02
created

ReviewController::onCreateReview()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 30
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 13.4275

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
dl 0
loc 30
ccs 9
cts 22
cp 0.4091
rs 8.9297
c 1
b 0
f 0
cc 6
nc 17
nop 2
crap 13.4275
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Controllers;
4
5
use GeminiLabs\SiteReviews\Commands\AssignPosts;
6
use GeminiLabs\SiteReviews\Commands\AssignTerms;
7
use GeminiLabs\SiteReviews\Commands\AssignUsers;
8
use GeminiLabs\SiteReviews\Commands\CreateReview;
9
use GeminiLabs\SiteReviews\Commands\ToggleStatus;
10
use GeminiLabs\SiteReviews\Commands\UnassignPosts;
11
use GeminiLabs\SiteReviews\Commands\UnassignTerms;
12
use GeminiLabs\SiteReviews\Commands\UnassignUsers;
13
use GeminiLabs\SiteReviews\Database;
14
use GeminiLabs\SiteReviews\Database\Cache;
15
use GeminiLabs\SiteReviews\Database\CountManager;
16
use GeminiLabs\SiteReviews\Database\Query;
17
use GeminiLabs\SiteReviews\Database\ReviewManager;
18
use GeminiLabs\SiteReviews\Defaults\RatingDefaults;
19
use GeminiLabs\SiteReviews\Helper;
20
use GeminiLabs\SiteReviews\Helpers\Arr;
21
use GeminiLabs\SiteReviews\Helpers\Cast;
22
use GeminiLabs\SiteReviews\Metaboxes\ResponseMetabox;
23
use GeminiLabs\SiteReviews\Modules\Avatar;
24
use GeminiLabs\SiteReviews\Modules\Html\ReviewHtml;
25
use GeminiLabs\SiteReviews\Modules\Queue;
26
use GeminiLabs\SiteReviews\Request;
27
use GeminiLabs\SiteReviews\Review;
28
29
class ReviewController extends AbstractController
30
{
31
    /**
32
     * @param \WP_Post[] $posts
33
     *
34
     * @return \WP_Post[]
35
     *
36
     * @filter the_posts
37
     */
38
    public function filterPostsToCacheReviews(array $posts): array
39
    {
40
        $reviews = array_filter($posts, fn ($post) => glsr()->post_type === $post->post_type);
41
        if ($postIds = wp_list_pluck($reviews, 'ID')) {
42
            glsr(Query::class)->reviews([], $postIds); // this caches the associated Review objects
43
        }
44
        return $posts;
45
    }
46
47
    /**
48
     * @filter wp_insert_post_data
49
     */
50
    public function filterReviewPostData(array $data, array $sanitized): array
51
    {
52
        if (empty($sanitized['ID']) || empty($sanitized['action']) || glsr()->post_type !== Arr::get($sanitized, 'post_type')) {
53
            return $data;
54
        }
55
        if (!empty(filter_input(INPUT_GET, 'bulk_edit'))) {
56
            if (is_numeric(filter_input(INPUT_GET, 'post_author'))) {
57
                $data['post_author'] = filter_input(INPUT_GET, 'post_author');
58
            } else {
59
                unset($data['post_author']);
60
            }
61
        }
62
        if (is_numeric(filter_input(INPUT_POST, 'post_author_override'))) {
63
            // use the value from the author meta box
64
            $data['post_author'] = filter_input(INPUT_POST, 'post_author_override');
65
        }
66
        return $data;
67
    }
68 28
69
    /**
70 28
     * @filter site-reviews/rendered/template/review
71 28
     */
72
    public function filterReviewTemplate(string $template, array $data): string
73
    {
74
        $search = 'id="review-';
75
        $dataType = Arr::get($data, 'review.type', 'local');
76
        $replace = sprintf('data-type="%s" %s', $dataType, $search);
77
        if (Arr::get($data, 'review.is_pinned')) {
78
            $replace = 'data-pinned="1" '.$replace;
79
        }
80
        if (Arr::get($data, 'review.is_verified')) {
81
            $replace = 'data-verified="1" '.$replace;
82
        }
83
        return str_replace($search, $replace, $template);
84
    }
85
86
    /**
87
     * @filter site-reviews/query/sql/clause/operator
88
     */
89
    public function filterSqlClauseOperator(string $operator): string
90 8
    {
91
        $operators = ['loose' => 'OR', 'strict' => 'AND'];
92 8
        return Arr::get($operators, glsr_get_option('reviews.assignment', 'strict', 'string'), $operator);
93 8
    }
94 8
95 8
    /**
96
     * @filter site-reviews/review/build/after
97
     */
98 8
    public function filterTemplateTags(array $tags, Review $review, ReviewHtml $reviewHtml): array
99
    {
100
        $tags['assigned_links'] = $reviewHtml->buildTemplateTag($review, 'assigned_links', $review->assigned_posts);
101 8
        return $tags;
102
    }
103
104
    /**
105
     * Triggered when one or more categories are added or removed from a review.
106
     *
107 8
     * @action set_object_terms
108
     */
109 8
    public function onAfterChangeAssignedTerms(
110 8
        int $postId,
111
        array $terms,
112
        array $newTTIds,
113
        string $taxonomy,
114
        bool $append,
115
        array $oldTTIds
116 8
    ): void {
117
        if (Review::isReview($postId)) {
118 8
            $review = glsr(ReviewManager::class)->get($postId);
119 8
            $diff = $this->getAssignedDiffs($oldTTIds, $newTTIds);
120
            $this->execute(new UnassignTerms($review, $diff['old']));
121
            $this->execute(new AssignTerms($review, $diff['new']));
122
        }
123
    }
124
125
    /**
126
     * Triggered when a post status changes or when a review is approved|unapproved|trashed.
127
     *
128
     * @action transition_post_status
129
     */
130
    public function onAfterChangeStatus(string $new, string $old, ?\WP_Post $post): void
131
    {
132
        if (is_null($post)) {
133 28
            return; // This should never happen, but some plugins are bad actors so...
134
        }
135 28
        if (in_array($old, ['new', $new])) {
136 24
            return;
137 24
        }
138 24
        if (Review::isReview($post)) {
139 24
            $isAutoDraft = 'auto-draft' === $old && 'auto-draft' !== $new;
140
            if ($isAutoDraft) {
141
                glsr(ReviewManager::class)->createFromPost($post->ID);
142
            }
143
            $isPublished = 'publish' === $new;
144
            glsr(ReviewManager::class)->updateRating($post->ID, ['is_approved' => $isPublished]);
145
            glsr(Cache::class)->delete($post->ID, 'reviews');
146
            glsr(CountManager::class)->recalculate();
147
            if ($isAutoDraft) {
148
                return;
149
            }
150
            $review = glsr_get_review($post->ID);
151 28
            if ('publish' === $new) {
152
                glsr()->action('review/approved', $review, $old, $new);
153 28
            } elseif ('pending' === $new) {
154 28
                glsr()->action('review/unapproved', $review, $old, $new);
155
            } elseif ('trash' === $new) {
156
                glsr()->action('review/trashed', $review, $old, $new);
157
            }
158
            glsr()->action('review/transitioned', $review, $new, $old);
159
        } else {
160
            glsr(ReviewManager::class)->updateAssignedPost($post->ID);
161
        }
162
    }
163
164
    /**
165
     * Fallback action if ajax is not working for any reason.
166
     *
167
     * @action admin_action_approve
168
     */
169
    public function onApprove(): void
170
    {
171
        if (glsr()->id === filter_input(INPUT_GET, 'plugin')) {
172
            check_admin_referer('approve-review_'.($postId = $this->getPostId()));
173
            $this->execute(new ToggleStatus(new Request([
174
                'post_id' => $postId,
175
                'status' => 'publish',
176
            ])));
177
            wp_safe_redirect(wp_get_referer());
178
            exit;
179
        }
180
    }
181
182
    /**
183
     * Triggered when a review's assigned post IDs are updated.
184
     *
185
     * @action site-reviews/review/updated/post_ids
186
     */
187
    public function onChangeAssignedPosts(Review $review, array $postIds = []): void
188
    {
189
        $diff = $this->getAssignedDiffs($review->assigned_posts, $postIds);
190
        $this->execute(new UnassignPosts($review, $diff['old']));
191
        $this->execute(new AssignPosts($review, $diff['new']));
192
    }
193
194
    /**
195
     * Triggered when a review's assigned users IDs are updated.
196
     *
197
     * @action site-reviews/review/updated/user_ids
198
     */
199
    public function onChangeAssignedUsers(Review $review, array $userIds = []): void
200
    {
201
        $diff = $this->getAssignedDiffs($review->assigned_users, $userIds);
202
        $this->execute(new UnassignUsers($review, $diff['old']));
203
        $this->execute(new AssignUsers($review, $diff['new']));
204
    }
205
206
    /**
207
     * Triggered after a review is created.
208
     *
209
     * @action site-reviews/review/created
210 24
     */
211
    public function onCreatedReview(Review $review, CreateReview $command): void
212 24
    {
213 24
        $this->execute(new AssignPosts($review, $command->assigned_posts));
214
        $this->execute(new AssignUsers($review, $command->assigned_users));
215
    }
216
217
    /**
218
     * Triggered when a review is created.
219
     *
220 24
     * @action site-reviews/review/create
221
     */
222 24
    public function onCreateReview(int $postId, CreateReview $command): void
223 24
    {
224 24
        $values = glsr()->args($command->toArray()); // this filters the values
225 24
        $data = glsr(RatingDefaults::class)->restrict($values->toArray());
226 24
        $data['review_id'] = $postId;
227
        $data['is_approved'] = 'publish' === get_post_status($postId);
228
        if (false === glsr(Database::class)->insert('ratings', $data)) {
229
            glsr_log()->error('A review could not be created. Here are some things to try which may fix the problem:'.
230
                PHP_EOL.'1. First, deactivate Site Reviews and then reactivate it (this should fix any broken database table indexes).'.
231
                PHP_EOL.'2. Next, hold down the ALT key (Option key if using a Mac) and run the Migrate Plugin tool.'.
232
                PHP_EOL.'3. Finally, run the "Repair Review Relations" tool.'.
233
                PHP_EOL.'4. If the problem persists, please use the "Contact Support" section on the Help page.'
234
            );
235
            glsr_log()->debug($data);
236
            wp_delete_post($postId, true); // remove post as review was not created
237 24
            return;
238 24
        }
239
        $termIds = wp_set_object_terms($postId, $values->assigned_terms, glsr()->taxonomy);
240
        if (is_wp_error($termIds)) {
241 24
            glsr_log()->error($termIds->get_error_message());
242
        }
243
        if ($excluded = Cast::toArray($command->request()->decrypt('excluded'))) {
244
            glsr(Database::class)->metaSet($postId, 'excluded', $excluded); // save the fields hidden in the review form
245 24
        }
246
        if (!empty($values->response)) { // save the response if one is provided
247
            glsr(Database::class)->metaSet($postId, 'response', $values->response);
248
            glsr(Database::class)->metaSet($postId, 'response_by', $values->response_by); // @phpstan-ignore-line
249
        }
250
        foreach ($values->custom as $key => $value) {
251
            glsr(Database::class)->metaSet($postId, "custom_{$key}", $value);
252
        }
253
    }
254
255
    /**
256
     * Triggered when a review or other post type is deleted and the posts table uses the MyISAM engine.
257
     *
258
     * @action deleted_post
259
     */
260
    public function onDeletePost(int $postId, \WP_Post $post): void
261
    {
262
        if (glsr()->post_type === $post->post_type) {
263
            $this->onDeleteReview($postId);
264
            return;
265
        }
266
        $reviewIds = glsr(Query::class)->reviewIds([
267
            'assigned_posts' => $postId,
268
            'per_page' => -1,
269
            'status' => 'all',
270
        ]);
271
        if (glsr(Database::class)->delete('assigned_posts', ['post_id' => $postId])) {
272
            array_walk($reviewIds, function ($reviewId) {
273
                glsr(Cache::class)->delete($reviewId, 'reviews');
274
            });
275
        }
276
    }
277
278
    /**
279
     * Triggered when a review is deleted and the posts table uses the MyISAM engine.
280
     *
281
     * @see $this->onDeletePost()
282
     */
283
    public function onDeleteReview(int $reviewId): void
284
    {
285
        glsr(ReviewManager::class)->deleteRating($reviewId);
286
    }
287
288
    /**
289
     * Triggered when a user is deleted and the users table uses the MyISAM engine.
290
     *
291
     * @action deleted_user
292
     */
293
    public function onDeleteUser(int $userId = 0): void
294
    {
295
        $reviewIds = glsr(Query::class)->reviewIds([
296
            'assigned_users' => $userId,
297
            'per_page' => -1,
298
            'status' => 'all',
299
        ]);
300
        if (glsr(Database::class)->delete('assigned_users', ['user_id' => $userId])) {
301
            array_walk($reviewIds, function ($reviewId) {
302
                glsr(Cache::class)->delete($reviewId, 'reviews');
303
            });
304
        }
305
    }
306
307
    /**
308
     * Triggered when a review is edited or trashed.
309
     * It's unnecessary to trigger a term recount as this is done by the set_object_terms hook
310
     * We need to use "post_updated" to support revisions (vs "save_post").
311
     *
312
     * @action post_updated
313
     */
314
    public function onEditReview(int $postId, ?\WP_Post $post, ?\WP_Post $oldPost): void
315
    {
316
        if (is_null($post) || is_null($oldPost)) {
317
            return; // This should never happen, but some plugins are bad actors so...
318
        }
319
        if (!glsr()->can('edit_posts') || !$this->isEditedReview($post, $oldPost)) {
320
            return;
321
        }
322
        if (glsr()->id === filter_input(INPUT_GET, 'plugin')) {
323
            return; // the fallback approve/unapprove action is being run
324
        }
325
        if (!in_array(glsr_current_screen()->base, ['edit', 'post'])) {
326
            return; // only trigger this action from the Site Reviews edit/post screens
327
        }
328
        $review = glsr(ReviewManager::class)->get($postId);
329
        if ('edit' === glsr_current_screen()->base) {
330
            $this->bulkUpdateReview($review, $oldPost);
331
        } else {
332
            $this->updateReview($review, $oldPost);
333 24
        }
334
    }
335 24
336
    /**
337
     * Fallback action if ajax is not working for any reason.
338 24
     *
339 24
     * @action admin_action_unapprove
340
     */
341
    public function onUnapprove(): void
342
    {
343
        if (glsr()->id === filter_input(INPUT_GET, 'plugin')) {
344
            $postId = $this->getPostId();
345
            check_admin_referer("unapprove-review_{$postId}");
346
            $this->execute(new ToggleStatus(new Request([
347
                'post_id' => $postId,
348
                'status' => 'publish',
349
            ])));
350
            wp_safe_redirect(wp_get_referer());
351
            exit;
352
        }
353
    }
354
355
    /**
356
     * @action site-reviews/review/created
357
     */
358
    public function sendNotification(Review $review): void
359
    {
360
        if (defined('WP_IMPORTING')) {
361
            return;
362
        }
363
        if (empty(glsr_get_option('general.notifications'))) {
364
            return;
365
        }
366
        if (!in_array($review->status, ['pending', 'publish'])) {
367
            return; // this review is likely a draft made in the wp-admin
368
        }
369
        glsr(Queue::class)->async('queue/notification', ['review_id' => $review->ID]);
370
    }
371
372
    protected function bulkUpdateReview(Review $review, \WP_Post $oldPost): void
373
    {
374
        if ($assignedPostIds = filter_input(INPUT_GET, 'post_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY)) {
375
            glsr()->action('review/updated/post_ids', $review, Cast::toArray($assignedPostIds)); // trigger a recount of assigned posts
376 24
        }
377
        if ($assignedUserIds = filter_input(INPUT_GET, 'user_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY)) {
378 24
            glsr()->action('review/updated/user_ids', $review, Cast::toArray($assignedUserIds)); // trigger a recount of assigned users
379 24
        }
380 24
        $review = glsr(ReviewManager::class)->get($review->ID); // get a fresh copy of the review
381 24
        glsr()->action('review/updated', $review, [], $oldPost); // pass an empty array since review values are unchanged
382 2
    }
383 2
384 2
    protected function getAssignedDiffs(array $existing, array $replacements): array
385
    {
386 24
        sort($existing);
387 24
        sort($replacements);
388 24
        $new = $old = [];
389 24
        if ($existing !== $replacements) {
390
            $ignored = array_intersect($existing, $replacements);
391
            $new = array_diff($replacements, $ignored);
392
            $old = array_diff($existing, $ignored);
393
        }
394
        return [
395
            'new' => $new,
396
            'old' => $old,
397
        ];
398
    }
399
400
    protected function isEditedReview(\WP_Post $post, \WP_Post $oldPost): bool
401
    {
402
        if (glsr()->post_type !== $post->post_type) {
403
            return false;
404
        }
405
        if (in_array('trash', [$post->post_status, $oldPost->post_status])) {
406
            return false; // trashed posts cannot be edited
407
        }
408
        $input = 'edit' === glsr_current_screen()->base ? INPUT_GET : INPUT_POST;
409
        return filter_input($input, 'action') !== glsr()->prefix.'admin_action'; // abort if not a proper post update (i.e. approve/unapprove)
410
    }
411
412
    protected function refreshAvatar(array $data, Review $review): string
413
    {
414
        $avatarUrl = Cast::toString($data['avatar'] ?? '');
415
        if ($review->author === ($data['name'] ?? false)) {
416
            return $avatarUrl;
417
        }
418
        $url = preg_replace('/(.*)\/site-reviews\/avatars\/[\p{L&}]+\.svg$/u', '', $avatarUrl);
419
        if (empty($url)) { // only update the initials fallback avatar
420
            $review->set('author', $data['name'] ?? '');
421
            $avatarUrl = glsr(Avatar::class)->generateInitials($review);
422
        }
423
        return $avatarUrl;
424
    }
425
426
    protected function updateReview(Review $review, \WP_Post $oldPost): void
427
    {
428
        $customDefaults = array_fill_keys(array_keys($review->custom()->toArray()), '');
429
        $data = Helper::filterInputArray(glsr()->id);
430
        $data = wp_parse_args($data, $customDefaults); // this ensures we save all empty custom values
431
        if (Arr::get($data, 'is_editing_review')) {
432
            $data['avatar'] = $this->refreshAvatar($data, $review);
433
            $data['rating'] = Arr::get($data, 'rating');
434
            $data['terms'] = Arr::get($data, 'terms', 0);
435
            if (!glsr()->filterBool('verification/enabled', false)) {
436
                unset($data['is_verified']);
437
            }
438
            glsr(ReviewManager::class)->updateRating($review->ID, $data); // values are sanitized here
439
            glsr(ReviewManager::class)->updateCustom($review->ID, $data); // values are sanitized here
440
            $review = glsr(ReviewManager::class)->get($review->ID); // get a fresh copy of the review
441
        }
442
        $assignedPostIds = filter_input(INPUT_POST, 'post_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY);
443
        $assignedUserIds = filter_input(INPUT_POST, 'user_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY);
444
        glsr()->action('review/updated/post_ids', $review, Cast::toArray($assignedPostIds)); // trigger a recount of assigned posts
445
        glsr()->action('review/updated/user_ids', $review, Cast::toArray($assignedUserIds)); // trigger a recount of assigned users
446
        glsr(ResponseMetabox::class)->save($review);
447
        $review = glsr(ReviewManager::class)->get($review->ID); // get a fresh copy of the review
448
        glsr()->action('review/updated', $review, $data, $oldPost);
449
    }
450
}
451