PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / trunk
Defender Security – Malware Scanner, Login Security & Firewall vtrunk
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 3.1.1 All 140 releases
defender-security / src / controller / class-dashboard.php

class-dashboard.php in Defender Security – Malware Scanner, Login Security & Firewall trunk, at src/controller/class-dashboard.php

623 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles the main admin page.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use WP_Defender\Component\Audit;
11 use WP_Defender\Event;
12 use Calotes\Helper\HTTP;
13 use Calotes\Helper\Route;
14 use WP_Defender\Model\Setting\Login_Lockout;
15 use WP_Defender\Model\Setting\Notfound_Lockout;
16 use WP_Defender\Model\Setting\User_Agent_Lockout;
17 use WP_Defender\Traits\Defender_Dashboard_Client;
18 use WP_Defender\Traits\IO;
19 use Calotes\Component\Request;
20 use Calotes\Component\Response;
21 use WP_Defender\Traits\Formats;
22 use WP_Defender\Behavior\WPMUDEV;
23 use WP_Defender\Component\Feature_Modal;
24 use WP_Defender\Component\Hub_Connector as Hub_Connector_Component;
25 use WP_Defender\Model\Setting\Audit_Logging as Audit_Logging_Settings;
26 use WP_Defender\Model\Audit_Log;
27 use WP_Defender\Model\Setting\Global_Ip_Lockout;
28 use WP_Defender\Component\Config\Config_Hub_Helper;
29 use WP_Defender\Component\IP\Global_IP as Global_IP_Component;
30 use WP_Defender\Model\Setting\Session_Protection;
31 use WP_Defender\Controller\Session_Protection as Session_Protection_Controller;
32
33 /**
34 * Handles the main admin page.
35 */
36 class Dashboard extends Event {
37
38 use IO;
39 use Formats;
40 use Defender_Dashboard_Client;
41
42 /**
43 * The slug identifier for this controller.
44 *
45 * @var string
46 */
47 public $slug = 'wp-defender';
48
49 /**
50 * Site option key for the one-time report-schedule upgrade notice.
51 */
52 public const REPORT_SCHEDULE_NOTICE_OPTION = 'wd_show_report_schedule_notice';
53
54 /**
55 * Site-wide dismissal key for the Dashboard plugin required modal.
56 */
57 public const DASHBOARD_REQUIRED_NOTICE_OPTION = 'wpdef_dashboard_required_notice_dismissed';
58
59 /**
60 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
61 */
62 public function __construct() {
63 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
64 $this->add_main_page();
65 $this->register_routes();
66 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
67 add_filter( 'custom_menu_order', '__return_true' );
68 add_filter( 'menu_order', array( $this, 'menu_order' ) );
69 add_filter( 'plugins_api', array( $this, 'filter_dashboard_plugin_info' ), 101, 3 );
70 add_action( 'admin_init', array( $this, 'maybe_redirect_notification_request' ), 99 );
71 }
72
73 /**
74 * Because we move the notifications on separate modules, so links from HUB should be redirected to correct URL.
75 *
76 * @return void
77 */
78 public function maybe_redirect_notification_request(): void {
79 $page = HTTP::get( 'page' );
80 if ( ! in_array( $page, array( 'wdf-scan', 'wdf-ip-lockout', 'wdf-hardener', 'wdf-logging' ), true ) ) {
81 return;
82 }
83 $view = HTTP::get( 'view' );
84 if ( in_array( $view, array( 'reporting', 'notification', 'report' ), true ) ) {
85 wp_safe_redirect( network_admin_url( 'admin.php?page=wdf-notification' ) );
86 exit;
87 }
88 }
89
90 /**
91 * Filter out the defender menu for changing text.
92 *
93 * @param array $menu_order The current menu order.
94 *
95 * @return array
96 */
97 public function menu_order( $menu_order ) {
98 global $submenu;
99 if ( isset( $submenu['wp-defender'] ) ) {
100 $defender_menu = $submenu['wp-defender'];
101 $defender_menu[0][0] = esc_html__( 'Dashboard', 'defender-security' );
102 $defender_menu = array_values( $defender_menu );
103 // Change the global $submenu variable, because otherwise the menu name/order will not change.
104 $submenu['wp-defender'] = $defender_menu; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
105 }
106
107 global $menu;
108 // Get the total scanning active issues.
109 $count = wd_di()->get( \WP_Defender\Component\Scan::class )->indicator_issue_count();
110
111 $indicator = $count > 0
112 ? ' <span class="update-plugins wd-issue-indicator-sidebar"></span>'
113 : null;
114 foreach ( $menu as $k => $item ) {
115 if ( 'wp-defender' === $item[2] ) {
116 // Add a badge next to the "Defender" menu item in the global $menu variable.
117 $menu[ $k ][0] .= $indicator; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
118 }
119 }
120
121 return $menu_order;
122 }
123
124 /**
125 * Registers the main page in the WordPress admin menu.
126 */
127 protected function add_main_page() {
128 $this->register_page(
129 $this->get_page_title(),
130 $this->parent_slug,
131 array( $this, 'main_view' ),
132 null,
133 $this->get_menu_icon(),
134 $this->get_menu_title()
135 );
136 }
137
138 /**
139 * Renders the main view for this page.
140 */
141 public function main_view() {
142 $this->render( 'main' );
143 }
144
145 /**
146 * Page-specific data for a concrete controller.
147 *
148 * @return array
149 */
150 protected function get_page_data(): array {
151 $security_tweaks = wd_di()->get( \WP_Defender\Controller\Security_Tweaks::class );
152 $security_tweaks->refresh_tweaks_status();
153 $security_tweaks_data = $security_tweaks->dashboard_widget();
154
155 $audit_model = wd_di()->get( Audit_Logging_Settings::class );
156 $enabled_audit = $audit_model->is_active();
157 // Audit summary.
158 $audit_events_logged = 0;
159 $last_event_time = esc_html__( '-', 'defender-security' );
160 if ( $enabled_audit ) {
161 $audit_events_logged = Audit::get_audit_events_per_week();
162 $audit_last = Audit_Log::get_last();
163 if ( is_object( $audit_last ) ) {
164 $last_event_time = $this->get_date( $audit_last->timestamp );
165 }
166 }
167
168 // Firewall summary.
169 $firewall = wd_di()->get( Firewall::class )->get_summary();
170 // Different lockout types.
171 $enabled_login = wd_di()->get( Login_Lockout::class )->enabled;
172 $enabled_nf = wd_di()->get( Notfound_Lockout::class )->enabled;
173 $enabled_ua = wd_di()->get( User_Agent_Lockout::class )->enabled;
174
175 return array(
176 'defenderSetupNonce' => wp_create_nonce( 'defender_quick_setup' ),
177 'securityTweaks' => $security_tweaks_data['summary']['issues_count'],
178 'scanData' => array(
179 'numberIssues' => wd_di()->get( \WP_Defender\Component\Scan::class )->indicator_issue_count(),
180 'settings' => wd_di()->get( \WP_Defender\Model\Setting\Scan::class )->export(),
181 // Scan routes & nonces are set above.
182 ),
183 'firewallData' => array(
184 'enabledLocalFirewall' => $enabled_login || $enabled_nf || $enabled_ua,
185 'enabledLogin' => $enabled_login,
186 'enabledNotFound' => $enabled_nf,
187 'enabledUserAgent' => $enabled_ua,
188 'loginLockoutMonth' => $firewall['lockout_login_this_month'],
189 'nfLockoutMonth' => $firewall['lockout_404_this_month'],
190 'uaLockoutMonth' => $firewall['lockout_ua_this_month'],
191 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
192 ),
193 'site_id' => wd_di()->get( WPMUDEV::class )->get_site_id(),
194 'auditData' => array(
195 'enabled' => $enabled_audit,
196 'eventsLogged' => $audit_events_logged,
197 'lastEvent' => $last_event_time,
198 ),
199 'sessionProtection' => wd_di()->get( Session_Protection::class )->export(),
200 'showReportScheduleNotice' => ! defender_is_wp_org_version()
201 && (bool) get_site_option( self::REPORT_SCHEDULE_NOTICE_OPTION, false ),
202 );
203 }
204
205 /**
206 * Enqueues scripts and styles for this page.
207 * Only enqueues assets if the page is active.
208 */
209 public function enqueue_assets() {
210 if ( ! $this->is_page_active() ) {
211 return;
212 }
213
214 $wizard_action = HTTP::get( 'wizard_action' );
215 $wizard_source = HTTP::get( 'source' );
216
217 // Fallback completion marker for setup wizard integrations rendered on dashboard.
218 // This prevents onboarding loops if client-side completion request fails.
219 if (
220 'setup_wizard' === $wizard_source
221 && in_array( $wizard_action, array( 'view_results', 'close_wizard', 'finish_wizard' ), true )
222 && current_user_can( 'manage_options' )
223 ) {
224 update_site_option( 'wp_defender_shown_activator', true );
225 }
226
227 $show_onboarding = \WP_Defender\Model\Onboard::maybe_show_onboarding();
228 $api_error = HTTP::get( 'api_error' );
229 $hub_connection_source = HTTP::get( 'hub_connection_source' );
230 $hub_connect_recovery = ! $show_onboarding
231 && is_string( $api_error )
232 && '' !== $api_error
233 && 'profile_menu' !== $hub_connection_source
234 && ! Hub_Connector_Component::is_logged_in()
235 && ! Hub_Connector_Component::is_wpmudev_dashboard_connected();
236
237 if ( $show_onboarding || $hub_connect_recovery ) {
238 add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
239 }
240
241 $handle = 'defender-ui-dashboard';
242 wp_enqueue_script(
243 $handle,
244 WP_DEFENDER_BASE_URL . 'assets/js/dashboard-ui.js',
245 array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
246 DEFENDER_VERSION,
247 true
248 );
249 wp_set_script_translations( $handle, 'wpdef' );
250
251 $setup_wizard_data = wd_di()->get( Setup_Wizard::class )->data_frontend();
252 $tracking_data = wd_di()->get( Data_Tracking::class )->get_dashboard_notice_data();
253 $dashboard_data = $this->dump_routes_and_nonces();
254 $firewall_data = wd_di()->get( Firewall::class )->dump_routes_and_nonces();
255 $scan_data = wd_di()->get( \WP_Defender\Controller\Scan::class )->dump_routes_and_nonces();
256 $routes = array_merge(
257 $setup_wizard_data['routes'] ?? array(),
258 $tracking_data['routes'] ?? array(),
259 $dashboard_data['routes'] ?? array(),
260 $firewall_data['routes'] ?? array(),
261 $scan_data['routes'] ?? array(),
262 );
263 $nonces = array_merge(
264 $setup_wizard_data['nonces'] ?? array(),
265 $tracking_data['nonces'] ?? array(),
266 $dashboard_data['nonces'] ?? array(),
267 $firewall_data['nonces'] ?? array(),
268 $scan_data['nonces'] ?? array(),
269 );
270 unset( $setup_wizard_data['routes'], $setup_wizard_data['nonces'] );
271 unset( $tracking_data['routes'], $tracking_data['nonces'] );
272 unset( $firewall_data['routes'], $firewall_data['nonces'] );
273 wp_localize_script(
274 $handle,
275 'defenderUIData',
276 array_merge(
277 $this->get_shared_data(),
278 $this->get_page_data(),
279 // Welcome modal's details.
280 wd_di()->get( Feature_Modal::class )->get_dashboard_modals(),
281 // Specific data.
282 array(
283 'showOnboarding' => $show_onboarding,
284 'dashboardRequiredNotice' => $this->get_dashboard_required_notice_data(),
285 'routes' => $routes,
286 'nonces' => $nonces,
287 ),
288 $setup_wizard_data,
289 $tracking_data,
290 wd_di()->get( \WP_Defender\Controller\Scan::class )->get_initial_scan_data()
291 )
292 );
293
294 wp_enqueue_style(
295 $handle,
296 WP_DEFENDER_BASE_URL . 'assets/css/showcase.css',
297 array(),
298 DEFENDER_VERSION
299 );
300
301 $this->enqueue_main_assets();
302 }
303
304 /**
305 * Get the state and action URLs for the Dashboard plugin required modal.
306 *
307 * @return array
308 */
309 private function get_dashboard_required_notice_data(): array {
310 return array(
311 'isProPlugin' => WP_DEFENDER_PRO_PATH === DEFENDER_PLUGIN_BASENAME,
312 'dashboardActive' => $this->is_dash_activated(),
313 'dashboardInstalled' => $this->is_dash_installed(),
314 'dashboardPageUrl' => network_admin_url( 'admin.php?page=wpmudev' ),
315 'activateUrl' => add_query_arg(
316 array(
317 '_wpnonce' => wp_create_nonce( 'activate-plugin_wpmudev-updates/update-notifications.php' ),
318 'action' => 'activate',
319 'plugin' => 'wpmudev-updates/update-notifications.php',
320 ),
321 network_admin_url( 'plugins.php' )
322 ),
323 'installUrl' => add_query_arg(
324 array(
325 '_wpnonce' => wp_create_nonce( 'install-plugin_install_wpmudev_dash' ),
326 'action' => 'install-plugin',
327 'plugin' => 'install_wpmudev_dash',
328 ),
329 network_admin_url( 'update.php' )
330 ),
331 'dismissed' => (bool) get_site_option( self::DASHBOARD_REQUIRED_NOTICE_OPTION, false ),
332 );
333 }
334
335 /**
336 * Supply WordPress with the WPMU DEV Dashboard package details.
337 *
338 * @param mixed $result Existing Plugins API result.
339 * @param string $action Requested Plugins API action.
340 * @param object $args Requested plugin arguments.
341 *
342 * @return mixed
343 */
344 public function filter_dashboard_plugin_info( $result, $action, $args ) {
345 if (
346 'plugin_information' !== $action
347 || ! is_object( $args )
348 || ! isset( $args->slug )
349 || '' === trim( (string) $args->slug )
350 || false === strpos( $args->slug, 'install_wpmudev_dash' )
351 ) {
352 return $result;
353 }
354
355 $plugin = new \stdClass();
356 $plugin->name = 'WPMU DEV Dashboard';
357 $plugin->slug = 'wpmu-dev-dashboard';
358 $plugin->version = '';
359 $plugin->rating = 100;
360 $plugin->homepage = 'https://wpmudev.com/project/wpmu-dev-dashboard/';
361 $plugin->download_link = 'https://wpmudev.com/api/dashboard/v1/download-dashboard';
362 $plugin->tested = get_bloginfo( 'version' );
363
364 return $plugin;
365 }
366
367 /**
368 * Adds onboarding body classes on dashboard when onboarding wizard is active.
369 *
370 * @param string $classes Existing admin body classes.
371 *
372 * @return string
373 */
374 public function admin_body_class( $classes ): string {
375 $classes .= ' wdf-onboarding-active ';
376
377 return $classes;
378 }
379
380 /**
381 * Returns the current hardening (security tweaks) issue count via AJAX.
382 *
383 * @return Response
384 * @defender_route
385 * @defender_redirect
386 */
387 public function get_hardening_count(): Response {
388 $security_tweaks = wd_di()->get( \WP_Defender\Controller\Security_Tweaks::class )->dashboard_widget();
389
390 return new Response(
391 true,
392 array(
393 'count' => (int) ( $security_tweaks['summary']['issues_count'] ?? 0 ),
394 )
395 );
396 }
397
398 /**
399 * Handles the request to hide new features modal.
400 *
401 * @param Request $request The request object containing data.
402 *
403 * @return Response The response object indicating success or failure.
404 * @defender_route
405 */
406 public function hide_new_features( Request $request ): Response {
407 $data = $request->get_data(
408 array(
409 'intention' => array(
410 'type' => 'string',
411 'sanitize' => 'sanitize_text_field',
412 ),
413 )
414 );
415 $intention = $data['intention'] ?? false;
416 if ( 'welcome_modal' === $intention ) {
417 Feature_Modal::delete_modal_key();
418 }
419
420 return new Response( true, array() );
421 }
422
423 /**
424 * Activate Global IP submodule with the enabled Auto sync option.
425 *
426 * @return Response
427 * @defender_route
428 */
429 public function activate_global_ip(): Response {
430 // Changes for Global IP.
431 $model = wd_di()->get( Global_Ip_Lockout::class );
432 $model->enabled = true;
433 $model->blocklist_autosync = true;
434 $model->save();
435 // Clear Global IP reminder.
436 wd_di()->get( Global_IP_Component::class )->delete_dashboard_notice_reminder();
437 // Changes for Hub.
438 Config_Hub_Helper::set_clear_active_flag();
439
440 return new Response(
441 true,
442 array(
443 'redirect' => network_admin_url( 'admin.php?page=wdf-ip-lockout&view=global-ip' ),
444 'interval' => 1,
445 )
446 );
447 }
448
449 /**
450 * Activate Session Protection submodule.
451 *
452 * @return Response
453 * @defender_route
454 */
455 public function activate_session_protection(): Response {
456 $model = wd_di()->get( Session_Protection::class );
457 $model->enabled = true;
458 $model->save();
459 // Changes for Hub.
460 Config_Hub_Helper::set_clear_active_flag();
461
462 return new Response(
463 true,
464 array(
465 'redirect' => network_admin_url( 'admin.php?page=wdf-advanced-tools&view=session-protection' ),
466 'interval' => 1,
467 )
468 );
469 }
470
471 /**
472 * Remove Global IP notice reminder.
473 *
474 * @return Response
475 * @defender_route
476 */
477 public function remove_global_ip_notice_reminder(): Response {
478 wd_di()->get( Global_IP_Component::class )->delete_dashboard_notice_reminder();
479
480 return new Response( true, array() );
481 }
482
483 /**
484 * Dismiss the one-time report-schedule notice set during the 6.1.0 upgrade.
485 *
486 * @return Response
487 * @defender_route
488 */
489 public function dismiss_report_schedule_notice(): Response {
490 delete_site_option( self::REPORT_SCHEDULE_NOTICE_OPTION );
491
492 return new Response( true, array() );
493 }
494
495 /**
496 * Permanently dismiss the Dashboard plugin required modal for this site.
497 *
498 * @return Response
499 * @defender_route
500 */
501 public function dismiss_dashboard_required_notice(): Response {
502 update_site_option( self::DASHBOARD_REQUIRED_NOTICE_OPTION, true );
503
504 return new Response( true, array() );
505 }
506
507 /**
508 * Toggle a dashboard feature by feature key.
509 *
510 * @param Request $request The current request data.
511 *
512 * @return Response
513 * @defender_route
514 */
515 public function toggle_feature( Request $request ): Response {
516 $data = $request->get_data(
517 array(
518 'feature' => array(
519 'type' => 'string',
520 'sanitize' => 'sanitize_text_field',
521 ),
522 )
523 );
524 $feature = $data['feature'] ?? '';
525
526 $feature_controllers = array(
527 'antibot' => Antibot_Global_Firewall::class,
528 'audit' => Audit_Logging::class,
529 'session_protection' => Session_Protection_Controller::class,
530 );
531 if ( isset( $feature_controllers[ $feature ] ) ) {
532 return wd_di()->get( $feature_controllers[ $feature ] )->save_settings( $request );
533 }
534
535 return new Response(
536 false,
537 array(
538 'message' => esc_html__( 'Unsupported feature toggle request.', 'defender-security' ),
539 )
540 );
541 }
542
543 /**
544 * Removes settings for all submodules.
545 */
546 public function remove_settings() {
547 wd_di()->get( Feature_Modal::class )->upgrade_site_options();
548
549 delete_site_option( self::REPORT_SCHEDULE_NOTICE_OPTION );
550 }
551
552 /**
553 * Delete all the data & the cache.
554 */
555 public function remove_data() {
556 delete_site_option( self::DASHBOARD_REQUIRED_NOTICE_OPTION );
557 }
558
559 /**
560 * Provides data for the frontend.
561 *
562 * @return array An array of data for the frontend.
563 */
564 public function data_frontend(): array {
565 [ $endpoints, $nonces ] = Route::export_routes( 'dashboard' );
566 $firewall = wd_di()->get( Firewall::class );
567
568 return array_merge(
569 wd_di()->get( Feature_Modal::class )->get_dashboard_modals(),
570 array(
571 'scan' => wd_di()->get( Scan::class )->data_frontend(),
572 'firewall' => $firewall->data_frontend(),
573 'blocklist_monitor' => wd_di()->get( Blocklist_Monitor::class )->data_frontend(),
574 'blacklist' => array(
575 'nonces' => $nonces,
576 'endpoints' => $endpoints,
577 ),
578 'two_fa' => wd_di()->get( Two_Factor::class )->data_frontend(),
579 'advanced_tools' => array(
580 'mask_login' => wd_di()->get( Mask_Login::class )->dashboard_widget(),
581 'security_headers' => wd_di()->get( Security_Headers::class )->dashboard_widget(),
582 'pwned_passwords' => wd_di()->get( Password_Protection::class )->dashboard_widget(),
583 'captcha' => wd_di()->get( Captcha::class )->dashboard_widget(),
584 'strong_passwords' => wd_di()->get( Strong_Password::class )->dashboard_widget(),
585 ),
586 'security_tweaks' => wd_di()->get( Security_Tweaks::class )->dashboard_widget(),
587 'notifications' => wd_di()->get( Notification::class )->data_frontend(),
588 'settings' => wd_di()->get( Main_Setting::class )->data_frontend(),
589 'countries' => $firewall->dashboard_widget(),
590 'global_ip' => wd_di()->get( Global_Ip::class )->data_frontend(),
591 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
592 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
593 )
594 );
595 }
596
597 /**
598 * Converts the current object state to an array.
599 *
600 * @return array The array representation of the object.
601 */
602 public function to_array(): array {
603 return array();
604 }
605
606 /**
607 * Imports data into the model.
608 *
609 * @param array $data Data to be imported into the model.
610 */
611 public function import_data( array $data ) {
612 }
613
614 /**
615 * Exports strings.
616 *
617 * @return array An array of strings.
618 */
619 public function export_strings(): array {
620 return array();
621 }
622 }
623