PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.3.1
Defender Security – Malware Scanner, Login Security & Firewall v5.3.1
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 5.3.1, at src/component/audit/class-post-audit.php

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