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-menu-audit.php

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

377 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles the auditing of navigation menu events.
4 *
5 * @package WP_Defender\Component\Audit
6 */
7
8 namespace WP_Defender\Component\Audit;
9
10 use Calotes\Helper\Array_Cache;
11 use WP_Defender\Model\Audit_Log;
12
13 /**
14 * Handles the auditing of navigation menu events such as menu creation.
15 *
16 * @author Shoaib Ali
17 * @since 2.6.1
18 */
19 class Menu_Audit extends Audit_Event {
20
21 public const ACTION_CREATED = 'created';
22 /**
23 * Type of audit.
24 *
25 * @var string
26 */
27 protected $type = 'Navigation Menu';
28
29 /**
30 * Menu_Audit constructor.
31 * Hooks into the admin_init action to cache the old menu state.
32 */
33 public function __construct() {
34 add_action( 'admin_init', array( $this, 'cache_old_menu' ) );
35 }
36
37 /**
38 * Cache old menu before updating.
39 */
40 public function cache_old_menu() {
41 $script_name = filter_var( defender_get_data_from_request( 'SCRIPT_NAME', 's' ), FILTER_SANITIZE_URL );
42 if ( filter_input( INPUT_POST, 'action' ) ) {
43 $action_type = filter_input( INPUT_POST, 'action' );
44 } else {
45 $action_type = filter_input( INPUT_GET, 'action' );
46 }
47 if ( filter_input( INPUT_POST, 'menu' ) ) {
48 $term_id = filter_input( INPUT_POST, 'menu' );
49 } else {
50 $term_id = filter_input( INPUT_GET, 'menu' );
51 }
52
53 if (
54 'nav-menus.php' === basename( $script_name )
55 && ! empty( $action_type )
56 && ! empty( $term_id )
57 ) {
58 Array_Cache::append(
59 'menu_updated',
60 $this->build_menu( $term_id ),
61 'audit'
62 );
63 }
64 }
65
66 /**
67 * Constructs an array representing the menu structure based on the provided term ID.
68 *
69 * @param mixed $term_id The ID of the menu.
70 *
71 * @return array
72 */
73 public function build_menu( $term_id ): array {
74 $menu = array();
75 $menu_obj = wp_get_nav_menu_object( $term_id );
76
77 if ( ! empty( $menu_obj ) ) {
78 $menu['term_id'] = $menu_obj->term_id;
79 $menu['name'] = $menu_obj->name;
80 $menu['items'] = array();
81
82 $items = wp_get_nav_menu_items( $menu_obj->term_id );
83 if ( ! empty( $items ) ) {
84 foreach ( $items as $item ) {
85 array_push(
86 $menu['items'],
87 array(
88 'item_id' => $item->ID,
89 'title' => $item->title,
90 'url' => $item->url,
91 'object' => $item->object,
92 'menu_order' => $item->menu_order,
93 'menu_item_parent' => $item->menu_item_parent,
94 )
95 );
96 }
97 }
98 }
99
100 return $menu;
101 }
102
103 /**
104 * Returns an array of hooks associated with various menu actions for auditing purposes.
105 *
106 * @return array
107 */
108 public function get_hooks(): array {
109 return array(
110 'wp_create_nav_menu' => array(
111 'args' => array( 'term_id', 'menu_data' ),
112 'callback' => array( self::class, 'menu_created_callback' ),
113 'event_type' => Audit_Log::EVENT_TYPE_MENU,
114 'action_type' => self::ACTION_CREATED,
115 ),
116 'wp_update_nav_menu' => array(
117 'args' => array( 'menu_id', 'menu_data' ),
118 'callback' => array( self::class, 'menu_updated_callback' ),
119 'event_type' => Audit_Log::EVENT_TYPE_MENU,
120 'action_type' => self::ACTION_UPDATED,
121 ),
122 'wp_delete_nav_menu' => array(
123 'args' => array( 'term_id' ),
124 'callback' => array( self::class, 'menu_deleted_callback' ),
125 'event_type' => Audit_Log::EVENT_TYPE_MENU,
126 'action_type' => self::ACTION_DELETED,
127 ),
128 'wp_update_nav_menu_item' => array(
129 'args' => array( 'menu_id', 'menu_item_db_id', 'args' ),
130 'callback' => array( self::class, 'menu_item_updated_callback' ),
131 'event_type' => Audit_Log::EVENT_TYPE_MENU,
132 'action_type' => self::ACTION_UPDATED,
133 ),
134 'delete_post' => array(
135 'args' => array( 'post_ID' ),
136 'callback' => array( self::class, 'menu_item_deleted_callback' ),
137 'event_type' => Audit_Log::EVENT_TYPE_MENU,
138 'action_type' => self::ACTION_DELETED,
139 ),
140 );
141 }
142
143 /**
144 * Callback for when a menu item is updated. It checks if the update is significant for logging.
145 *
146 * @return array|bool
147 */
148 public function menu_item_updated_callback() {
149 if ( $this->doing_autosave() ) {
150 return false;
151 }
152
153 // Filter $_POST.
154 $post_array = filter_input_array( INPUT_POST );
155
156 if ( ! isset( $post_array['menu-name'] ) ) {
157 return false;
158 }
159
160 $func_args = func_get_args();
161 $menu_id = $func_args[1]['menu_id'];
162 $menu_item_db_id = $func_args[1]['menu_item_db_id'];
163 $args = $func_args[1]['args'];
164 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
165 $old_menu = $this->get_cached_menu( $menu_id );
166
167 if ( ! empty( $old_menu ) ) {
168 $old_items = array_column( $old_menu['items'], 'item_id' );
169 $new_items = array_keys( $post_array['menu-item-title'] );
170
171 if ( ! empty( $old_menu['items'] ) ) {
172 foreach ( $old_menu['items'] as $old_item ) {
173 if ( $menu_item_db_id === $old_item['item_id'] ) {
174 if (
175 $old_item['menu_order'] !== $args['menu-item-position']
176 || $old_item['menu_item_parent'] !== $args['menu-item-parent-id']
177 || ( ! empty( $args['menu-item-title'] ) && $old_item['title'] !== $args['menu-item-title'] )
178 ) {
179 return array(
180 sprintf(
181 /* translators: 1: Blog name, 2: User's display name, 3: Menu item, 4: Menu name */
182 esc_html__( '%1$s %2$s updated item "%3$s" from menu "%4$s"', 'defender-security' ),
183 $blog_name,
184 $this->get_user_display( get_current_user_id() ),
185 $post_array['menu-item-title'][ $menu_item_db_id ],
186 $post_array['menu-name']
187 ),
188 $this->type,
189 );
190 }
191
192 break;
193 }
194 }
195 }
196
197 $added_items = array_diff( $new_items, $old_items );
198 if ( in_array( $menu_item_db_id, $added_items, true ) ) {
199 return array(
200 sprintf(
201 /* translators: 1: Blog name, 2: User's display name, 3: Menu item, 4: Menu name */
202 esc_html__( '%1$s %2$s added item "%3$s" to menu "%4$s"', 'defender-security' ),
203 $blog_name,
204 $this->get_user_display( get_current_user_id() ),
205 $post_array['menu-item-title'][ $menu_item_db_id ],
206 $post_array['menu-name']
207 ),
208 $this->type,
209 );
210 }
211 }
212
213 return false;
214 }
215
216 /**
217 * Callback for when a menu item is deleted. It logs the deletion if the item existed in the cached menu.
218 *
219 * @return array|bool
220 */
221 public function menu_item_deleted_callback() {
222 $post_array = filter_input_array( INPUT_POST );
223
224 if ( ! isset( $post_array['menu-name'] ) ) {
225 return false;
226 }
227
228 $func_args = func_get_args();
229 $menu_item_db_id = $func_args[1]['post_ID'];
230 $menu_id = (int) $post_array['menu'];
231 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
232 $old_menu = $this->get_cached_menu( $menu_id );
233
234 if ( ! empty( $old_menu ) && is_array( $old_menu ) ) {
235 $old_items = array_column( $old_menu['items'], 'item_id' );
236 if ( in_array( $menu_item_db_id, $old_items, true ) ) {
237 $key = array_search( $menu_item_db_id, $old_items, true );
238
239 return array(
240 sprintf(
241 /* translators: 1: Blog name, 2: User's display name, 3: Menu item, 4: Menu name */
242 esc_html__( '%1$s %2$s removed item "%3$s" from menu "%4$s"', 'defender-security' ),
243 $blog_name,
244 $this->get_user_display( get_current_user_id() ),
245 $old_menu['items'][ $key ]['title'],
246 $post_array['menu-name']
247 ),
248 $this->type,
249 );
250 }
251 }
252
253 return false;
254 }
255
256 /**
257 * Callback for when a new menu is created. It logs the creation event.
258 *
259 * @return array
260 */
261 public function menu_created_callback(): array {
262 $func_args = func_get_args();
263 $menu_data = $func_args[1]['menu_data'];
264 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
265
266 return array(
267 sprintf(
268 /* translators: 1: Blog name, 2: User's display name, 3: Menu name */
269 esc_html__( '%1$s %2$s created a new menu "%3$s"', 'defender-security' ),
270 $blog_name,
271 $this->get_user_display( get_current_user_id() ),
272 $menu_data['menu-name']
273 ),
274 $this->type,
275 );
276 }
277
278 /**
279 * Callback for when a menu is updated. It logs the update if there are changes to the menu structure.
280 *
281 * @return array|bool
282 */
283 public function menu_updated_callback() {
284 $func_args = func_get_args();
285 $menu_id = $func_args[1]['menu_id'];
286 $menu_data = $func_args[1]['menu_data'];
287 $post_array = filter_input_array( INPUT_POST );
288 $new_items = isset( $post_array['menu-item-title'] ) ? array_keys( $post_array['menu-item-title'] ) : array();
289 $old_menu = $this->get_cached_menu( $menu_id );
290 $old_items = array_column( $old_menu['items'], 'item_id' );
291
292 sort( $new_items );
293 sort( $old_items );
294 if ( $new_items === $old_items ) {
295 // Check if item title, position or parent is changed.
296 $is_any_item_changed = false;
297 foreach ( $old_menu['items'] as $old_items ) {
298 if (
299 $old_items['title'] !== $post_array['menu-item-title'][ $old_items['item_id'] ]
300 || $old_items['menu_order'] !== (int) $post_array['menu-item-position'][ $old_items['item_id'] ]
301 || $old_items['menu_item_parent'] !== $post_array['menu-item-parent-id'][ $old_items['item_id'] ]
302 ) {
303 $is_any_item_changed = true;
304 break;
305 }
306 }
307
308 if ( false === $is_any_item_changed ) {
309 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
310
311 return array(
312 sprintf(
313 /* translators: 1: Blog name, 2: User's display name, 3: Menu name */
314 esc_html__( '%1$s %2$s updated menu "%3$s"', 'defender-security' ),
315 $blog_name,
316 $this->get_user_display( get_current_user_id() ),
317 $menu_data['menu-name']
318 ),
319 $this->type,
320 );
321 }
322 }
323
324 return false;
325 }
326
327 /**
328 * Callback for when a menu is deleted. It logs the deletion event.
329 *
330 * @return array
331 */
332 public function menu_deleted_callback(): array {
333 $func_args = func_get_args();
334 $menu_id = $func_args[1]['term_id'];
335 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
336 $old_menu = $this->get_cached_menu( $menu_id );
337
338 return array(
339 sprintf(
340 /* translators: 1: Blog name, 2: User's display name, 3: Menu name */
341 esc_html__( '%1$s %2$s deleted menu "%3$s"', 'defender-security' ),
342 $blog_name,
343 $this->get_user_display( get_current_user_id() ),
344 $old_menu['name']
345 ),
346 $this->type,
347 );
348 }
349
350 /**
351 * Retrieves a cached menu from the runtime cache based on the term ID.
352 *
353 * @param int $term_id Menu term ID.
354 *
355 * @return bool|array
356 */
357 public function get_cached_menu( $term_id ) {
358 $cached = Array_Cache::get( 'menu_updated', 'audit', array() );
359 foreach ( $cached as $menu_arr ) {
360 if ( $term_id === $menu_arr['term_id'] ) {
361 return $menu_arr;
362 }
363 }
364
365 return false;
366 }
367
368 /**
369 * Checks if the current request is doing an auto save.
370 *
371 * @return bool
372 */
373 public function doing_autosave(): bool {
374 return defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE;
375 }
376 }
377