PluginProbe
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity / trunk
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity vtrunk
3.3.8 trunk 1.0 1.1.0 1.10.0 1.11.0 1.11.1 1.12.0 1.13.0 1.14.0 1.15.0 1.16.0 1.17.0 1.17.1 1.18.0 1.19.0 1.2.0 1.20.0 1.20.1 1.3.0 1.3.1 1.4.0 1.5.0 1.6.0 1.6.1 All 66 releases
logtivity / Loggers / Core / Logtivity_Post.php

Logtivity_Post.php in Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity trunk, at Loggers/Core/Logtivity_Post.php

535 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @package Logtivity
5 * @contact logtivity.io, hello@logtivity.io
6 * @copyright 2024-2026 Logtivity. All rights reserved
7 * @license https://www.gnu.org/licenses/gpl.html GNU/GPL
8 *
9 * This file is part of Logtivity.
10 *
11 * Logtivity is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation, either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * Logtivity is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with Logtivity. If not, see <https://www.gnu.org/licenses/>.
23 */
24
25 // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
26 // phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
27 // phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
28
29 class Logtivity_Post extends Logtivity_Abstract_Logger
30 {
31 /**
32 * @var array[]
33 */
34 protected array $post = [];
35
36 /**
37 * @var array[]
38 */
39 protected array $meta = [];
40
41 /**
42 * @var string[]
43 */
44 protected array $statusTexts = [
45 'publish' => 'Published',
46 'future' => 'Scheduled',
47 'draft' => 'Draft Saved',
48 'pending' => 'Pending',
49 'private' => 'Made Private',
50 'trash' => 'Trashed',
51 ];
52
53 /**
54 * @var string[]
55 */
56 protected array $ignoreStatuses = [
57 'auto-draft',
58 'draft',
59 ];
60
61 /**
62 * @var string[]
63 */
64 protected array $ignoreDataKeys = [
65 'post_modified',
66 'post_modified_gmt',
67 'post_date',
68 'post_date_gmt',
69 self::LAST_LOGGED_KEY,
70 '_edit_lock',
71 '_edit_last',
72 '_wp_attachment_metadata',
73 '_encloseme',
74 '_wp_trash_meta_status',
75 '_wp_trash_meta_time',
76 '_wp_desired_post_slug',
77 '_wp_trash_meta_comments_status',
78 '_pingme',
79 '_{$prefix}old_slug',
80 '_encloseme',
81 '_{$prefix}trash_meta_status',
82 '_{$prefix}trash_meta_time',
83 ];
84
85 /**
86 * @var string[]
87 */
88 protected array $clearName = [
89 '__trashed',
90 ];
91
92 /**
93 * @var string[]
94 */
95 protected array $ignoreTypes = [
96 'revision',
97 'customize_changeset',
98 'nav_menu_item',
99 /* @TODO: These need to be moved to the EDD Logger */
100 'edd_log',
101 'edd_payment',
102 'edd_license_log',
103 ];
104
105 /**
106 * @inheritDoc
107 */
108 protected function registerHooks(): void
109 {
110 global $wpdb;
111
112 $prefix = $wpdb->get_blog_prefix();
113
114 $this->ignoreDataKeys = array_map(
115 function (string $key) use ($prefix) {
116 return str_replace('{$prefix}', $prefix, $key);
117 },
118 $this->ignoreDataKeys
119 );
120
121 // Post meta changes
122
123 if (get_option('logtivity_enable_post_meta_logging')) {
124 add_action('added_post_meta', [$this, 'added_post_meta'], 10, 4);
125 add_action('updated_post_meta', [$this, 'updated_post_meta'], 10, 4);
126 add_action('delete_post_meta', [$this, 'delete_post_meta'], 10, 4);
127 }
128
129 // Core post changes
130 add_action('pre_post_update', [$this, 'saveOriginalData'], 10, 2);
131 add_action('save_post', [$this, 'save_post'], 10, 3);
132 add_action('before_delete_post', [$this, 'before_delete_post'], 10, 2);
133 add_action('after_delete_post', [$this, 'after_delete_post'], 10, 2);
134
135 // Catch category changes that are otherwise missed
136 add_action('set_object_terms', [$this, 'set_object_terms'], 10, 6);
137
138 // Media/Attachment changes
139 // @TODO: skip media data changes for now. Maybe always?
140 add_filter('wp_handle_upload', [$this, 'wp_handle_upload'], 10, 2);
141 //add_action('attachment_updated', [$this, 'attachment_updated'], 10, 3);
142 add_action('deleted_post', [$this, 'deleted_post'], 10, 2);
143
144 }
145
146 public function __call(string $name, array $arguments)
147 {
148 $log = Logtivity::log($name)
149 ->setContext('TESTING');
150
151 foreach ($arguments as $i => $argument) {
152 $log->addMeta('Arg #' . ($i + 1), $argument);
153 }
154
155 $log->addTrace()->send();
156
157 // For filters
158 return $arguments[0] ?? null;
159 }
160
161 /**
162 * @param int $metaId
163 * @param int $objectId
164 * @param string $key
165 * @param mixed $value
166 *
167 * @return void
168 */
169 public function added_post_meta($metaId, $objectId, $key, $value): void
170 {
171 $this->logMetaChange($objectId, $key, $value, 'Added');
172 }
173
174 /**
175 * @param int $metaId
176 * @param int $objectId
177 * @param string $key
178 * @param mixed $value
179 *
180 * @return void
181 */
182 public function updated_post_meta($metaId, $objectId, $key, $value): void
183 {
184 $this->logMetaChange($objectId, $key, $value, 'Updated');
185 }
186
187 public function delete_post_meta($metaId, $objectId, $key, $value): void
188 {
189 $this->logMetaChange($objectId, $key, $value, 'Deleted');
190 }
191
192 /**
193 * @param int $objectId
194 * @param string $key
195 * @param mixed $value
196 * @param string $action
197 *
198 * @return void
199 */
200 protected function logMetaChange($objectId, $key, $value, $action): void
201 {
202 $oldValue = $this->meta[$objectId][$key] ?? null;
203 $post = get_post($objectId);
204
205 if (
206 $post
207 && $this->shouldIgnorePost($post) == false
208 && array_search($key, $this->ignoreDataKeys) == false
209 && $oldValue != $value
210 && $post->post_type != 'attachment' // @TODO: deal with these later
211 ) {
212 $action = sprintf(
213 '%s Meta %s',
214 logtivity_logger_label($post->post_type),
215 $action
216 );
217
218 Logtivity::log($action)
219 ->setContext($key)
220 ->setPostType($post->post_type)
221 ->setPostId($objectId)
222 ->addMeta('Post Title', $post->post_title)
223 ->addMeta('Old Value', $oldValue)
224 ->addMeta('New Value', $value)
225 ->send();
226
227 if (isset($this->meta[$objectId][$key])) {
228 $this->meta[$objectId][$key] = $value;
229 }
230 }
231 }
232
233 /**
234 * @param int $objectId
235 * @param int[] $terms
236 * @param int[] $ttIds
237 * @param string $taxonomy
238 *
239 * @return void
240 */
241 public function set_object_terms($objectId, $terms, $ttIds, $taxonomy): void
242 {
243 if (
244 isset($this->post[$objectId])
245 && $taxonomy == 'category'
246 ) {
247 $oldPost = $this->post[$objectId];
248 $categories = $this->getCategoryNames($terms);
249
250 if (
251 $categories != $oldPost['post_category']
252 && $this->recentlyLogged($objectId, 'post') == false
253 ) {
254 // Thw normal post updated entry probably missed category changes
255 $oldCategories = join(':', $oldPost['post_category']);
256 $newCategories = join(':', $categories);
257 $post = $this->getPostData(get_post($objectId));
258 $action = $this->getPostAction($this->sanitizeDataFields($post), 'Category Changed');
259
260 Logtivity::log($action)
261 ->setContext($post['post_title'])
262 ->setPostType($post['post_type'])
263 ->setPostId($objectId)
264 ->addMeta('Old Categories', $oldCategories)
265 ->addMeta('New Categories', $newCategories)
266 ->send();
267 }
268 }
269 }
270
271 /**
272 * Note that we are ignoring the second argument passed to this hook
273 *
274 * @param int $postId
275 *
276 * @return void
277 */
278 public function saveOriginalData($postId): void
279 {
280 if (empty($this->post[$postId])) {
281 $this->post[$postId] = $this->getPostData(get_post($postId));
282 $this->meta[$postId] = $this->getPostMeta($postId);
283 }
284 }
285
286 /**
287 * @param int $postId
288 * @param WP_Post $post
289 *
290 * @return void
291 */
292 public function save_post($postId, $post): void
293 {
294 if (
295 $this->shouldIgnorePost($post) == false
296 && $this->recentlyLogged($postId, 'post') == false
297 ) {
298 $oldData = $this->sanitizeDataFields($this->post[$postId] ?? []);
299 $data = $this->sanitizeDataFields($this->getPostData($post));
300
301 $contentChanged = $this->checkContent($oldData, $data);
302
303 $oldMeta = $this->sanitizeDataFields($this->meta[$postId] ?? []);
304 $meta = $this->sanitizeDataFields($this->getPostMeta($postId));
305
306 if ($contentChanged || $oldData != $data || $oldMeta != $meta) {
307 $revisions = wp_get_post_revisions($postId);
308
309 Logtivity::log($this->getPostAction($data, $oldData))
310 ->setContext($post->post_title)
311 ->setPostType($post->post_type)
312 ->setPostId($postId)
313 ->addMeta('Status', $post->post_status)
314 ->addMeta('Revisions', count($revisions), false)
315 ->addMeta('View Revision', $this->getRevisionLink($revisions))
316 ->addMetaIf($contentChanged, 'Content Changed', 'Yes')
317 ->addMetaChanges($oldData, $data)
318 ->addMetaChanges($oldMeta, $meta)
319 ->send();
320
321 $this->setRecentlyLogged($postId, 'post');
322 }
323 }
324 }
325
326 /**
327 * @param int $postId
328 *
329 * @return void
330 */
331 public function before_delete_post($postId): void
332 {
333 $this->meta[$postId] = $this->getPostMeta($postId);
334 }
335
336 /**
337 * @param int $postId
338 * @param WP_Post $post
339 *
340 * @return void
341 */
342 public function after_delete_post($postId, $post): void
343 {
344 if ($this->shouldIgnorePost($post) == false) {
345 $postData = $this->sanitizeDataFields($this->getPostData($post));
346 unset($postData['Post Content']);
347
348 $metaData = $this->sanitizeDataFields($this->meta[$postId] ?? []);
349
350 Logtivity::log(ucfirst($post->post_type) . ' Deleted')
351 ->setContext($post->post_title)
352 ->setPostType($post->post_type)
353 ->setPostId($postId)
354 ->addMetaArray($postData)
355 ->addMetaArray($metaData)
356 ->send();
357 }
358 }
359
360 /**
361 * Using this filter rather than add_attachment action
362 * because we get all the info we want more easily
363 *
364 * @param array $upload
365 * @param string $context
366 *
367 * @return array
368 */
369 public function wp_handle_upload($upload, $context): array
370 {
371 Logtivity::log()
372 ->setAction('Attachment Uploaded')
373 ->setContext(basename($upload['file']))
374 ->addMeta('Url', $upload['url'])
375 ->addMeta('Type', $upload['type'])
376 ->addMeta('Context', $context)
377 ->addTrace()
378 ->send();
379
380 return $upload;
381 }
382
383 /**
384 * @param int $postId
385 * @param WP_Post $post
386 *
387 * @return void
388 */
389 public function deleted_post($postId, $post): void
390 {
391 if ($post->post_type == 'attachment') {
392 $data = $this->getPostData($post);
393 unset($data['post_content']);
394
395 Logtivity::log('Attachment Deleted')
396 ->setContext($post->post_title)
397 ->setPostType($post->post_type)
398 ->setPostId($postId)
399 ->addMetaArray($this->sanitizeDataFields($data))
400 ->send();
401 }
402 }
403
404 /**
405 * @param WP_Post $post
406 *
407 * @return array
408 */
409 protected function getPostData(WP_Post $post): array
410 {
411 $data = $post->to_array();
412
413 // Never show actual content
414 $data['post_content'] = sha1($post->post_content);
415
416 // Some statuses change name reflecting status
417 $data['post_name'] = str_replace($this->clearName, '', $data['post_name']);
418
419 // Convert author ID to author name
420 if (empty($data['post_author'] == false)) {
421 if ($author = get_user($data['post_author'])) {
422 $data['post_author'] = $author->user_login;
423 } else {
424 $data['post_author'] = 'ID ' . $data['post_author'];
425 }
426 }
427
428 $data['post_category'] = $this->getCategoryNames($data['post_category']);
429
430 return $data;
431 }
432
433 /**
434 * @param int $postId
435 *
436 * @return array
437 */
438 protected function getPostMeta(int $postId): array
439 {
440 return $this->getMetadata('post', $postId);
441 }
442
443 /**
444 * @param ?array $categoryIds
445 *
446 * @return array
447 */
448 protected function getCategoryNames(?array $categoryIds): array
449 {
450 $categories = [];
451 $terms = get_categories($categoryIds);
452 foreach ($terms as $term) {
453 $categories[] = $term->name;
454 }
455
456 return $categories;
457 }
458
459 /**
460 * @inheritDoc
461 */
462 protected function sanitizeDataFields(array $fields, array $ignoredKeys = []): array
463 {
464 return parent::sanitizeDataFields($fields, $this->ignoreDataKeys);
465 }
466
467 /**
468 * @param array $current
469 * @param string|array $previous
470 *
471 * @return string
472 */
473 protected function getPostAction(array $current, $previous): string
474 {
475 $type = ucfirst($current['Post Type'] ?? 'Post');
476 $status = $current['Post Status'] ?? '';
477
478 if (is_string($previous)) {
479 $change = $previous;
480 } else {
481 $oldStatus = $previous['Post Status'] ?? '';
482 $change = ($status != $oldStatus) ? ($this->statusTexts[$status] ?? null) : 'Updated';
483 }
484
485 return sprintf('%s %s', $type, $change);
486 }
487
488 /**
489 * @param array $old
490 * @param array $current
491 *
492 * @return bool
493 */
494 protected function checkContent(array &$old, array &$current): bool
495 {
496 // Assume arrays have already been sanitized
497 $key = 'Post Content';
498
499 if (isset($old[$key])) {
500 $contentChanged = $old[$key] != $current[$key];
501 unset($old[$key], $current[$key]);
502 }
503
504 return $contentChanged ?? false;
505 }
506
507 /**
508 * @param WP_Post[] $revisions
509 *
510 * @return ?string
511 */
512 protected function getRevisionLink(array $revisions): ?string
513 {
514 reset($revisions);
515
516 $id = key($revisions);
517 return $id
518 ? add_query_arg('revision', $id, admin_url('revision.php'))
519 : null;
520 }
521
522 /**
523 * @param WP_Post $post
524 *
525 * @return bool
526 */
527 protected function shouldIgnorePost(WP_Post $post): bool
528 {
529 return in_array($post->post_status, $this->ignoreStatuses)
530 || in_array($post->post_type, $this->ignoreTypes);
531 }
532 }
533
534 new Logtivity_Post();
535