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

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

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