PluginProbe
User Access Manager / 2.3.14
User Access Manager v2.3.14
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.14, at src/Controller/Frontend/PostController.php

510 lines 15.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_Hook;
23 use WP_Post;
24 use WP_Query;
25 use WP_REST_Request;
26 use WP_REST_Response;
27
28 class PostController extends ContentController
29 {
30 private array $wordpressFilters = [];
31 private stdClass|array|null $cachedCounts = [];
32
33 private WeakMap $posts;
34
35 public function __construct(
36 Php $php,
37 Wordpress $wordpress,
38 WordpressConfig $wordpressConfig,
39 MainConfig $mainConfig,
40 Util $util,
41 ObjectHandler $objectHandler,
42 UserHandler $userHandler,
43 UserGroupHandler $userGroupHandler,
44 AccessHandler $accessHandler,
45 private Database $database
46 ) {
47 parent::__construct(
48 $php,
49 $wordpress,
50 $wordpressConfig,
51 $mainConfig,
52 $util,
53 $objectHandler,
54 $userHandler,
55 $userGroupHandler,
56 $accessHandler
57 );
58
59 $this->posts = new WeakMap();
60 }
61
62 public function getWordpressFilters(): array
63 {
64 return $this->wordpressFilters;
65 }
66
67 private function filtersSuppressed(WP_Query $wpQuery): bool
68 {
69 return isset($wpQuery->query_vars['suppress_filters']) === true
70 && $wpQuery->query_vars['suppress_filters'] === true;
71 }
72
73 private function addExcludedPosts(mixed $postsNotIn, array $excludedPosts): array
74 {
75 return array_unique(array_merge((array) $postsNotIn, $excludedPosts));
76 }
77
78 /**
79 * @throws UserGroupTypeException
80 */
81 public function parseQuery(WP_Query $wpQuery): void
82 {
83 if ($this->filtersSuppressed($wpQuery) === true) {
84 $excludedPosts = $this->accessHandler->getExcludedPosts();
85
86 if ($excludedPosts !== []) {
87 $wpQuery->query_vars['post__not_in'] = $this->addExcludedPosts(
88 $wpQuery->query_vars['post__not_in'] ?? [],
89 $excludedPosts
90 );
91 }
92 }
93 }
94
95 /**
96 * @param WP_Hook[] $filters
97 */
98 private function extractOwnFilters(array $filters): bool
99 {
100 if (isset($filters['the_posts']->callbacks[10]) === true) {
101 foreach ($filters['the_posts']->callbacks[10] as $postFilter) {
102 if (is_array($postFilter['function']) === true
103 && $postFilter['function'][0] instanceof PostController
104 && $postFilter['function'][1] === 'showPosts'
105 ) {
106 $this->wordpressFilters['the_posts'] = $filters['the_posts'];
107 $filters['the_posts']->callbacks = [10 => [$postFilter]];
108 return true;
109 }
110 }
111 }
112
113 return false;
114 }
115
116 public function postsPreQuery(?array $posts, WP_Query $query): ?array
117 {
118 if ($this->filtersSuppressed($query) === true) {
119 $filters = $this->wordpress->getFilters();
120
121 // Only unset filter if the user access filter is active
122 if ($this->extractOwnFilters($filters) === true) {
123 $query->query_vars['suppress_filters'] = false;
124
125 if (isset($filters['posts_results']) === true) {
126 $this->wordpressFilters['posts_results'] = $filters['posts_results'];
127 unset($filters['posts_results']);
128 }
129
130 $this->wordpress->setFilters($filters);
131 }
132 }
133
134 return $posts;
135 }
136
137 private function restoreFilters(): void
138 {
139 if (count($this->wordpressFilters) > 0) {
140 $filters = $this->wordpress->getFilters();
141
142 foreach ($this->wordpressFilters as $filterKey => $filter) {
143 $filters[$filterKey] = $filter;
144 }
145
146 $this->wordpress->setFilters($filters);
147 $this->wordpressFilters = [];
148 }
149 }
150
151 private function getPost(mixed $post): bool|WP_Post
152 {
153 if ($post instanceof WP_post) {
154 return $post;
155 } elseif (is_int($post) === true) {
156 return $this->objectHandler->getPost($post);
157 } elseif (isset($post->ID) === true) {
158 return $this->objectHandler->getPost($post->ID);
159 }
160
161 return false;
162 }
163
164 private function processPostContent(WP_Post $post): string
165 {
166 $uamPostContent = htmlspecialchars_decode($this->mainConfig->getPostTypeContent($post->post_type));
167
168 if ($this->mainConfig->showPostTypeContentBeforeMore($post->post_type) === true
169 && preg_match('/<!--more(.*?)?-->/', $post->post_content, $matches)
170 ) {
171 $uamPostContent = explode($matches[0], $post->post_content)[0] . ' ' . $uamPostContent;
172 }
173
174 return stripslashes($uamPostContent);
175 }
176
177 /**
178 * @throws UserGroupTypeException
179 */
180 private function processPost(WP_Post $post): WP_Post|bool
181 {
182 $post->post_title .= $this->adminOutput($post->post_type, $post->ID);
183
184 if ($this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === false) {
185 if ($this->removePostFromList($post->post_type) === true) {
186 return false;
187 }
188
189 $post->post_content = $this->processPostContent($post);
190
191 if ($this->mainConfig->hidePostTypeTitle($post->post_type) === true) {
192 $post->post_title = $this->mainConfig->getPostTypeTitle($post->post_type);
193 }
194
195 if ($this->mainConfig->lockPostTypeComments($post->post_type) === true) {
196 $post->comment_status = 'close';
197 }
198 }
199
200 return $post;
201 }
202
203 /**
204 * @throws UserGroupTypeException
205 */
206 private function getProcessedPost(WP_Post $post): ?WP_Post
207 {
208 $post = $this->posts[$post] ??= $this->processPost($post);
209 return $post === false ? null : $post;
210 }
211
212 /**
213 * @throws UserGroupTypeException
214 */
215 private function filterRawPosts(array $rawPosts): array
216 {
217 $filteredPosts = [];
218
219 foreach ($rawPosts as $rawPost) {
220 $post = $this->getPost($rawPost);
221
222 if ($post !== false) {
223 $post = $this->getProcessedPost($post);
224
225 if ($post !== null) {
226 $filteredPosts[] = $post;
227 }
228 } else {
229 $filteredPosts[] = $rawPost;
230 }
231 }
232
233 return $filteredPosts;
234 }
235
236 /**
237 * @throws UserGroupTypeException
238 */
239 public function showPosts(?array $showPosts = []): ?array
240 {
241 if ($this->wordpress->isFeed() === false || $this->mainConfig->protectFeed() === true) {
242 $showPosts = $this->filterRawPosts((array) $showPosts);
243 }
244
245 $this->restoreFilters();
246
247 return $showPosts;
248 }
249
250 /**
251 * @param WP_Post[] $rawPages The pages.
252 * @throws UserGroupTypeException
253 */
254 public function showPages(array $rawPages = []): array
255 {
256 return $this->filterRawPosts($rawPages);
257 }
258
259 private function isSingleObjectRestRequest(mixed $request, WP_Post $post): bool
260 {
261 return $request instanceof WP_REST_Request
262 && $request->get_param('id') !== null
263 && (int) $request->get_param('id') === (int) $post->ID;
264 }
265
266 private function setRestField(array &$data, string $field, string $value): void
267 {
268 if (array_key_exists($field, $data) === false) {
269 return;
270 }
271
272 if (is_array($data[$field]) === true) {
273 if (array_key_exists('rendered', $data[$field]) === true) {
274 $data[$field]['rendered'] = $value;
275 }
276
277 if (array_key_exists('raw', $data[$field]) === true) {
278 $data[$field]['raw'] = $value;
279 }
280
281 if (array_key_exists('protected', $data[$field]) === true) {
282 $data[$field]['protected'] = false;
283 }
284 } else {
285 $data[$field] = $value;
286 }
287 }
288
289 /**
290 * The_posts / posts_where_paged never run for REST single-item requests,
291 * which resolve through get_post() directly, so access is enforced here too.
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 if ($this->removePostFromList($post->post_type) === true
305 && $this->isSingleObjectRestRequest($request, $post) === true
306 ) {
307 return $this->wordpress->getWpError(
308 'uam_rest_access_denied',
309 TXT_UAM_REST_ACCESS_DENIED,
310 ['status' => $this->wordpress->isUserLoggedIn() === true ? 403 : 401]
311 );
312 }
313
314 $restrictedContent = $this->processPostContent($post);
315 $data = (array) $response->get_data();
316
317 $this->setRestField($data, 'content', $restrictedContent);
318 $this->setRestField($data, 'excerpt', $restrictedContent);
319
320 if ($this->mainConfig->hidePostTypeTitle($post->post_type) === true) {
321 $this->setRestField($data, 'title', $this->mainConfig->getPostTypeTitle($post->post_type));
322 }
323
324 $response->set_data($data);
325
326 return $response;
327 }
328
329 /**
330 * @throws UserGroupTypeException
331 */
332 public function excludeRestrictedPostsFromRestQuery(array $queryArgs): array
333 {
334 $excludedPosts = $this->accessHandler->getExcludedPosts();
335
336 if ($excludedPosts !== []) {
337 $queryArgs['post__not_in'] = $this->addExcludedPosts($queryArgs['post__not_in'] ?? [], $excludedPosts);
338 }
339
340 return $queryArgs;
341 }
342
343 /**
344 * @throws UserGroupTypeException
345 */
346 public function getAttachedFile(string $file, int|string|null $attachmentId): bool|string
347 {
348 $isImage = (bool) preg_match('/(?i)\.(jpg|jpeg|jpe|png|gif)$/', $file);
349
350 if ($isImage === false && $this->mainConfig->lockFile() === true) {
351 $hasAccess = $this->accessHandler->checkObjectAccess(ObjectHandler::ATTACHMENT_OBJECT_TYPE, $attachmentId);
352 return ($hasAccess === true) ? $file : false;
353 }
354
355 return $file;
356 }
357
358 /**
359 * @throws UserGroupTypeException
360 */
361 private function addQueryExcludedPostFilter(string $query, string $table): string
362 {
363 $excludedPosts = $this->accessHandler->getExcludedPosts();
364
365 if ($excludedPosts !== []) {
366 $excludedPostsStr = implode(', ', array_map('intval', $excludedPosts));
367 $query .= " AND $table.ID NOT IN ($excludedPostsStr) ";
368 }
369
370 return $query;
371 }
372
373 /**
374 * @throws UserGroupTypeException
375 */
376 public function showPostSql(string $query): string
377 {
378 return $this->addQueryExcludedPostFilter($query, $this->database->getPostsTable());
379 }
380
381 /**
382 * @throws UserGroupTypeException
383 */
384 public function showNextPreviousPost(string $query): string
385 {
386 return $this->addQueryExcludedPostFilter($query, 'p');
387 }
388
389 private function getPostCountQuery(array $excludedPosts, string $type, string $perm): string
390 {
391 $excludedPosts = implode(', ', array_map('intval', $excludedPosts));
392 $query = "SELECT post_status, COUNT(*) AS num_posts
393 FROM {$this->database->getPostsTable()}
394 WHERE post_type = %s
395 AND ID NOT IN ($excludedPosts)";
396
397 if ('readable' === $perm
398 && $this->wordpress->isUserLoggedIn() === true
399 && $this->wordpress->currentUserCan(
400 $this->wordpress->getPostTypeObject($type)->cap->read_private_posts
401 ) === false
402 ) {
403 $query .= $this->database->prepare(
404 ' AND (post_status != \'private\' OR (post_author = %d AND post_status = \'private\'))',
405 $this->wordpress->getCurrentUser()->ID
406 );
407 }
408
409 $query .= ' GROUP BY post_status';
410 return $query;
411 }
412
413 /**
414 * @throws UserGroupTypeException
415 */
416 public function showPostCount(stdClass $counts, string $type, string $perm): stdClass
417 {
418 if (isset($this->cachedCounts[$type]) === false) {
419 $excludedPosts = $this->accessHandler->getExcludedPosts();
420
421 if ($excludedPosts !== []) {
422 $query = $this->getPostCountQuery($excludedPosts, $type, $perm);
423 $results = (array) $this->database->getResults(
424 $this->database->prepare($query, $type),
425 ARRAY_A
426 );
427
428 foreach ($results as $result) {
429 if (isset($counts->{$result['post_status']})) {
430 $counts->{$result['post_status']} = $result['num_posts'];
431 }
432 }
433 }
434
435 $this->cachedCounts[$type] = $counts;
436 }
437
438 return $this->cachedCounts[$type];
439 }
440
441 private function hidePostComment(string $postType): bool
442 {
443 return $this->mainConfig->lockPostTypeComments($postType) === true
444 || $this->mainConfig->hidePostType($postType) === true
445 || $this->wordpressConfig->atAdminPanel() === true;
446 }
447
448 /**
449 * @param WP_Comment[] $comments The comments.
450 * @throws UserGroupTypeException
451 */
452 public function showComment(array $comments = []): array
453 {
454 $showComments = [];
455
456 foreach ($comments as $comment) {
457 $post = $this->objectHandler->getPost($comment->comment_post_ID);
458
459 if ($post !== false
460 && $this->accessHandler->checkObjectAccess($post->post_type, $post->ID) === false
461 ) {
462 if ($this->hidePostComment($post->post_type)) {
463 continue;
464 }
465
466 if ($this->mainConfig->hidePostTypeComments($post->post_type) === true) {
467 $comment->comment_content = $this->mainConfig->getPostTypeCommentContent($post->post_type);
468 }
469 }
470
471 $showComments[] = $comment;
472 }
473
474 return $showComments;
475 }
476
477 /**
478 * @throws UserGroupTypeException
479 */
480 public function showEditLink(?string $link, int|string|null $postId): string
481 {
482 if ($this->mainConfig->hideEditLinkOnNoAccess() === true
483 && $this->accessHandler->checkObjectAccess(ObjectHandler::GENERAL_POST_OBJECT_TYPE, $postId, true) === false
484 ) {
485 $link = '';
486 }
487
488 if ($this->mainConfig->showAssignedGroups() === true) {
489 $userGroups = $this->userGroupHandler->getFilteredUserGroupsForObject(
490 ObjectHandler::GENERAL_POST_OBJECT_TYPE,
491 $postId
492 );
493
494 if (count($userGroups) > 0) {
495 $escapedGroups = array_map(
496 function (AbstractUserGroup $group) {
497 return htmlentities($group->getName());
498 },
499 $userGroups
500 );
501
502 $link .= $link !== '' ? ' | ' : ' ';
503 $link .= TXT_UAM_ASSIGNED_GROUPS . ': ' . implode(', ', $escapedGroups);
504 }
505 }
506
507 return (string) $link;
508 }
509 }
510