PluginProbe
User Access Manager / trunk
User Access Manager vtrunk
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 / Content / PostController.php

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

591 lines 18.0 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\Content;
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 array $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 if ($this->extractOwnFilters($filters) === true) {
126 $query->query_vars['suppress_filters'] = false;
127
128 if (isset($filters['posts_results']) === true) {
129 $this->wordpressFilters['posts_results'] = $filters['posts_results'];
130 unset($filters['posts_results']);
131 }
132
133 $this->wordpress->setFilters($filters);
134 }
135 }
136
137 return $posts;
138 }
139
140 private function restoreFilters(): void
141 {
142 if (count($this->wordpressFilters) > 0) {
143 $filters = $this->wordpress->getFilters();
144
145 foreach ($this->wordpressFilters as $filterKey => $filter) {
146 $filters[$filterKey] = $filter;
147 }
148
149 $this->wordpress->setFilters($filters);
150 $this->wordpressFilters = [];
151 }
152 }
153
154 private function getPost(mixed $post): bool|WP_Post
155 {
156 if ($post instanceof WP_Post) {
157 return $post;
158 } elseif (is_int($post) === true) {
159 return $this->objectHandler->getPost($post);
160 } elseif (isset($post->ID) === true) {
161 return $this->objectHandler->getPost($post->ID);
162 }
163
164 return false;
165 }
166
167 private function processPostContent(WP_Post $post): string
168 {
169 $uamPostContent = htmlspecialchars_decode($this->mainConfig->getPostTypeContent($post->post_type));
170
171 if ($this->mainConfig->showPostTypeContentBeforeMore($post->post_type) === true
172 && preg_match('/<!--more(.*?)?-->/', $post->post_content, $matches)
173 ) {
174 $uamPostContent = explode($matches[0], $post->post_content)[0] . ' ' . $uamPostContent;
175 }
176
177 return stripslashes($uamPostContent);
178 }
179
180 /**
181 * @throws UserGroupTypeException
182 */
183 private function processPost(WP_Post $post): WP_Post|bool
184 {
185 $post->post_title .= $this->adminOutput($post->post_type, $post->ID);
186
187 if ($this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === false) {
188 if ($this->removePostFromList($post->post_type) === true) {
189 return false;
190 }
191
192 $post->post_content = $this->processPostContent($post);
193
194 if ($this->mainConfig->hidePostTypeTitle($post->post_type) === true) {
195 $post->post_title = $this->mainConfig->getPostTypeTitle($post->post_type);
196 }
197
198 if ($this->mainConfig->lockPostTypeComments($post->post_type) === true) {
199 $post->comment_status = 'close';
200 }
201 }
202
203 return $post;
204 }
205
206 /**
207 * @throws UserGroupTypeException
208 */
209 private function getProcessedPost(WP_Post $post): ?WP_Post
210 {
211 $post = $this->posts[$post] ??= $this->processPost($post);
212 return $post === false ? null : $post;
213 }
214
215 /**
216 * @throws UserGroupTypeException
217 */
218 private function filterRawPosts(array $rawPosts): array
219 {
220 $filteredPosts = [];
221
222 foreach ($rawPosts as $rawPost) {
223 $post = $this->getPost($rawPost);
224
225 if ($post !== false) {
226 $post = $this->getProcessedPost($post);
227
228 if ($post !== null) {
229 $filteredPosts[] = $post;
230 }
231 } else {
232 $filteredPosts[] = $rawPost;
233 }
234 }
235
236 return $filteredPosts;
237 }
238
239 /**
240 * @throws UserGroupTypeException
241 */
242 public function showPosts(?array $showPosts = []): ?array
243 {
244 if ($this->wordpress->isFeed() === false || $this->mainConfig->protectFeed() === true) {
245 $showPosts = $this->filterRawPosts((array) $showPosts);
246 }
247
248 $this->restoreFilters();
249
250 return $showPosts;
251 }
252
253 /**
254 * @param WP_Post[] $rawPages The pages.
255 * @throws UserGroupTypeException
256 */
257 public function showPages(array $rawPages = []): array
258 {
259 return $this->filterRawPosts($rawPages);
260 }
261
262 private function getRestAccessDeniedError(): WP_Error
263 {
264 return $this->wordpress->getWpError(
265 'uam_rest_access_denied',
266 TXT_UAM_REST_ACCESS_DENIED,
267 ['status' => $this->wordpress->isUserLoggedIn() === true ? 403 : 401]
268 );
269 }
270
271 private function setRestField(array &$data, string $field, string $value): void
272 {
273 if (array_key_exists($field, $data) === false) {
274 return;
275 }
276
277 if (is_array($data[$field]) === false) {
278 $data[$field] = $value;
279
280 return;
281 }
282
283 $restrictedValues = ['rendered' => $value, 'raw' => $value, 'protected' => false];
284
285 foreach ($restrictedValues as $key => $restrictedValue) {
286 if (array_key_exists($key, $data[$field]) === true) {
287 $data[$field][$key] = $restrictedValue;
288 }
289 }
290 }
291
292 /**
293 * @throws UserGroupTypeException
294 */
295 public function restrictRestResponse(mixed $response, mixed $post = null, mixed $request = null): mixed
296 {
297 if (($response instanceof WP_REST_Response) === false
298 || ($post instanceof WP_Post) === false
299 || $this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === true
300 ) {
301 return $response;
302 }
303
304 $restrictedContent = $this->processPostContent($post);
305 $data = (array) $response->get_data();
306
307 $this->setRestField($data, 'content', $restrictedContent);
308 $this->setRestField($data, 'excerpt', $restrictedContent);
309
310 if ($this->mainConfig->hidePostTypeTitle($post->post_type) === true) {
311 $this->setRestField($data, 'title', $this->mainConfig->getPostTypeTitle($post->post_type));
312 }
313
314 $response->set_data($data);
315
316 return $response;
317 }
318
319 /**
320 * @throws UserGroupTypeException
321 */
322 public function excludeRestrictedPostsFromRestQuery(array $queryArgs): array
323 {
324 $excludedPosts = $this->accessHandler->getExcludedPosts();
325
326 if ($excludedPosts !== []) {
327 $queryArgs['post__not_in'] = $this->addExcludedPosts($queryArgs['post__not_in'] ?? [], $excludedPosts);
328 }
329
330 return $queryArgs;
331 }
332
333 private function getRestBaseToPostTypeMap(): array
334 {
335 if ($this->restBaseToPostTypeMap !== null) {
336 return $this->restBaseToPostTypeMap;
337 }
338
339 $this->restBaseToPostTypeMap = [];
340
341 foreach ((array) $this->objectHandler->getPostTypes() as $postType) {
342 $restBase = $this->wordpress->getPostTypeObject($postType)?->rest_base;
343 $this->restBaseToPostTypeMap[empty($restBase) === true ? $postType : $restBase] = $postType;
344 }
345
346 return $this->restBaseToPostTypeMap;
347 }
348
349 /**
350 * @return null|array{type: string, id: int, addressesSubResource: bool}
351 */
352 private function getRestRouteTarget(WP_REST_Request $request): ?array
353 {
354 if (preg_match(self::REST_OBJECT_ROUTE_PATTERN, (string) $request->get_route(), $matches) !== 1) {
355 return null;
356 }
357
358 $postType = $this->getRestBaseToPostTypeMap()[$matches[1]] ?? null;
359
360 return $postType === null ? null : [
361 'type' => $postType,
362 'id' => (int) $matches[2],
363 'addressesSubResource' => ($matches[3] ?? '') !== ''
364 ];
365 }
366
367 private function isReadingRestRequest(WP_REST_Request $request): bool
368 {
369 return in_array(strtoupper((string) $request->get_method()), Wordpress::REST_READING_METHODS, true);
370 }
371
372 private function isEditingRestRoute(bool $addressesSubResource, WP_REST_Request $request): bool
373 {
374 return $this->isReadingRestRequest($request) === false || $addressesSubResource === true;
375 }
376
377 /**
378 * @throws UserGroupTypeException
379 */
380 private function hasRestRouteAccess(string $objectType, int $objectId, bool $isEditingRoute): bool
381 {
382 if ($isEditingRoute === true) {
383 return $this->accessHandler->checkObjectAccess($objectType, $objectId, true);
384 }
385
386 return $this->removePostFromList($objectType) === false
387 || $this->accessHandler->checkObjectAccess($objectType, $objectId);
388 }
389
390 /**
391 * @throws UserGroupTypeException
392 */
393 public function restrictRestRequest(mixed $result, mixed $server = null, mixed $request = null): mixed
394 {
395 if (($request instanceof WP_REST_Request) === false) {
396 return $result;
397 }
398
399 $routeTarget = $this->getRestRouteTarget($request);
400 $isEditingRoute = $routeTarget !== null
401 && $this->isEditingRestRoute($routeTarget['addressesSubResource'], $request);
402
403 $this->wordpress->setRestRequestContext(
404 $isEditingRoute === true || $request->get_param('context') === 'edit'
405 );
406
407 if ($result !== null || $routeTarget === null) {
408 return $result;
409 }
410
411 ['type' => $type, 'id' => $id] = $routeTarget;
412
413 return $this->hasRestRouteAccess($type, $id, $isEditingRoute) === true ?
414 $result : $this->getRestAccessDeniedError();
415 }
416
417 /**
418 * @throws UserGroupTypeException
419 */
420 public function getAttachedFile(string $file, int|string|null $attachmentId): bool|string
421 {
422 $isImage = (bool) preg_match('/(?i)\.(jpg|jpeg|jpe|png|gif)$/', $file);
423
424 if ($isImage === false && $this->mainConfig->lockFile() === true) {
425 $hasAccess = $this->accessHandler->checkObjectAccess(ObjectHandler::ATTACHMENT_OBJECT_TYPE, $attachmentId);
426 return ($hasAccess === true) ? $file : false;
427 }
428
429 return $file;
430 }
431
432 private function toIdList(array $postIds): string
433 {
434 return implode(', ', array_map('intval', $postIds));
435 }
436
437 /**
438 * @throws UserGroupTypeException
439 */
440 private function addQueryExcludedPostFilter(string $query, string $table): string
441 {
442 $excludedPosts = $this->accessHandler->getExcludedPosts();
443
444 if ($excludedPosts !== []) {
445 $query .= " AND $table.ID NOT IN ({$this->toIdList($excludedPosts)}) ";
446 }
447
448 return $query;
449 }
450
451 /**
452 * @throws UserGroupTypeException
453 */
454 public function showPostSql(string $query): string
455 {
456 return $this->addQueryExcludedPostFilter($query, $this->database->getPostsTable());
457 }
458
459 /**
460 * @throws UserGroupTypeException
461 */
462 public function showNextPreviousPost(string $query): string
463 {
464 return $this->addQueryExcludedPostFilter($query, 'p');
465 }
466
467 private function getPostCountQuery(array $excludedPosts, string $type, string $perm): string
468 {
469 $query = "SELECT `post_status`, COUNT(*) AS `num_posts`
470 FROM `{$this->database->getPostsTable()}`
471 WHERE `post_type` = %s
472 AND `ID` NOT IN ({$this->toIdList($excludedPosts)})";
473
474 if ('readable' === $perm
475 && $this->wordpress->isUserLoggedIn() === true
476 && $this->wordpress->currentUserCan(
477 $this->wordpress->getPostTypeObject($type)->cap->read_private_posts
478 ) === false
479 ) {
480 $query .= $this->database->prepare(
481 ' AND (`post_status` != \'private\' OR (`post_author` = %d AND `post_status` = \'private\'))',
482 $this->wordpress->getCurrentUser()->ID
483 );
484 }
485
486 $query .= ' GROUP BY `post_status`';
487 return $query;
488 }
489
490 /**
491 * @throws UserGroupTypeException
492 */
493 public function showPostCount(stdClass $counts, string $type, string $perm): stdClass
494 {
495 if (isset($this->cachedCounts[$type]) === true) {
496 return $this->cachedCounts[$type];
497 }
498
499 $excludedPosts = $this->accessHandler->getExcludedPosts();
500
501 if ($excludedPosts !== []) {
502 $query = $this->getPostCountQuery($excludedPosts, $type, $perm);
503 $results = (array) $this->database->getResults($this->database->prepare($query, $type), ARRAY_A);
504
505 foreach ($results as $result) {
506 if (isset($counts->{$result['post_status']})) {
507 $counts->{$result['post_status']} = $result['num_posts'];
508 }
509 }
510 }
511
512 $this->cachedCounts[$type] = $counts;
513
514 return $counts;
515 }
516
517 private function hidePostComment(string $postType): bool
518 {
519 return $this->mainConfig->lockPostTypeComments($postType) === true
520 || $this->mainConfig->hidePostType($postType) === true
521 || $this->wordpressConfig->atAdminPanel() === true;
522 }
523
524 /**
525 * @param WP_Comment[] $comments The comments.
526 * @throws UserGroupTypeException
527 */
528 public function showComment(array $comments = []): array
529 {
530 $showComments = [];
531
532 foreach ($comments as $comment) {
533 $post = $this->objectHandler->getPost($comment->comment_post_ID);
534
535 if ($post !== false
536 && $this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === false
537 ) {
538 if ($this->hidePostComment($post->post_type)) {
539 continue;
540 }
541
542 if ($this->mainConfig->hidePostTypeComments($post->post_type) === true) {
543 $comment->comment_content = $this->mainConfig->getPostTypeCommentContent($post->post_type);
544 }
545 }
546
547 $showComments[] = $comment;
548 }
549
550 return $showComments;
551 }
552
553 private function getAssignedGroupsHint(int|string|null $postId): string
554 {
555 $userGroups = $this->userGroupHandler->getFilteredUserGroupsForObject(
556 ObjectHandler::GENERAL_POST_OBJECT_TYPE,
557 $postId
558 );
559
560 if ($userGroups === []) {
561 return '';
562 }
563
564 $escapedNames = array_map(fn(AbstractUserGroup $group) => htmlentities($group->getName()), $userGroups);
565
566 return TXT_UAM_ASSIGNED_GROUPS . ': ' . implode(', ', $escapedNames);
567 }
568
569 /**
570 * @throws UserGroupTypeException
571 */
572 public function showEditLink(?string $link, int|string|null $postId): string
573 {
574 if ($this->mainConfig->hideEditLinkOnNoAccess() === true
575 && $this->accessHandler->checkObjectAccess(ObjectHandler::GENERAL_POST_OBJECT_TYPE, $postId, true) === false
576 ) {
577 $link = '';
578 }
579
580 if ($this->mainConfig->showAssignedGroups() === true) {
581 $assignedGroupsHint = $this->getAssignedGroupsHint($postId);
582
583 if ($assignedGroupsHint !== '') {
584 $link .= ($link !== '' ? ' | ' : ' ') . $assignedGroupsHint;
585 }
586 }
587
588 return (string) $link;
589 }
590 }
591