PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.0
Defender Security – Malware Scanner, Login Security & Firewall v6.2.0
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 6.2.0, at src/component/audit/class-menu-audit.php

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