PluginProbe
User Access Manager / 2.3.15
User Access Manager v2.3.15
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.15, at src/Controller/Frontend/PostController.php

580 lines 17.7 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 isSingleObjectRestRequest(mixed $request, WP_Post $post): bool
273 {
274 $routeId = $request instanceof WP_REST_Request ? ($request->get_url_params()['id'] ?? null) : null;
275
276 return $routeId !== null && (int) $routeId === (int) $post->ID;
277 }
278
279 private function setRestField(array &$data, string $field, string $value): void
280 {
281 if (array_key_exists($field, $data) === false) {
282 return;
283 }
284
285 if (is_array($data[$field]) === false) {
286 $data[$field] = $value;
287
288 return;
289 }
290
291 $restrictedValues = ['rendered' => $value, 'raw' => $value, 'protected' => false];
292
293 foreach ($restrictedValues as $key => $restrictedValue) {
294 if (array_key_exists($key, $data[$field]) === true) {
295 $data[$field][$key] = $restrictedValue;
296 }
297 }
298 }
299
300 /**
301 * @throws UserGroupTypeException
302 */
303 public function restrictRestResponse(mixed $response, mixed $post = null, mixed $request = null): mixed
304 {
305 if (($response instanceof WP_REST_Response) === false
306 || ($post instanceof WP_Post) === false
307 || $this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === true
308 ) {
309 return $response;
310 }
311
312 if ($this->removePostFromList($post->post_type) === true
313 && $this->isSingleObjectRestRequest($request, $post) === true
314 ) {
315 return $this->getRestAccessDeniedError();
316 }
317
318 $restrictedContent = $this->processPostContent($post);
319 $data = (array) $response->get_data();
320
321 $this->setRestField($data, 'content', $restrictedContent);
322 $this->setRestField($data, 'excerpt', $restrictedContent);
323
324 if ($this->mainConfig->hidePostTypeTitle($post->post_type) === true) {
325 $this->setRestField($data, 'title', $this->mainConfig->getPostTypeTitle($post->post_type));
326 }
327
328 $response->set_data($data);
329
330 return $response;
331 }
332
333 /**
334 * @throws UserGroupTypeException
335 */
336 public function excludeRestrictedPostsFromRestQuery(array $queryArgs): array
337 {
338 $excludedPosts = $this->accessHandler->getExcludedPosts();
339
340 if ($excludedPosts !== []) {
341 $queryArgs['post__not_in'] = $this->addExcludedPosts($queryArgs['post__not_in'] ?? [], $excludedPosts);
342 }
343
344 return $queryArgs;
345 }
346
347 private function getRestBaseToPostTypeMap(): array
348 {
349 if ($this->restBaseToPostTypeMap !== null) {
350 return $this->restBaseToPostTypeMap;
351 }
352
353 $this->restBaseToPostTypeMap = [];
354
355 foreach ((array) $this->objectHandler->getPostTypes() as $postType) {
356 $restBase = $this->wordpress->getPostTypeObject($postType)?->rest_base;
357 $this->restBaseToPostTypeMap[empty($restBase) === true ? $postType : $restBase] = $postType;
358 }
359
360 return $this->restBaseToPostTypeMap;
361 }
362
363 /**
364 * @return array{0: string, 1: int}|null
365 */
366 private function getGuardedRestRouteTarget(WP_REST_Request $request): ?array
367 {
368 if (preg_match(self::REST_OBJECT_ROUTE_PATTERN, (string) $request->get_route(), $matches) !== 1) {
369 return null;
370 }
371
372 $isSubResourceRoute = ($matches[3] ?? '') !== '';
373 $isReadingRequest = in_array(
374 strtoupper((string) $request->get_method()),
375 Wordpress::REST_READING_METHODS,
376 true
377 );
378
379 if ($isReadingRequest === true && $isSubResourceRoute === false) {
380 return null;
381 }
382
383 $postType = $this->getRestBaseToPostTypeMap()[$matches[1]] ?? null;
384
385 return $postType === null ? null : [$postType, (int) $matches[2]];
386 }
387
388 /**
389 * @throws UserGroupTypeException
390 */
391 public function restrictRestRequest(mixed $result, mixed $server = null, mixed $request = null): mixed
392 {
393 if (($request instanceof WP_REST_Request) === false) {
394 return $result;
395 }
396
397 $routeTarget = $this->getGuardedRestRouteTarget($request);
398
399 $this->wordpress->setRestRequestContext(
400 $routeTarget !== null || $request->get_param('context') === 'edit'
401 );
402
403 if ($result !== null
404 || $routeTarget === null
405 || $this->accessHandler->checkObjectAccess($routeTarget[0], $routeTarget[1], true) === true
406 ) {
407 return $result;
408 }
409
410 return $this->getRestAccessDeniedError();
411 }
412
413 /**
414 * @throws UserGroupTypeException
415 */
416 public function getAttachedFile(string $file, int|string|null $attachmentId): bool|string
417 {
418 $isImage = (bool) preg_match('/(?i)\.(jpg|jpeg|jpe|png|gif)$/', $file);
419
420 if ($isImage === false && $this->mainConfig->lockFile() === true) {
421 $hasAccess = $this->accessHandler->checkObjectAccess(ObjectHandler::ATTACHMENT_OBJECT_TYPE, $attachmentId);
422 return ($hasAccess === true) ? $file : false;
423 }
424
425 return $file;
426 }
427
428 /**
429 * @throws UserGroupTypeException
430 */
431 private function addQueryExcludedPostFilter(string $query, string $table): string
432 {
433 $excludedPosts = $this->accessHandler->getExcludedPosts();
434
435 if ($excludedPosts !== []) {
436 $excludedPostsStr = implode(', ', array_map('intval', $excludedPosts));
437 $query .= " AND $table.ID NOT IN ($excludedPostsStr) ";
438 }
439
440 return $query;
441 }
442
443 /**
444 * @throws UserGroupTypeException
445 */
446 public function showPostSql(string $query): string
447 {
448 return $this->addQueryExcludedPostFilter($query, $this->database->getPostsTable());
449 }
450
451 /**
452 * @throws UserGroupTypeException
453 */
454 public function showNextPreviousPost(string $query): string
455 {
456 return $this->addQueryExcludedPostFilter($query, 'p');
457 }
458
459 private function getPostCountQuery(array $excludedPosts, string $type, string $perm): string
460 {
461 $excludedPosts = implode(', ', array_map('intval', $excludedPosts));
462 $query = "SELECT post_status, COUNT(*) AS num_posts
463 FROM {$this->database->getPostsTable()}
464 WHERE post_type = %s
465 AND ID NOT IN ($excludedPosts)";
466
467 if ('readable' === $perm
468 && $this->wordpress->isUserLoggedIn() === true
469 && $this->wordpress->currentUserCan(
470 $this->wordpress->getPostTypeObject($type)->cap->read_private_posts
471 ) === false
472 ) {
473 $query .= $this->database->prepare(
474 ' AND (post_status != \'private\' OR (post_author = %d AND post_status = \'private\'))',
475 $this->wordpress->getCurrentUser()->ID
476 );
477 }
478
479 $query .= ' GROUP BY post_status';
480 return $query;
481 }
482
483 /**
484 * @throws UserGroupTypeException
485 */
486 public function showPostCount(stdClass $counts, string $type, string $perm): stdClass
487 {
488 if (isset($this->cachedCounts[$type]) === false) {
489 $excludedPosts = $this->accessHandler->getExcludedPosts();
490
491 if ($excludedPosts !== []) {
492 $query = $this->getPostCountQuery($excludedPosts, $type, $perm);
493 $results = (array) $this->database->getResults(
494 $this->database->prepare($query, $type),
495 ARRAY_A
496 );
497
498 foreach ($results as $result) {
499 if (isset($counts->{$result['post_status']})) {
500 $counts->{$result['post_status']} = $result['num_posts'];
501 }
502 }
503 }
504
505 $this->cachedCounts[$type] = $counts;
506 }
507
508 return $this->cachedCounts[$type];
509 }
510
511 private function hidePostComment(string $postType): bool
512 {
513 return $this->mainConfig->lockPostTypeComments($postType) === true
514 || $this->mainConfig->hidePostType($postType) === true
515 || $this->wordpressConfig->atAdminPanel() === true;
516 }
517
518 /**
519 * @param WP_Comment[] $comments The comments.
520 * @throws UserGroupTypeException
521 */
522 public function showComment(array $comments = []): array
523 {
524 $showComments = [];
525
526 foreach ($comments as $comment) {
527 $post = $this->objectHandler->getPost($comment->comment_post_ID);
528
529 if ($post !== false
530 && $this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === false
531 ) {
532 if ($this->hidePostComment($post->post_type)) {
533 continue;
534 }
535
536 if ($this->mainConfig->hidePostTypeComments($post->post_type) === true) {
537 $comment->comment_content = $this->mainConfig->getPostTypeCommentContent($post->post_type);
538 }
539 }
540
541 $showComments[] = $comment;
542 }
543
544 return $showComments;
545 }
546
547 /**
548 * @throws UserGroupTypeException
549 */
550 public function showEditLink(?string $link, int|string|null $postId): string
551 {
552 if ($this->mainConfig->hideEditLinkOnNoAccess() === true
553 && $this->accessHandler->checkObjectAccess(ObjectHandler::GENERAL_POST_OBJECT_TYPE, $postId, true) === false
554 ) {
555 $link = '';
556 }
557
558 if ($this->mainConfig->showAssignedGroups() === true) {
559 $userGroups = $this->userGroupHandler->getFilteredUserGroupsForObject(
560 ObjectHandler::GENERAL_POST_OBJECT_TYPE,
561 $postId
562 );
563
564 if (count($userGroups) > 0) {
565 $escapedGroups = array_map(
566 function (AbstractUserGroup $group) {
567 return htmlentities($group->getName());
568 },
569 $userGroups
570 );
571
572 $link .= $link !== '' ? ' | ' : ' ';
573 $link .= TXT_UAM_ASSIGNED_GROUPS . ': ' . implode(', ', $escapedGroups);
574 }
575 }
576
577 return (string) $link;
578 }
579 }
580