PluginProbe
User Access Manager / 2.3.16
User Access Manager v2.3.16
2.3.20 2.3.19 2.3.18 2.3.17 2.3.16 2.3.15 2.3.14 2.3.13 trunk 0.6 0.6.1 0.6.2 0.7 0.7 Beta 0.7.0.1 0.8 0.8.0.1 0.8.0.2 0.9 0.9.1 0.9.1.1 0.9.1.2 0.9.1.3 0.9.1.4 1.0 All 136 releases
user-access-manager / src / Controller / Frontend / PostController.php

PostController.php in User Access Manager 2.3.16, at src/Controller/Frontend/PostController.php

585 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace UserAccessManager\Controller\Frontend;
6
7 use stdClass;
8 use UserAccessManager\Access\AccessHandler;
9 use UserAccessManager\Config\MainConfig;
10 use UserAccessManager\Config\WordpressConfig;
11 use UserAccessManager\Database\Database;
12 use UserAccessManager\Object\ObjectHandler;
13 use UserAccessManager\User\UserHandler;
14 use UserAccessManager\UserGroup\AbstractUserGroup;
15 use UserAccessManager\UserGroup\UserGroupHandler;
16 use UserAccessManager\UserGroup\UserGroupTypeException;
17 use UserAccessManager\Util\Util;
18 use UserAccessManager\Wrapper\Php;
19 use UserAccessManager\Wrapper\Wordpress;
20 use WeakMap;
21 use WP_Comment;
22 use WP_Error;
23 use WP_Hook;
24 use WP_Post;
25 use WP_Query;
26 use WP_REST_Request;
27 use WP_REST_Response;
28
29 class PostController extends ContentController
30 {
31 private const REST_OBJECT_ROUTE_PATTERN = '#^/[^/]+/v\d+/([^/]+)/(\d+)(?:/(\w+))?#';
32
33 private array $wordpressFilters = [];
34 private stdClass|array|null $cachedCounts = [];
35 private ?array $restBaseToPostTypeMap = null;
36
37 private WeakMap $posts;
38
39 public function __construct(
40 Php $php,
41 Wordpress $wordpress,
42 WordpressConfig $wordpressConfig,
43 MainConfig $mainConfig,
44 Util $util,
45 ObjectHandler $objectHandler,
46 UserHandler $userHandler,
47 UserGroupHandler $userGroupHandler,
48 AccessHandler $accessHandler,
49 private Database $database
50 ) {
51 parent::__construct(
52 $php,
53 $wordpress,
54 $wordpressConfig,
55 $mainConfig,
56 $util,
57 $objectHandler,
58 $userHandler,
59 $userGroupHandler,
60 $accessHandler
61 );
62
63 $this->posts = new WeakMap();
64 }
65
66 public function getWordpressFilters(): array
67 {
68 return $this->wordpressFilters;
69 }
70
71 private function filtersSuppressed(WP_Query $wpQuery): bool
72 {
73 return isset($wpQuery->query_vars['suppress_filters']) === true
74 && $wpQuery->query_vars['suppress_filters'] === true;
75 }
76
77 private function addExcludedPosts(mixed $postsNotIn, array $excludedPosts): array
78 {
79 return array_unique(array_merge((array) $postsNotIn, $excludedPosts));
80 }
81
82 /**
83 * @throws UserGroupTypeException
84 */
85 public function parseQuery(WP_Query $wpQuery): void
86 {
87 if ($this->filtersSuppressed($wpQuery) === true) {
88 $excludedPosts = $this->accessHandler->getExcludedPosts();
89
90 if ($excludedPosts !== []) {
91 $wpQuery->query_vars['post__not_in'] = $this->addExcludedPosts(
92 $wpQuery->query_vars['post__not_in'] ?? [],
93 $excludedPosts
94 );
95 }
96 }
97 }
98
99 /**
100 * @param WP_Hook[] $filters
101 */
102 private function extractOwnFilters(array $filters): bool
103 {
104 if (isset($filters['the_posts']->callbacks[10]) === true) {
105 foreach ($filters['the_posts']->callbacks[10] as $postFilter) {
106 if (is_array($postFilter['function']) === true
107 && $postFilter['function'][0] instanceof PostController
108 && $postFilter['function'][1] === 'showPosts'
109 ) {
110 $this->wordpressFilters['the_posts'] = $filters['the_posts'];
111 $filters['the_posts']->callbacks = [10 => [$postFilter]];
112 return true;
113 }
114 }
115 }
116
117 return false;
118 }
119
120 public function postsPreQuery(?array $posts, WP_Query $query): ?array
121 {
122 if ($this->filtersSuppressed($query) === true) {
123 $filters = $this->wordpress->getFilters();
124
125 // Only unset filter if the user access filter is active
126 if ($this->extractOwnFilters($filters) === true) {
127 $query->query_vars['suppress_filters'] = false;
128
129 if (isset($filters['posts_results']) === true) {
130 $this->wordpressFilters['posts_results'] = $filters['posts_results'];
131 unset($filters['posts_results']);
132 }
133
134 $this->wordpress->setFilters($filters);
135 }
136 }
137
138 return $posts;
139 }
140
141 private function restoreFilters(): void
142 {
143 if (count($this->wordpressFilters) > 0) {
144 $filters = $this->wordpress->getFilters();
145
146 foreach ($this->wordpressFilters as $filterKey => $filter) {
147 $filters[$filterKey] = $filter;
148 }
149
150 $this->wordpress->setFilters($filters);
151 $this->wordpressFilters = [];
152 }
153 }
154
155 private function getPost(mixed $post): bool|WP_Post
156 {
157 if ($post instanceof WP_post) {
158 return $post;
159 } elseif (is_int($post) === true) {
160 return $this->objectHandler->getPost($post);
161 } elseif (isset($post->ID) === true) {
162 return $this->objectHandler->getPost($post->ID);
163 }
164
165 return false;
166 }
167
168 private function processPostContent(WP_Post $post): string
169 {
170 $uamPostContent = htmlspecialchars_decode($this->mainConfig->getPostTypeContent($post->post_type));
171
172 if ($this->mainConfig->showPostTypeContentBeforeMore($post->post_type) === true
173 && preg_match('/<!--more(.*?)?-->/', $post->post_content, $matches)
174 ) {
175 $uamPostContent = explode($matches[0], $post->post_content)[0] . ' ' . $uamPostContent;
176 }
177
178 return stripslashes($uamPostContent);
179 }
180
181 /**
182 * @throws UserGroupTypeException
183 */
184 private function processPost(WP_Post $post): WP_Post|bool
185 {
186 $post->post_title .= $this->adminOutput($post->post_type, $post->ID);
187
188 if ($this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === false) {
189 if ($this->removePostFromList($post->post_type) === true) {
190 return false;
191 }
192
193 $post->post_content = $this->processPostContent($post);
194
195 if ($this->mainConfig->hidePostTypeTitle($post->post_type) === true) {
196 $post->post_title = $this->mainConfig->getPostTypeTitle($post->post_type);
197 }
198
199 if ($this->mainConfig->lockPostTypeComments($post->post_type) === true) {
200 $post->comment_status = 'close';
201 }
202 }
203
204 return $post;
205 }
206
207 /**
208 * @throws UserGroupTypeException
209 */
210 private function getProcessedPost(WP_Post $post): ?WP_Post
211 {
212 $post = $this->posts[$post] ??= $this->processPost($post);
213 return $post === false ? null : $post;
214 }
215
216 /**
217 * @throws UserGroupTypeException
218 */
219 private function filterRawPosts(array $rawPosts): array
220 {
221 $filteredPosts = [];
222
223 foreach ($rawPosts as $rawPost) {
224 $post = $this->getPost($rawPost);
225
226 if ($post !== false) {
227 $post = $this->getProcessedPost($post);
228
229 if ($post !== null) {
230 $filteredPosts[] = $post;
231 }
232 } else {
233 $filteredPosts[] = $rawPost;
234 }
235 }
236
237 return $filteredPosts;
238 }
239
240 /**
241 * @throws UserGroupTypeException
242 */
243 public function showPosts(?array $showPosts = []): ?array
244 {
245 if ($this->wordpress->isFeed() === false || $this->mainConfig->protectFeed() === true) {
246 $showPosts = $this->filterRawPosts((array) $showPosts);
247 }
248
249 $this->restoreFilters();
250
251 return $showPosts;
252 }
253
254 /**
255 * @param WP_Post[] $rawPages The pages.
256 * @throws UserGroupTypeException
257 */
258 public function showPages(array $rawPages = []): array
259 {
260 return $this->filterRawPosts($rawPages);
261 }
262
263 private function getRestAccessDeniedError(): WP_Error
264 {
265 return $this->wordpress->getWpError(
266 'uam_rest_access_denied',
267 TXT_UAM_REST_ACCESS_DENIED,
268 ['status' => $this->wordpress->isUserLoggedIn() === true ? 403 : 401]
269 );
270 }
271
272 private function setRestField(array &$data, string $field, string $value): void
273 {
274 if (array_key_exists($field, $data) === false) {
275 return;
276 }
277
278 if (is_array($data[$field]) === false) {
279 $data[$field] = $value;
280
281 return;
282 }
283
284 $restrictedValues = ['rendered' => $value, 'raw' => $value, 'protected' => false];
285
286 foreach ($restrictedValues as $key => $restrictedValue) {
287 if (array_key_exists($key, $data[$field]) === true) {
288 $data[$field][$key] = $restrictedValue;
289 }
290 }
291 }
292
293 /**
294 * @throws UserGroupTypeException
295 */
296 public function restrictRestResponse(mixed $response, mixed $post = null, mixed $request = null): mixed
297 {
298 if (($response instanceof WP_REST_Response) === false
299 || ($post instanceof WP_Post) === false
300 || $this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === true
301 ) {
302 return $response;
303 }
304
305 $restrictedContent = $this->processPostContent($post);
306 $data = (array) $response->get_data();
307
308 $this->setRestField($data, 'content', $restrictedContent);
309 $this->setRestField($data, 'excerpt', $restrictedContent);
310
311 if ($this->mainConfig->hidePostTypeTitle($post->post_type) === true) {
312 $this->setRestField($data, 'title', $this->mainConfig->getPostTypeTitle($post->post_type));
313 }
314
315 $response->set_data($data);
316
317 return $response;
318 }
319
320 /**
321 * @throws UserGroupTypeException
322 */
323 public function excludeRestrictedPostsFromRestQuery(array $queryArgs): array
324 {
325 $excludedPosts = $this->accessHandler->getExcludedPosts();
326
327 if ($excludedPosts !== []) {
328 $queryArgs['post__not_in'] = $this->addExcludedPosts($queryArgs['post__not_in'] ?? [], $excludedPosts);
329 }
330
331 return $queryArgs;
332 }
333
334 private function getRestBaseToPostTypeMap(): array
335 {
336 if ($this->restBaseToPostTypeMap !== null) {
337 return $this->restBaseToPostTypeMap;
338 }
339
340 $this->restBaseToPostTypeMap = [];
341
342 foreach ((array) $this->objectHandler->getPostTypes() as $postType) {
343 $restBase = $this->wordpress->getPostTypeObject($postType)?->rest_base;
344 $this->restBaseToPostTypeMap[empty($restBase) === true ? $postType : $restBase] = $postType;
345 }
346
347 return $this->restBaseToPostTypeMap;
348 }
349
350 /**
351 * @return null|array{type: string, id: int, addressesSubResource: bool}
352 */
353 private function getRestRouteTarget(WP_REST_Request $request): ?array
354 {
355 if (preg_match(self::REST_OBJECT_ROUTE_PATTERN, (string) $request->get_route(), $matches) !== 1) {
356 return null;
357 }
358
359 $postType = $this->getRestBaseToPostTypeMap()[$matches[1]] ?? null;
360
361 return $postType === null ? null : [
362 'type' => $postType,
363 'id' => (int) $matches[2],
364 'addressesSubResource' => ($matches[3] ?? '') !== ''
365 ];
366 }
367
368 private function isReadingRestRequest(WP_REST_Request $request): bool
369 {
370 return in_array(strtoupper((string) $request->get_method()), Wordpress::REST_READING_METHODS, true);
371 }
372
373 private function isEditingRestRoute(bool $addressesSubResource, WP_REST_Request $request): bool
374 {
375 return $this->isReadingRestRequest($request) === false || $addressesSubResource === true;
376 }
377
378 /**
379 * @throws UserGroupTypeException
380 */
381 private function hasRestRouteAccess(string $objectType, int $objectId, bool $isEditingRoute): bool
382 {
383 if ($isEditingRoute === true) {
384 return $this->accessHandler->checkObjectAccess($objectType, $objectId, true);
385 }
386
387 return $this->removePostFromList($objectType) === false
388 || $this->accessHandler->checkObjectAccess($objectType, $objectId);
389 }
390
391 /**
392 * @throws UserGroupTypeException
393 */
394 public function restrictRestRequest(mixed $result, mixed $server = null, mixed $request = null): mixed
395 {
396 if (($request instanceof WP_REST_Request) === false) {
397 return $result;
398 }
399
400 $routeTarget = $this->getRestRouteTarget($request);
401 $isEditingRoute = $routeTarget !== null
402 && $this->isEditingRestRoute($routeTarget['addressesSubResource'], $request);
403
404 $this->wordpress->setRestRequestContext(
405 $isEditingRoute === true || $request->get_param('context') === 'edit'
406 );
407
408 if ($result !== null || $routeTarget === null) {
409 return $result;
410 }
411
412 ['type' => $type, 'id' => $id] = $routeTarget;
413
414 return $this->hasRestRouteAccess($type, $id, $isEditingRoute) === true ?
415 $result : $this->getRestAccessDeniedError();
416 }
417
418 /**
419 * @throws UserGroupTypeException
420 */
421 public function getAttachedFile(string $file, int|string|null $attachmentId): bool|string
422 {
423 $isImage = (bool) preg_match('/(?i)\.(jpg|jpeg|jpe|png|gif)$/', $file);
424
425 if ($isImage === false && $this->mainConfig->lockFile() === true) {
426 $hasAccess = $this->accessHandler->checkObjectAccess(ObjectHandler::ATTACHMENT_OBJECT_TYPE, $attachmentId);
427 return ($hasAccess === true) ? $file : false;
428 }
429
430 return $file;
431 }
432
433 /**
434 * @throws UserGroupTypeException
435 */
436 private function addQueryExcludedPostFilter(string $query, string $table): string
437 {
438 $excludedPosts = $this->accessHandler->getExcludedPosts();
439
440 if ($excludedPosts !== []) {
441 $excludedPostsStr = implode(', ', array_map('intval', $excludedPosts));
442 $query .= " AND $table.ID NOT IN ($excludedPostsStr) ";
443 }
444
445 return $query;
446 }
447
448 /**
449 * @throws UserGroupTypeException
450 */
451 public function showPostSql(string $query): string
452 {
453 return $this->addQueryExcludedPostFilter($query, $this->database->getPostsTable());
454 }
455
456 /**
457 * @throws UserGroupTypeException
458 */
459 public function showNextPreviousPost(string $query): string
460 {
461 return $this->addQueryExcludedPostFilter($query, 'p');
462 }
463
464 private function getPostCountQuery(array $excludedPosts, string $type, string $perm): string
465 {
466 $excludedPosts = implode(', ', array_map('intval', $excludedPosts));
467 $query = "SELECT post_status, COUNT(*) AS num_posts
468 FROM {$this->database->getPostsTable()}
469 WHERE post_type = %s
470 AND ID NOT IN ($excludedPosts)";
471
472 if ('readable' === $perm
473 && $this->wordpress->isUserLoggedIn() === true
474 && $this->wordpress->currentUserCan(
475 $this->wordpress->getPostTypeObject($type)->cap->read_private_posts
476 ) === false
477 ) {
478 $query .= $this->database->prepare(
479 ' AND (post_status != \'private\' OR (post_author = %d AND post_status = \'private\'))',
480 $this->wordpress->getCurrentUser()->ID
481 );
482 }
483
484 $query .= ' GROUP BY post_status';
485 return $query;
486 }
487
488 /**
489 * @throws UserGroupTypeException
490 */
491 public function showPostCount(stdClass $counts, string $type, string $perm): stdClass
492 {
493 if (isset($this->cachedCounts[$type]) === false) {
494 $excludedPosts = $this->accessHandler->getExcludedPosts();
495
496 if ($excludedPosts !== []) {
497 $query = $this->getPostCountQuery($excludedPosts, $type, $perm);
498 $results = (array) $this->database->getResults(
499 $this->database->prepare($query, $type),
500 ARRAY_A
501 );
502
503 foreach ($results as $result) {
504 if (isset($counts->{$result['post_status']})) {
505 $counts->{$result['post_status']} = $result['num_posts'];
506 }
507 }
508 }
509
510 $this->cachedCounts[$type] = $counts;
511 }
512
513 return $this->cachedCounts[$type];
514 }
515
516 private function hidePostComment(string $postType): bool
517 {
518 return $this->mainConfig->lockPostTypeComments($postType) === true
519 || $this->mainConfig->hidePostType($postType) === true
520 || $this->wordpressConfig->atAdminPanel() === true;
521 }
522
523 /**
524 * @param WP_Comment[] $comments The comments.
525 * @throws UserGroupTypeException
526 */
527 public function showComment(array $comments = []): array
528 {
529 $showComments = [];
530
531 foreach ($comments as $comment) {
532 $post = $this->objectHandler->getPost($comment->comment_post_ID);
533
534 if ($post !== false
535 && $this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === false
536 ) {
537 if ($this->hidePostComment($post->post_type)) {
538 continue;
539 }
540
541 if ($this->mainConfig->hidePostTypeComments($post->post_type) === true) {
542 $comment->comment_content = $this->mainConfig->getPostTypeCommentContent($post->post_type);
543 }
544 }
545
546 $showComments[] = $comment;
547 }
548
549 return $showComments;
550 }
551
552 /**
553 * @throws UserGroupTypeException
554 */
555 public function showEditLink(?string $link, int|string|null $postId): string
556 {
557 if ($this->mainConfig->hideEditLinkOnNoAccess() === true
558 && $this->accessHandler->checkObjectAccess(ObjectHandler::GENERAL_POST_OBJECT_TYPE, $postId, true) === false
559 ) {
560 $link = '';
561 }
562
563 if ($this->mainConfig->showAssignedGroups() === true) {
564 $userGroups = $this->userGroupHandler->getFilteredUserGroupsForObject(
565 ObjectHandler::GENERAL_POST_OBJECT_TYPE,
566 $postId
567 );
568
569 if (count($userGroups) > 0) {
570 $escapedGroups = array_map(
571 function (AbstractUserGroup $group) {
572 return htmlentities($group->getName());
573 },
574 $userGroups
575 );
576
577 $link .= $link !== '' ? ' | ' : ' ';
578 $link .= TXT_UAM_ASSIGNED_GROUPS . ': ' . implode(', ', $escapedGroups);
579 }
580 }
581
582 return (string) $link;
583 }
584 }
585