This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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
![]() |
|||
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 |