PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.2
Defender Security – Malware Scanner, Login Security & Firewall v6.2.2
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / audit / class-post-audit.php

class-post-audit.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.2, at src/component/audit/class-post-audit.php

432 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Audit logging for post-related events in WordPress.
4 *
5 * @package WP_Defender\Component\Audit
6 */
7
8 namespace WP_Defender\Component\Audit;
9
10 use WP_Defender\Traits\User;
11 use Calotes\Helper\Array_Cache;
12 use WP_Defender\Model\Audit_Log;
13
14 /**
15 * Handles post-related audit events such as post creation, post update, and post deletion.
16 */
17 class Post_Audit extends Audit_Event {
18
19 use User;
20
21 /**
22 * Exclude events.
23 *
24 * @return array
25 * @since 3.9.0
26 */
27 protected function exclude_events() {
28 // Share excluded post types, e.g. from Hummingbird.
29 $public_events = apply_filters( 'wd_audit_excluded_post_types', array( 'wphb_minify_group' ) );
30 $public_events = is_array( $public_events ) ? $public_events : (array) $public_events;
31 $excluded_post_types = array(
32 // From Defender.
33 'wdscan_result',
34 'wdf_scan',
35 'wd_iplockout_log',
36 'wd_ip_lockout',
37 'wdf_scan_item',
38 // Default types.
39 'nav_menu_item',
40 'revision',
41 );
42
43 return array_unique( array_merge( $excluded_post_types, $public_events ) );
44 }
45
46 /**
47 * We will add a hook, for updated event, and cache that event content.
48 * Later we weill use the hook save post, to determine this is insert new post or update the cache will be
49 * the array of various post, as we don't want data be excluded. This way we can get more control.
50 */
51 public function __construct() {
52 add_action( 'post_updated', array( $this, 'cache_post_updated' ), 10, 3 );
53 }
54
55 /**
56 * Caches the before and after states of a post when it is updated. This cache is used later to determine the
57 * nature of the update.
58 *
59 * @param mixed $post_id The ID of the post being updated.
60 * @param mixed $after The state of the post after the update.
61 * @param mixed $before The state of the post before the update.
62 *
63 * @return void
64 */
65 public function cache_post_updated( $post_id, $after, $before ) {
66 Array_Cache::append(
67 'post_updated',
68 array(
69 'post_id' => $post_id,
70 'after' => $after,
71 'before' => $before,
72 ),
73 'audit'
74 );
75 }
76
77 /**
78 * Defines and returns an array of hooks associated with post events and their corresponding callback functions,
79 * event types, and other relevant parameters.
80 *
81 * @return array
82 */
83 public function get_hooks(): array {
84 return array(
85 'save_post' => array(
86 'args' => array( 'post_ID', 'post', 'is_updated' ),
87 'callback' => array( self::class, 'post_updated_callback' ),
88 'event_type' => Audit_Log::EVENT_TYPE_CONTENT,
89 'action_type' => self::ACTION_UPDATED,
90 ),
91 'transition_post_status' => array(
92 'args' => array( 'new_status', 'old_status', 'post' ),
93 'event_type' => Audit_Log::EVENT_TYPE_CONTENT,
94 'action_type' => self::ACTION_UPDATED,
95 'false_when' => array(
96 array(
97 '{{post->post_status}}',
98 array(
99 'inherit',
100 'new',
101 'auto-draft',
102 'trash',
103 ),
104 ),
105 array(
106 '{{post->post_type}}',
107 $this->exclude_events(),
108 ),
109 array(
110 '{{new_status}}',
111 '{{old_status}}',
112 ),
113 array(
114 '{{old_status}}',
115 array(
116 'trash',
117 'new',
118 ),
119 ),
120 ),
121 'text' => array(
122 array(
123 sprintf(
124 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Post type, 4: Post title */
125 esc_html__( '%1$s %2$s published %3$s "%4$s"', 'defender-security' ),
126 '{{blog_name}}',
127 '{{wp_user}}',
128 '{{post_type_label}}',
129 '{{post_title}}'
130 ),
131 '{{new_status}}',
132 'publish',
133 '==',
134 ),
135 array(
136 sprintf(
137 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Post type, 4: Post title */
138 esc_html__( '%1$s %2$s pending %3$s "%4$s"', 'defender-security' ),
139 '{{blog_name}}',
140 '{{wp_user}}',
141 '{{post_type_label}}',
142 '{{post_title}}'
143 ),
144 '{{new_status}}',
145 'pending',
146 '==',
147 ),
148 array(
149 sprintf(
150 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Post type, 4: Post title */
151 esc_html__( '%1$s %2$s drafted %3$s "%4$s"', 'defender-security' ),
152 '{{blog_name}}',
153 '{{wp_user}}',
154 '{{post_type_label}}',
155 '{{post_title}}'
156 ),
157 '{{new_status}}',
158 'draft',
159 '==',
160 ),
161 array(
162 sprintf(
163 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Post type, 4: Post title, 5: Old status, 6: New status */
164 esc_html__( '%1$s %2$s changed %3$s "%4$s" status from %5$s to %6$s', 'defender-security' ),
165 '{{blog_name}}',
166 '{{wp_user}}',
167 '{{post_type_label}}',
168 '{{post_title}}',
169 '{{old_status}}',
170 '{{new_status}}'
171 ),
172 '{{new_status}}',
173 '{{new_status}}',
174 '==',
175 ),
176 ),
177 'program_args' => array(
178 'post_type_label' => array(
179 'callable' => 'get_post_type_object',
180 'params' => array( '{{post->post_type}}' ),
181 'result_property' => 'labels->singular_name',
182 ),
183 ),
184 'custom_args' => array(
185 'post_title' => '{{post->post_title}}',
186 ),
187 'context' => '{{post_type_label}}',
188 ),
189 'delete_post' => array(
190 'args' => array( 'post_ID' ),
191 'event_type' => Audit_Log::EVENT_TYPE_CONTENT,
192 'action_type' => self::ACTION_DELETED,
193 'text' => sprintf(
194 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Post type, 4: Post title */
195 esc_html__( '%1$s %2$s deleted %3$s "%4$s"', 'defender-security' ),
196 '{{blog_name}}',
197 '{{wp_user}}',
198 '{{post_type_label}}',
199 '{{post_title}}'
200 ),
201 'program_args' => array(
202 'post' => array(
203 'callable' => 'get_post',
204 'params' => array( '{{post_ID}}' ),
205 ),
206 'post_type_label' => array(
207 'callable' => 'get_post_type_object',
208 'params' => array( '{{post->post_type}}' ),
209 'result_property' => 'labels->singular_name',
210 ),
211 'post_title' => array(
212 'callable' => 'get_post',
213 'params' => array( '{{post_ID}}' ),
214 'result_property' => 'post_title',
215 ),
216 ),
217 'context' => '{{post_type_label}}',
218 'false_when' => array(
219 array(
220 '{{post->post_type}}',
221 array_merge(
222 array( 'attachment' ),
223 $this->exclude_events()
224 ),
225 ),
226 array(
227 '{{post_type_label}}',
228 '',
229 ),
230 ),
231 ),
232 'untrashed_post' => array(
233 'args' => array( 'post_ID' ),
234 'action_type' => self::ACTION_RESTORED,
235 'event_type' => Audit_Log::EVENT_TYPE_CONTENT,
236 'text' => sprintf(
237 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Post type, 4: Post title */
238 esc_html__( '%1$s %2$s untrashed %3$s "%4$s"', 'defender-security' ),
239 '{{blog_name}}',
240 '{{wp_user}}',
241 '{{post_type_label}}',
242 '{{post_title}}'
243 ),
244 'program_args' => array(
245 'post' => array(
246 'callable' => 'get_post',
247 'params' => array( '{{post_ID}}' ),
248 ),
249 'post_type_label' => array(
250 'callable' => 'get_post_type_object',
251 'params' => array( '{{post->post_type}}' ),
252 'result_property' => 'labels->singular_name',
253 ),
254 'post_title' => array(
255 'callable' => 'get_post',
256 'params' => array( '{{post_ID}}' ),
257 'result_property' => 'post_title',
258 ),
259 ),
260 'context' => '{{post_type_label}}',
261 ),
262 'trashed_post' => array(
263 'args' => array( 'post_ID' ),
264 'action_type' => self::ACTION_TRASHED,
265 'event_type' => Audit_Log::EVENT_TYPE_CONTENT,
266 'text' => sprintf(
267 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Post type, 4: Post title */
268 esc_html__( '%1$s %2$s trashed %3$s "%4$s"', 'defender-security' ),
269 '{{blog_name}}',
270 '{{wp_user}}',
271 '{{post_type_label}}',
272 '{{post_title}}'
273 ),
274 'program_args' => array(
275 'post' => array(
276 'callable' => 'get_post',
277 'params' => array( '{{post_ID}}' ),
278 ),
279 'post_type_label' => array(
280 'callable' => 'get_post_type_object',
281 'params' => array( '{{post->post_type}}' ),
282 'result_property' => 'labels->singular_name',
283 ),
284 'post_title' => array(
285 'callable' => 'get_post',
286 'params' => array( '{{post_ID}}' ),
287 'result_property' => 'post_title',
288 ),
289 ),
290 'context' => '{{post_type_label}}',
291 ),
292 );
293 }
294
295 /**
296 * Callback function for the save_post hook. It determines if a post has been updated and logs the appropriate
297 * audit event.
298 *
299 * @return bool|array
300 */
301 public function post_updated_callback() {
302 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
303 return false;
304 }
305
306 $args = func_get_args();
307 $post = $args[1]['post'];
308
309 if ( in_array( $post->post_status, array( 'trash', 'auto-draft' ), true ) ) {
310 // Usually, wp adds :trash to the post name, so this case we just return.
311 return false;
312 }
313
314 if ( in_array( $post->post_type, $this->exclude_events(), true ) ) {
315 return false;
316 }
317
318 $post_type = get_post_type_object( $post->post_type );
319 if ( ! is_object( $post_type ) ) {
320 return false;
321 }
322
323 $is_updated = $args[1]['is_updated'];
324 $post_before = null;
325 $cached = Array_Cache::get( 'post_updated', 'audit', array() );
326 foreach ( $cached as $post_arr ) {
327 if ( $post->ID === $post_arr['post_id'] ) {
328 $post_before = $post_arr['before'];
329 break;
330 }
331 }
332
333 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
334 if ( true === $is_updated ) {
335 if ( ! is_null( $post_before ) ) {
336 $post_after = $post->to_array();
337 $post_before = $post_before->to_array();
338
339 // Status transitions are handled via other hooks.
340 if ( $post_before['post_status'] !== $post_after['post_status'] ) {
341 return false;
342 }
343
344 // Unset the date modified, and post status, as we got another hook for that.
345 unset( $post_after['post_modified'] );
346 unset( $post_after['post_modified_gmt'] );
347 unset( $post_after['post_status'] );
348 unset( $post_before['post_modified'] );
349 unset( $post_before['post_modified_gmt'] );
350 unset( $post_before['post_status'] );
351 if ( wp_json_encode( $post_before ) !== wp_json_encode( $post_after ) ) {
352 $item_changed_count = count( self::array_recursive_diff( $post_before, $post_after ) );
353 if ( $post_before['post_title'] !== $post_after['post_title'] && 1 === $item_changed_count ) {
354 $text = sprintf(
355 /* translators: 1: Blog name, 2: User's display name, 3: Post type, 4: Post ID, 5: Old post title, 6: New post title */
356 esc_html__( '%1$s %2$s updated %3$s ID %4$d, title from "%5$s" to "%6$s"', 'defender-security' ),
357 $blog_name,
358 $this->get_user_display( get_current_user_id() ),
359 $post_type->labels->singular_name,
360 $post_after['ID'],
361 $post_before['post_title'],
362 $post_after['post_title']
363 );
364 } elseif ( $post_before['post_name'] !== $post_after['post_name'] && 1 === $item_changed_count ) {
365 $text = sprintf(
366 /* translators: 1: Blog name, 2: User's display name, 3: Post type, 4: Post ID, 5: Old post slug, 6: New post slug */
367 esc_html__( '%1$s %2$s updated %3$s ID %4$d, slug from "%5$s" to "%6$s"', 'defender-security' ),
368 $blog_name,
369 $this->get_user_display( get_current_user_id() ),
370 $post_type->labels->singular_name,
371 $post_after['ID'],
372 $post_before['post_name'],
373 $post_after['post_name']
374 );
375 } elseif ( $post_before['post_author'] !== $post_after['post_author'] && 1 === $item_changed_count ) {
376 $text = sprintf(
377 /* translators: 1: Blog name, 2: User's display name, 3: Post type, 4: Post ID, 5: Old author name, 6: New author name */
378 esc_html__( '%1$s %2$s updated %3$s ID %4$d, author from "%5$s" to "%6$s"', 'defender-security' ),
379 $blog_name,
380 $this->get_user_display( get_current_user_id() ),
381 $post_type->labels->singular_name,
382 $post_after['ID'],
383 $this->get_user_display( $post_before['post_author'] ),
384 $this->get_user_display( $post_after['post_author'] )
385 );
386 } else {
387 $text = sprintf(
388 /* translators: 1: Blog name, 2: User's display name, 3: Post type, 4: Post title */
389 esc_html__( '%1$s %2$s updated %3$s "%4$s"', 'defender-security' ),
390 $blog_name,
391 $this->get_user_display( get_current_user_id() ),
392 $post_type->labels->singular_name,
393 $post_after['post_title']
394 );
395 }
396
397 return array(
398 $text,
399 $post_type->labels->singular_name,
400 );
401 }
402 }
403 } elseif ( is_null( $post_before ) ) {
404 $text = sprintf(
405 /* translators: 1: Blog name, 2: User's display name, 3: Post type, 4: Post title */
406 esc_html__( '%1$s %2$s added new %3$s "%4$s"', 'defender-security' ),
407 $blog_name,
408 $this->get_user_display( get_current_user_id() ),
409 $post_type->labels->singular_name,
410 $post->post_title
411 );
412
413 return array(
414 $text,
415 $post_type->labels->singular_name,
416 );
417 }
418
419 return false;
420 }
421
422 /**
423 * Provides a dictionary or glossary that could be used for translating or explaining terms. Currently, returns an
424 * empty array.
425 *
426 * @return array
427 */
428 public function dictionary(): array {
429 return array();
430 }
431 }
432