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 / controller / class-dashboard.php

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

626 lines 19.4 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 $nonce = HTTP::get( '_wpnonce' );
217
218 // Fallback completion marker for setup wizard integrations rendered on dashboard.
219 // This prevents onboarding loops if client-side completion request fails.
220 if (
221 'setup_wizard' === $wizard_source
222 && in_array( $wizard_action, array( 'view_results', 'close_wizard', 'finish_wizard' ), true )
223 && current_user_can( 'manage_options' )
224 && is_string( $nonce )
225 && false !== wp_verify_nonce( $nonce, 'wpdef_setup_wizard' )
226 ) {
227 update_site_option( 'wp_defender_shown_activator', true );
228 }
229
230 $show_onboarding = \WP_Defender\Model\Onboard::maybe_show_onboarding();
231 $api_error = HTTP::get( 'api_error' );
232 $hub_connection_source = HTTP::get( 'hub_connection_source' );
233 $hub_connect_recovery = ! $show_onboarding
234 && is_string( $api_error )
235 && '' !== $api_error
236 && 'profile_menu' !== $hub_connection_source
237 && ! Hub_Connector_Component::is_logged_in()
238 && ! Hub_Connector_Component::is_wpmudev_dashboard_connected();
239
240 if ( $show_onboarding || $hub_connect_recovery ) {
241 add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
242 }
243
244 $handle = 'defender-ui-dashboard';
245 wp_enqueue_script(
246 $handle,
247 WP_DEFENDER_BASE_URL . 'assets/js/dashboard-ui.js',
248 array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
249 DEFENDER_VERSION,
250 true
251 );
252 wp_set_script_translations( $handle, 'wpdef' );
253
254 $setup_wizard_data = wd_di()->get( Setup_Wizard::class )->data_frontend();
255 $tracking_data = wd_di()->get( Data_Tracking::class )->get_dashboard_notice_data();
256 $dashboard_data = $this->dump_routes_and_nonces();
257 $firewall_data = wd_di()->get( Firewall::class )->dump_routes_and_nonces();
258 $scan_data = wd_di()->get( \WP_Defender\Controller\Scan::class )->dump_routes_and_nonces();
259 $routes = array_merge(
260 $setup_wizard_data['routes'] ?? array(),
261 $tracking_data['routes'] ?? array(),
262 $dashboard_data['routes'] ?? array(),
263 $firewall_data['routes'] ?? array(),
264 $scan_data['routes'] ?? array(),
265 );
266 $nonces = array_merge(
267 $setup_wizard_data['nonces'] ?? array(),
268 $tracking_data['nonces'] ?? array(),
269 $dashboard_data['nonces'] ?? array(),
270 $firewall_data['nonces'] ?? array(),
271 $scan_data['nonces'] ?? array(),
272 );
273 unset( $setup_wizard_data['routes'], $setup_wizard_data['nonces'] );
274 unset( $tracking_data['routes'], $tracking_data['nonces'] );
275 unset( $firewall_data['routes'], $firewall_data['nonces'] );
276 wp_localize_script(
277 $handle,
278 'defenderUIData',
279 array_merge(
280 $this->get_shared_data(),
281 $this->get_page_data(),
282 // Welcome modal's details.
283 wd_di()->get( Feature_Modal::class )->get_dashboard_modals(),
284 // Specific data.
285 array(
286 'showOnboarding' => $show_onboarding,
287 'dashboardRequiredNotice' => $this->get_dashboard_required_notice_data(),
288 'routes' => $routes,
289 'nonces' => $nonces,
290 ),
291 $setup_wizard_data,
292 $tracking_data,
293 wd_di()->get( \WP_Defender\Controller\Scan::class )->get_initial_scan_data()
294 )
295 );
296
297 wp_enqueue_style(
298 $handle,
299 WP_DEFENDER_BASE_URL . 'assets/css/core-ui.css',
300 array(),
301 DEFENDER_VERSION
302 );
303
304 $this->enqueue_main_assets();
305 }
306
307 /**
308 * Get the state and action URLs for the Dashboard plugin required modal.
309 *
310 * @return array
311 */
312 private function get_dashboard_required_notice_data(): array {
313 return array(
314 'isProPlugin' => WP_DEFENDER_PRO_PATH === DEFENDER_PLUGIN_BASENAME,
315 'dashboardActive' => $this->is_dash_activated(),
316 'dashboardInstalled' => $this->is_dash_installed(),
317 'dashboardPageUrl' => network_admin_url( 'admin.php?page=wpmudev' ),
318 'activateUrl' => add_query_arg(
319 array(
320 '_wpnonce' => wp_create_nonce( 'activate-plugin_wpmudev-updates/update-notifications.php' ),
321 'action' => 'activate',
322 'plugin' => 'wpmudev-updates/update-notifications.php',
323 ),
324 network_admin_url( 'plugins.php' )
325 ),
326 'installUrl' => add_query_arg(
327 array(
328 '_wpnonce' => wp_create_nonce( 'install-plugin_install_wpmudev_dash' ),
329 'action' => 'install-plugin',
330 'plugin' => 'install_wpmudev_dash',
331 ),
332 network_admin_url( 'update.php' )
333 ),
334 'dismissed' => (bool) get_site_option( self::DASHBOARD_REQUIRED_NOTICE_OPTION, false ),
335 );
336 }
337
338 /**
339 * Supply WordPress with the WPMU DEV Dashboard package details.
340 *
341 * @param mixed $result Existing Plugins API result.
342 * @param string $action Requested Plugins API action.
343 * @param object $args Requested plugin arguments.
344 *
345 * @return mixed
346 */
347 public function filter_dashboard_plugin_info( $result, $action, $args ) {
348 if (
349 'plugin_information' !== $action
350 || ! is_object( $args )
351 || ! isset( $args->slug )
352 || '' === trim( (string) $args->slug )
353 || false === strpos( $args->slug, 'install_wpmudev_dash' )
354 ) {
355 return $result;
356 }
357
358 $plugin = new \stdClass();
359 $plugin->name = 'WPMU DEV Dashboard';
360 $plugin->slug = 'wpmu-dev-dashboard';
361 $plugin->version = '';
362 $plugin->rating = 100;
363 $plugin->homepage = 'https://wpmudev.com/project/wpmu-dev-dashboard/';
364 $plugin->download_link = 'https://wpmudev.com/api/dashboard/v1/download-dashboard';
365 $plugin->tested = get_bloginfo( 'version' );
366
367 return $plugin;
368 }
369
370 /**
371 * Adds onboarding body classes on dashboard when onboarding wizard is active.
372 *
373 * @param string $classes Existing admin body classes.
374 *
375 * @return string
376 */
377 public function admin_body_class( $classes ): string {
378 $classes .= ' wdf-onboarding-active ';
379
380 return $classes;
381 }
382
383 /**
384 * Returns the current hardening (security tweaks) issue count via AJAX.
385 *
386 * @return Response
387 * @defender_route
388 * @defender_redirect
389 */
390 public function get_hardening_count(): Response {
391 $security_tweaks = wd_di()->get( \WP_Defender\Controller\Security_Tweaks::class )->dashboard_widget();
392
393 return new Response(
394 true,
395 array(
396 'count' => (int) ( $security_tweaks['summary']['issues_count'] ?? 0 ),
397 )
398 );
399 }
400
401 /**
402 * Handles the request to hide new features modal.
403 *
404 * @param Request $request The request object containing data.
405 *
406 * @return Response The response object indicating success or failure.
407 * @defender_route
408 */
409 public function hide_new_features( Request $request ): Response {
410 $data = $request->get_data(
411 array(
412 'intention' => array(
413 'type' => 'string',
414 'sanitize' => 'sanitize_text_field',
415 ),
416 )
417 );
418 $intention = $data['intention'] ?? false;
419 if ( 'welcome_modal' === $intention ) {
420 Feature_Modal::delete_modal_key();
421 }
422
423 return new Response( true, array() );
424 }
425
426 /**
427 * Activate Global IP submodule with the enabled Auto sync option.
428 *
429 * @return Response
430 * @defender_route
431 */
432 public function activate_global_ip(): Response {
433 // Changes for Global IP.
434 $model = wd_di()->get( Global_Ip_Lockout::class );
435 $model->enabled = true;
436 $model->blocklist_autosync = true;
437 $model->save();
438 // Clear Global IP reminder.
439 wd_di()->get( Global_IP_Component::class )->delete_dashboard_notice_reminder();
440 // Changes for Hub.
441 Config_Hub_Helper::set_clear_active_flag();
442
443 return new Response(
444 true,
445 array(
446 'redirect' => network_admin_url( 'admin.php?page=wdf-ip-lockout&view=global-ip' ),
447 'interval' => 1,
448 )
449 );
450 }
451
452 /**
453 * Activate Session Protection submodule.
454 *
455 * @return Response
456 * @defender_route
457 */
458 public function activate_session_protection(): Response {
459 $model = wd_di()->get( Session_Protection::class );
460 $model->enabled = true;
461 $model->save();
462 // Changes for Hub.
463 Config_Hub_Helper::set_clear_active_flag();
464
465 return new Response(
466 true,
467 array(
468 'redirect' => network_admin_url( 'admin.php?page=wdf-advanced-tools&view=session-protection' ),
469 'interval' => 1,
470 )
471 );
472 }
473
474 /**
475 * Remove Global IP notice reminder.
476 *
477 * @return Response
478 * @defender_route
479 */
480 public function remove_global_ip_notice_reminder(): Response {
481 wd_di()->get( Global_IP_Component::class )->delete_dashboard_notice_reminder();
482
483 return new Response( true, array() );
484 }
485
486 /**
487 * Dismiss the one-time report-schedule notice set during the 6.1.0 upgrade.
488 *
489 * @return Response
490 * @defender_route
491 */
492 public function dismiss_report_schedule_notice(): Response {
493 delete_site_option( self::REPORT_SCHEDULE_NOTICE_OPTION );
494
495 return new Response( true, array() );
496 }
497
498 /**
499 * Permanently dismiss the Dashboard plugin required modal for this site.
500 *
501 * @return Response
502 * @defender_route
503 */
504 public function dismiss_dashboard_required_notice(): Response {
505 update_site_option( self::DASHBOARD_REQUIRED_NOTICE_OPTION, true );
506
507 return new Response( true, array() );
508 }
509
510 /**
511 * Toggle a dashboard feature by feature key.
512 *
513 * @param Request $request The current request data.
514 *
515 * @return Response
516 * @defender_route
517 */
518 public function toggle_feature( Request $request ): Response {
519 $data = $request->get_data(
520 array(
521 'feature' => array(
522 'type' => 'string',
523 'sanitize' => 'sanitize_text_field',
524 ),
525 )
526 );
527 $feature = $data['feature'] ?? '';
528
529 $feature_controllers = array(
530 'antibot' => Antibot_Global_Firewall::class,
531 'audit' => Audit_Logging::class,
532 'session_protection' => Session_Protection_Controller::class,
533 );
534 if ( isset( $feature_controllers[ $feature ] ) ) {
535 return wd_di()->get( $feature_controllers[ $feature ] )->save_settings( $request );
536 }
537
538 return new Response(
539 false,
540 array(
541 'message' => esc_html__( 'Unsupported feature toggle request.', 'defender-security' ),
542 )
543 );
544 }
545
546 /**
547 * Removes settings for all submodules.
548 */
549 public function remove_settings() {
550 wd_di()->get( Feature_Modal::class )->upgrade_site_options();
551
552 delete_site_option( self::REPORT_SCHEDULE_NOTICE_OPTION );
553 }
554
555 /**
556 * Delete all the data & the cache.
557 */
558 public function remove_data() {
559 delete_site_option( self::DASHBOARD_REQUIRED_NOTICE_OPTION );
560 }
561
562 /**
563 * Provides data for the frontend.
564 *
565 * @return array An array of data for the frontend.
566 */
567 public function data_frontend(): array {
568 [ $endpoints, $nonces ] = Route::export_routes( 'dashboard' );
569 $firewall = wd_di()->get( Firewall::class );
570
571 return array_merge(
572 wd_di()->get( Feature_Modal::class )->get_dashboard_modals(),
573 array(
574 'scan' => wd_di()->get( Scan::class )->data_frontend(),
575 'firewall' => $firewall->data_frontend(),
576 'blocklist_monitor' => wd_di()->get( Blocklist_Monitor::class )->data_frontend(),
577 'blacklist' => array(
578 'nonces' => $nonces,
579 'endpoints' => $endpoints,
580 ),
581 'two_fa' => wd_di()->get( Two_Factor::class )->data_frontend(),
582 'advanced_tools' => array(
583 'mask_login' => wd_di()->get( Mask_Login::class )->dashboard_widget(),
584 'security_headers' => wd_di()->get( Security_Headers::class )->dashboard_widget(),
585 'pwned_passwords' => wd_di()->get( Password_Protection::class )->dashboard_widget(),
586 'captcha' => wd_di()->get( Captcha::class )->dashboard_widget(),
587 'strong_passwords' => wd_di()->get( Strong_Password::class )->dashboard_widget(),
588 ),
589 'security_tweaks' => wd_di()->get( Security_Tweaks::class )->dashboard_widget(),
590 'notifications' => wd_di()->get( Notification::class )->data_frontend(),
591 'settings' => wd_di()->get( Main_Setting::class )->data_frontend(),
592 'countries' => $firewall->dashboard_widget(),
593 'global_ip' => wd_di()->get( Global_Ip::class )->data_frontend(),
594 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
595 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
596 )
597 );
598 }
599
600 /**
601 * Converts the current object state to an array.
602 *
603 * @return array The array representation of the object.
604 */
605 public function to_array(): array {
606 return array();
607 }
608
609 /**
610 * Imports data into the model.
611 *
612 * @param array $data Data to be imported into the model.
613 */
614 public function import_data( array $data ) {
615 }
616
617 /**
618 * Exports strings.
619 *
620 * @return array An array of strings.
621 */
622 public function export_strings(): array {
623 return array();
624 }
625 }
626