ReviewController::filterPostsToCacheReviews()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 7
ccs 0
cts 5
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 6
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 28
    public function filterReviewPostData(array $data, array $sanitized): array
51
    {
52 28
        if (empty($sanitized['ID']) || empty($sanitized['action']) || glsr()->post_type !== Arr::get($sanitized, 'post_type')) {
53 28
            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
69
    /**
70
     * @filter site-reviews/rendered/template/review
71
     */
72 8
    public function filterReviewTemplate(string $template, array $data): string
73
    {
74 8
        $search = 'id="review-';
75 8
        $dataType = Arr::get($data, 'review.type', 'local');
76 8
        $replace = sprintf('data-type="%s" %s', $dataType, $search);
77 8
        if (Arr::get($data, 'review.is_pinned')) {
78
            $replace = 'data-pinned="1" '.$replace;
79
        }
80 8
        if (Arr::get($data, 'review.is_verified')) {
81
            $replace = 'data-verified="1" '.$replace;
82
        }
83 8
        return str_replace($search, $replace, $template);
84
    }
85
86
    /**
87
     * @filter site-reviews/query/sql/clause/operator
88
     */
89 8
    public function filterSqlClauseOperator(string $operator): string
90
    {
91 8
        $operators = ['loose' => 'OR', 'strict' => 'AND'];
92 8
        return Arr::get($operators, glsr_get_option('reviews.assignment', 'strict', 'string'), $operator);
93
    }
94
95
    /**
96
     * @filter site-reviews/review/build/after
97
     */
98 8
    public function filterTemplateTags(array $tags, Review $review, ReviewHtml $reviewHtml): array
99
    {
100 8
        $tags['assigned_links'] = $reviewHtml->buildTemplateTag($review, 'assigned_links', $review->assigned_posts);
101 8
        return $tags;
102
    }
103
104
    /**
105
     * Triggered after one or more categories are added or removed from a review.
106
     *
107
     * @action set_object_terms
108
     */
109 28
    public function onAfterChangeAssignedTerms(
110
        int $postId,
111
        array $terms,
112
        array $newTTIds,
113
        string $taxonomy,
114
        bool $append,
115
        array $oldTTIds
116
    ): void {
117 28
        if (Review::isReview($postId)) {
118 24
            $review = glsr(ReviewManager::class)->get($postId);
119 24
            $diff = $this->getAssignedDiffs($oldTTIds, $newTTIds);
120 24
            $this->execute(new UnassignTerms($review, $diff['old']));
121 24
            $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 28
    public function onAfterChangeStatus(string $new, string $old, ?\WP_Post $post): void
131
    {
132 28
        if (is_null($post)) {
133 28
            return; // This should never happen, but some plugins are bad actors so...
134
        }
135
        if (in_array($old, ['new', $new])) {
136
            return;
137
        }
138
        if (Review::isReview($post)) {
139
            $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
            if ('publish' === $new) {
152
                glsr()->action('review/approved', $review, $old, $new);
153
            } elseif ('pending' === $new) {
154
                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 24
     *
209
     * @action site-reviews/review/created
210 24
     */
211 24
    public function onCreatedReview(Review $review, CreateReview $command): void
212
    {
213
        $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 24
     *
220
     * @action site-reviews/review/create
221 24
     */
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
        $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 24
            wp_delete_post($postId, true); // remove post as review was not created
237 24
            return;
238
        }
239
        $termIds = wp_set_object_terms($postId, $values->assigned_terms, glsr()->taxonomy);
240 24
        if (is_wp_error($termIds)) {
241
            glsr_log()->error($termIds->get_error_message());
242
        }
243
        $excluded = Cast::toArray($command->request()->excluded);
0 ignored issues
show
Bug introduced by
The property excluded does not seem to exist on GeminiLabs\SiteReviews\Request.
Loading history...
244 24
        if (!empty($excluded)) { // save the fields hidden in the review form
245
            glsr(Database::class)->metaSet($postId, 'excluded', $excluded);
246
        }
247
        if (!empty($values->response)) { // save the response if one is provided
248
            glsr(Database::class)->metaSet($postId, 'response', $values->response);
249
            glsr(Database::class)->metaSet($postId, 'response_by', $values->response_by); // @phpstan-ignore-line
250
        }
251
        foreach ($values->custom as $key => $value) {
252
            glsr(Database::class)->metaSet($postId, "custom_{$key}", $value);
253
        }
254
    }
255
256
    /**
257
     * Triggered when a review or other post type is deleted and the posts table uses the MyISAM engine.
258
     *
259
     * @action deleted_post
260
     */
261
    public function onDeletePost(int $postId, \WP_Post $post): void
262
    {
263
        if (glsr()->post_type === $post->post_type) {
264
            $this->onDeleteReview($postId);
265
            return;
266
        }
267
        $reviewIds = glsr(Query::class)->reviewIds([
268
            'assigned_posts' => $postId,
269
            'per_page' => -1,
270
            'status' => 'all',
271
        ]);
272
        if (glsr(Database::class)->delete('assigned_posts', ['post_id' => $postId])) {
273
            array_walk($reviewIds, function ($reviewId) {
274
                glsr(Cache::class)->delete($reviewId, 'reviews');
275
            });
276
        }
277
    }
278
279
    /**
280
     * Triggered when a review is deleted and the posts table uses the MyISAM engine.
281
     *
282
     * @see $this->onDeletePost()
283
     */
284
    public function onDeleteReview(int $reviewId): void
285
    {
286
        glsr(ReviewManager::class)->deleteRating($reviewId);
287
    }
288
289
    /**
290
     * Triggered when a user is deleted and the users table uses the MyISAM engine.
291
     *
292
     * @action deleted_user
293
     */
294
    public function onDeleteUser(int $userId = 0): void
295
    {
296
        $reviewIds = glsr(Query::class)->reviewIds([
297
            'assigned_users' => $userId,
298
            'per_page' => -1,
299
            'status' => 'all',
300
        ]);
301
        if (glsr(Database::class)->delete('assigned_users', ['user_id' => $userId])) {
302
            array_walk($reviewIds, function ($reviewId) {
303
                glsr(Cache::class)->delete($reviewId, 'reviews');
304
            });
305
        }
306
    }
307
308
    /**
309
     * Triggered when a review is edited or trashed.
310
     * It's unnecessary to trigger a term recount as this is done by the set_object_terms hook
311
     * We need to use "post_updated" to support revisions (vs "save_post").
312
     *
313
     * @action post_updated
314
     */
315
    public function onEditReview(int $postId, ?\WP_Post $post, ?\WP_Post $oldPost): void
316
    {
317
        if (is_null($post) || is_null($oldPost)) {
318
            return; // This should never happen, but some plugins are bad actors so...
319
        }
320
        if (!glsr()->can('edit_posts') || !$this->isEditedReview($post, $oldPost)) {
321
            return;
322
        }
323
        if (glsr()->id === filter_input(INPUT_GET, 'plugin')) {
324
            return; // the fallback approve/unapprove action is being run
325
        }
326
        if (!in_array(glsr_current_screen()->base, ['edit', 'post'])) {
327
            return; // only trigger this action from the Site Reviews edit/post screens
328
        }
329
        $review = glsr(ReviewManager::class)->get($postId);
330
        if ('edit' === glsr_current_screen()->base) {
331
            $this->bulkUpdateReview($review, $oldPost);
332
        } else {
333
            $this->updateReview($review, $oldPost);
334
        }
335
    }
336
337
    /**
338
     * Fallback action if ajax is not working for any reason.
339
     *
340
     * @action admin_action_unapprove
341
     */
342
    public function onUnapprove(): void
343
    {
344
        if (glsr()->id === filter_input(INPUT_GET, 'plugin')) {
345
            $postId = $this->getPostId();
346
            check_admin_referer("unapprove-review_{$postId}");
347
            $this->execute(new ToggleStatus(new Request([
348
                'post_id' => $postId,
349 24
                'status' => 'publish',
350
            ])));
351 24
            wp_safe_redirect(wp_get_referer());
352
            exit;
353
        }
354 24
    }
355 24
356
    /**
357
     * @action site-reviews/review/created
358
     */
359
    public function sendNotification(Review $review): void
360
    {
361
        if (defined('WP_IMPORTING')) {
362
            return;
363
        }
364
        if (empty(glsr_get_option('general.notifications'))) {
365
            return;
366
        }
367
        if (!in_array($review->status, ['pending', 'publish'])) {
368
            return; // this review is likely a draft made in the wp-admin
369
        }
370
        glsr(Queue::class)->async('queue/notification', ['review_id' => $review->ID]);
371
    }
372
373
    protected function bulkUpdateReview(Review $review, \WP_Post $oldPost): void
374
    {
375 24
        if ($assignedPostIds = filter_input(INPUT_GET, 'post_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY)) {
376
            glsr()->action('review/updated/post_ids', $review, Cast::toArray($assignedPostIds)); // trigger a recount of assigned posts
377 24
        }
378 24
        if ($assignedUserIds = filter_input(INPUT_GET, 'user_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY)) {
379 24
            glsr()->action('review/updated/user_ids', $review, Cast::toArray($assignedUserIds)); // trigger a recount of assigned users
380 24
        }
381 2
        $review = glsr(ReviewManager::class)->get($review->ID); // get a fresh copy of the review
382 2
        glsr()->action('review/updated', $review, [], $oldPost); // pass an empty array since review values are unchanged
383 2
    }
384
385 24
    protected function getAssignedDiffs(array $existing, array $replacements): array
386 24
    {
387 24
        sort($existing);
388 24
        sort($replacements);
389
        $new = $old = [];
390
        if ($existing !== $replacements) {
391
            $ignored = array_intersect($existing, $replacements);
392
            $new = array_diff($replacements, $ignored);
393
            $old = array_diff($existing, $ignored);
394
        }
395
        return [
396
            'new' => $new,
397
            'old' => $old,
398
        ];
399
    }
400
401
    protected function isEditedReview(\WP_Post $post, \WP_Post $oldPost): bool
402
    {
403
        if (glsr()->post_type !== $post->post_type) {
404
            return false;
405
        }
406
        if (in_array('trash', [$post->post_status, $oldPost->post_status])) {
407
            return false; // trashed posts cannot be edited
408
        }
409
        $input = 'edit' === glsr_current_screen()->base ? INPUT_GET : INPUT_POST;
410
        return filter_input($input, 'action') !== glsr()->prefix.'admin_action'; // abort if not a proper post update (i.e. approve/unapprove)
411
    }
412
413
    protected function refreshAvatar(array $data, Review $review): string
414
    {
415
        $avatarUrl = Cast::toString($data['avatar'] ?? '');
416
        if ($review->author === ($data['name'] ?? false)) {
417
            return $avatarUrl;
418
        }
419
        $url = preg_replace('/(.*)\/site-reviews\/avatars\/[\p{L&}]+\.svg$/u', '', $avatarUrl);
420
        if (empty($url)) { // only update the initials fallback avatar
421
            $review->set('author', $data['name'] ?? '');
422
            $avatarUrl = glsr(Avatar::class)->generateInitials($review);
423
        }
424
        return $avatarUrl;
425
    }
426
427
    /**
428
     * This is run after editing a review in the admin.
429
     */
430
    protected function updateReview(Review $review, \WP_Post $oldPost): void
431
    {
432
        $customDefaults = array_fill_keys(array_keys($review->custom()->toArray()), '');
433
        $data = Helper::filterInputArray(glsr()->id);
434
        $data = wp_parse_args($data, $customDefaults); // this ensures we save all empty custom values
435
        if (Arr::get($data, 'is_editing_review')) {
436
            $data['avatar'] = $this->refreshAvatar($data, $review);
437
            $data['rating'] ??= '';
438
            $data['terms'] ??= 0;
439
        }
440
        if (Arr::getAs('bool', $data, 'is_pinned') === $review->is_pinned) {
441
            unset($data['is_pinned']);
442
        }
443
        if (Arr::getAs('bool', $data, 'is_verified') === $review->is_verified || !glsr()->filterBool('verification/enabled', false)) {
444
            unset($data['is_verified']);
445
        }
446
        if (!empty($data)) {
447
            glsr(ReviewManager::class)->updateCustom($review->ID, $data); // values are sanitized here
448
            glsr(ReviewManager::class)->updateRating($review->ID, $data); // values are sanitized here
449
            $review = glsr(ReviewManager::class)->get($review->ID); // get a fresh copy of the review
450
        }
451
        $assignedPostIds = filter_input(INPUT_POST, 'post_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY);
452
        $assignedUserIds = filter_input(INPUT_POST, 'user_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_FORCE_ARRAY);
453
        glsr()->action('review/updated/post_ids', $review, Cast::toArray($assignedPostIds)); // trigger a recount of assigned posts
454
        glsr()->action('review/updated/user_ids', $review, Cast::toArray($assignedUserIds)); // trigger a recount of assigned users
455
        glsr(ResponseMetabox::class)->save($review);
456
        $review = glsr(ReviewManager::class)->get($review->ID); // get a fresh copy of the review
457
        glsr()->action('review/updated', $review, $data, $oldPost);
458
    }
459
}
460