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

376 lines 10.9 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 $menu['items'][] = array(
85 'item_id' => $item->ID,
86 'title' => $item->title,
87 'url' => $item->url,
88 'object' => $item->object,
89 'menu_order' => $item->menu_order,
90 'menu_item_parent' => $item->menu_item_parent,
91 );
92 }
93 }
94 }
95
96 return $menu;
97 }
98
99 /**
100 * Returns an array of hooks associated with various menu actions for auditing purposes.
101 *
102 * @return array
103 */
104 public function get_hooks(): array {
105 return array(
106 'wp_create_nav_menu' => array(
107 'args' => array( 'term_id', 'menu_data' ),
108 'callback' => array( self::class, 'menu_created_callback' ),
109 'event_type' => Audit_Log::EVENT_TYPE_MENU,
110 'action_type' => self::ACTION_CREATED,
111 ),
112 'wp_update_nav_menu' => array(
113 'args' => array( 'menu_id', 'menu_data' ),
114 'callback' => array( self::class, 'menu_updated_callback' ),
115 'event_type' => Audit_Log::EVENT_TYPE_MENU,
116 'action_type' => self::ACTION_UPDATED,
117 ),
118 'wp_delete_nav_menu' => array(
119 'args' => array( 'term_id' ),
120 'callback' => array( self::class, 'menu_deleted_callback' ),
121 'event_type' => Audit_Log::EVENT_TYPE_MENU,
122 'action_type' => self::ACTION_DELETED,
123 ),
124 'wp_update_nav_menu_item' => array(
125 'args' => array( 'menu_id', 'menu_item_db_id', 'args' ),
126 'callback' => array( self::class, 'menu_item_updated_callback' ),
127 'event_type' => Audit_Log::EVENT_TYPE_MENU,
128 'action_type' => self::ACTION_UPDATED,
129 ),
130 'delete_post' => array(
131 'args' => array( 'post_ID' ),
132 'callback' => array( self::class, 'menu_item_deleted_callback' ),
133 'event_type' => Audit_Log::EVENT_TYPE_MENU,
134 'action_type' => self::ACTION_DELETED,
135 ),
136 );
137 }
138
139 /**
140 * Callback for when a menu item is updated. It checks if the update is significant for logging.
141 *
142 * @return array|bool
143 */
144 public function menu_item_updated_callback() {
145 if ( $this->doing_autosave() ) {
146 return false;
147 }
148
149 // Filter $_POST.
150 $post_array = filter_input_array( INPUT_POST );
151
152 if ( ! isset( $post_array['menu-name'] ) ) {
153 return false;
154 }
155
156 $func_args = func_get_args();
157 $menu_id = $func_args[1]['menu_id'];
158 $menu_item_db_id = $func_args[1]['menu_item_db_id'];
159 $args = $func_args[1]['args'];
160 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
161 $old_menu = $this->get_cached_menu( $menu_id );
162
163 if ( isset( $old_menu['items'] ) && is_array( $old_menu['items'] ) ) {
164 $old_items = array_column( $old_menu['items'], 'item_id' );
165 $new_items = array_keys( $post_array['menu-item-title'] );
166
167 if ( array() !== $old_menu['items'] ) {
168 foreach ( $old_menu['items'] as $old_item ) {
169 if ( $menu_item_db_id === $old_item['item_id'] ) {
170 if (
171 $old_item['menu_order'] !== $args['menu-item-position']
172 || $old_item['menu_item_parent'] !== $args['menu-item-parent-id']
173 || ( isset( $args['menu-item-title'] ) && '' !== $args['menu-item-title'] && $old_item['title'] !== $args['menu-item-title'] )
174 ) {
175 return array(
176 sprintf(
177 /* translators: 1: Blog name, 2: User's display name, 3: Menu item, 4: Menu name */
178 esc_html__( '%1$s %2$s updated item "%3$s" from menu "%4$s"', 'defender-security' ),
179 $blog_name,
180 $this->get_user_display( get_current_user_id() ),
181 $post_array['menu-item-title'][ $menu_item_db_id ],
182 $post_array['menu-name']
183 ),
184 $this->type,
185 );
186 }
187
188 break;
189 }
190 }
191 }
192
193 $added_items = array_diff( $new_items, $old_items );
194 if ( in_array( $menu_item_db_id, $added_items, true ) ) {
195 return array(
196 sprintf(
197 /* translators: 1: Blog name, 2: User's display name, 3: Menu item, 4: Menu name */
198 esc_html__( '%1$s %2$s added item "%3$s" to menu "%4$s"', 'defender-security' ),
199 $blog_name,
200 $this->get_user_display( get_current_user_id() ),
201 $post_array['menu-item-title'][ $menu_item_db_id ],
202 $post_array['menu-name']
203 ),
204 $this->type,
205 );
206 }
207 }
208
209 return false;
210 }
211
212 /**
213 * Callback for when a menu item is deleted. It logs the deletion if the item existed in the cached menu.
214 *
215 * @return array|bool
216 */
217 public function menu_item_deleted_callback() {
218 $post_array = filter_input_array( INPUT_POST );
219
220 if ( ! isset( $post_array['menu-name'] ) ) {
221 return false;
222 }
223
224 $func_args = func_get_args();
225 $menu_item_db_id = $func_args[1]['post_ID'];
226 $menu_id = (int) $post_array['menu'];
227 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
228 $old_menu = $this->get_cached_menu( $menu_id );
229
230 if ( isset( $old_menu['items'] ) && is_array( $old_menu['items'] ) && array() !== $old_menu['items'] ) {
231 $old_items = array_column( $old_menu['items'], 'item_id' );
232 if ( in_array( $menu_item_db_id, $old_items, true ) ) {
233 $key = array_search( $menu_item_db_id, $old_items, true );
234
235 return array(
236 sprintf(
237 /* translators: 1: Blog name, 2: User's display name, 3: Menu item, 4: Menu name */
238 esc_html__( '%1$s %2$s removed item "%3$s" from menu "%4$s"', 'defender-security' ),
239 $blog_name,
240 $this->get_user_display( get_current_user_id() ),
241 $old_menu['items'][ $key ]['title'],
242 $post_array['menu-name']
243 ),
244 $this->type,
245 );
246 }
247 }
248
249 return false;
250 }
251
252 /**
253 * Callback for when a new menu is created. It logs the creation event.
254 *
255 * @return array
256 */
257 public function menu_created_callback(): array {
258 $func_args = func_get_args();
259 $menu_data = $func_args[1]['menu_data'];
260 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
261
262 return array(
263 sprintf(
264 /* translators: 1: Blog name, 2: User's display name, 3: Menu name */
265 esc_html__( '%1$s %2$s created a new menu "%3$s"', 'defender-security' ),
266 $blog_name,
267 $this->get_user_display( get_current_user_id() ),
268 $menu_data['menu-name']
269 ),
270 $this->type,
271 );
272 }
273
274 /**
275 * Callback for when a menu is updated. It logs the update if there are changes to the menu structure.
276 *
277 * @return array|bool
278 */
279 public function menu_updated_callback() {
280 $func_args = func_get_args();
281 $menu_id = $func_args[1]['menu_id'];
282 $menu_data = $func_args[1]['menu_data'];
283 $post_array = filter_input_array( INPUT_POST );
284 $new_items = isset( $post_array['menu-item-title'] ) ? array_keys( $post_array['menu-item-title'] ) : array();
285 $old_menu = $this->get_cached_menu( $menu_id );
286 if ( ! is_array( $old_menu ) || ! isset( $old_menu['items'] ) || ! is_array( $old_menu['items'] ) ) {
287 return false;
288 }
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'] ?? $old_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