PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.0.1
Defender Security – Malware Scanner, Login Security & Firewall v6.0.1
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-security-tweaks.php

class-security-tweaks.php in Defender Security – Malware Scanner, Login Security & Firewall 6.0.1, at src/controller/class-security-tweaks.php

1,268 lines 34.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles security tweaks operations.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use Countable;
11 use WP_Defender\Event;
12 use Calotes\Component\Request;
13 use Calotes\Component\Response;
14 use Calotes\Helper\Array_Cache;
15 use WP_Defender\Behavior\WPMUDEV;
16 use WP_Defender\Model\Notification\Tweak_Reminder;
17 use WP_Defender\Component\Config\Config_Hub_Helper;
18 use WP_Defender\Component\Security_Tweaks\Hide_Error;
19 use WP_Defender\Component\Security_Tweaks\WP_Version;
20 use WP_Defender\Component\Security_Tweaks\PHP_Version;
21 use WP_Defender\Component\Security_Tweaks\Prevent_PHP;
22 use WP_Defender\Component\Security_Tweaks\Change_Admin;
23 use WP_Defender\Component\Security_Tweaks\Security_Key;
24 use WP_Defender\Component\Security_Tweaks\Login_Duration;
25 use WP_Defender\Component\Security_Tweaks\Servers\Server;
26 use WP_Defender\Component\Security_Tweaks\Disable_XML_RPC;
27 use WP_Defender\Component\Security_Tweaks\Disable_Trackback;
28 use WP_Defender\Component\Security_Tweaks\Prevent_Enum_Users;
29 use WP_Defender\Component\Security_Tweaks\Disable_File_Editor;
30 use WP_Defender\Component\Security_Tweaks\Protect_Information;
31 use WP_Defender\Model\Setting\Security_Tweaks as Model_Security_Tweaks;
32
33 /**
34 * Contains methods for processing, reverting, ignoring security recommendations, and updating security reminders.
35 */
36 class Security_Tweaks extends Event {
37
38 /**
39 * The slug identifier for this controller.
40 *
41 * @var string
42 */
43 public $slug = 'wdf-hardener';
44
45 /**
46 * The model for handling the data.
47 *
48 * @var Model_Security_Tweaks
49 */
50 protected $model;
51
52 /**
53 * Components instance array.
54 *
55 * @var array
56 */
57 private $component_instances;
58
59 /**
60 * Instance of Security_Key.
61 *
62 * @var Security_Key
63 */
64 private $security_key;
65
66 /**
67 * Instance of Prevent_Enum_Users.
68 *
69 * @var Prevent_Enum_Users
70 */
71 private $prevent_enum_users;
72
73 public const STATUS_ISSUES = 'issues', STATUS_RESOLVE = 'fixed', STATUS_IGNORE = 'ignore', STATUS_RESTORE = 'restore';
74
75 /**
76 * Maps tweak slugs to frontend category identifiers.
77 *
78 * @var array<string,string>
79 */
80 private const TWEAK_CATEGORIES = array(
81 'disable-xml-rpc' => 'network',
82 'wp-version' => 'updates',
83 'hide-error' => 'error',
84 'php-version' => 'updates',
85 'replace-admin-username' => 'auth',
86 'security-key' => 'updates',
87 'login-duration' => 'auth',
88 'disable-trackback' => 'network',
89 'prevent-enum-users' => 'error',
90 'disable-file-editor' => 'access',
91 'protect-information' => 'error',
92 'prevent-php-executed' => 'access',
93 );
94
95 /**
96 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
97 */
98 public function __construct() {
99 $this->register_page(
100 esc_html__( 'Hardening', 'defender-security' ),
101 $this->slug,
102 array( $this, 'main_view' ),
103 $this->parent_slug
104 );
105 $this->model = wd_di()->get( Model_Security_Tweaks::class );
106 $this->register_routes();
107
108 // Init all the tweaks, should happen one time.
109 $this->component_instances = $this->init_tweaks();
110
111 $this->security_key = $this->component_instances['security-key'];
112 $this->prevent_enum_users = $this->component_instances['prevent-enum-users'];
113
114 // Now shield up.
115 $this->boot();
116 // Add addition hooks.
117 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
118 add_action( 'wp_loaded', array( $this, 'should_output_error' ) );
119 }
120
121 /**
122 * Dummy function for testing a check.
123 */
124 public function should_output_error() {
125 $test_err_reporting = defender_get_data_from_request( 'defender_test_error_reporting', 'g' );
126 if ( ! is_string( $test_err_reporting ) || '' === trim( $test_err_reporting ) ) {
127 return;
128 }
129 // It should be only trigger by admin.
130 if ( ! $this->check_permission() ) {
131 return;
132 }
133
134 $var = '$' . uniqid( '', true );
135 // This should output a warning. Ignored phpcs as it's a test.
136 echo ${$var}; // phpcs:ignore
137 exit();
138 }
139
140 /**
141 * Process.
142 *
143 * @param Request $request Request object.
144 *
145 * @return Response
146 * @defender_route
147 */
148 public function process( Request $request ) {
149 $data = $request->get_data(
150 array(
151 'slug' => array(
152 'type' => 'string',
153 'sanitize' => 'sanitize_text_field',
154 ),
155 'current_server' => array(
156 'type' => 'string',
157 'sanitize' => 'sanitize_text_field',
158 ),
159 'username' => array(
160 'type' => 'string',
161 'sanitize' => 'sanitize_user',
162 ),
163 )
164 );
165
166 $slug = $data['slug'] ?? false;
167 $tweak = $this->get_tweak( $slug );
168
169 if ( ! is_object( $tweak ) ) {
170 return new Response(
171 false,
172 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
173 );
174 }
175
176 if ( 'prevent-enum-users' === $slug ) {
177 $pe_data = $request->get_data();
178 $enabled_enums = $pe_data['enabled_user_enums'] ?? null;
179 $all_enums = array( 'enum-rest', 'enum-oembed', 'enum-sitemap' );
180 $this->prevent_enum_users->set_enabled_user_enums(
181 is_array( $enabled_enums ) ? $enabled_enums : $all_enums
182 );
183 }
184
185 if ( 'security-key' === $slug ) {
186 $sk_data = $request->get_data();
187 $remind_date = sanitize_text_field( $sk_data['remind_date'] ?? '' );
188 if ( $remind_date ) {
189 $is_autogen = filter_var( $sk_data['is_autogenerate_keys'] ?? false, FILTER_VALIDATE_BOOLEAN );
190 $values = get_site_option( 'defender_security_tweaks_' . $this->security_key->slug, array() );
191 $values['reminder_duration'] = $remind_date;
192 $values['reminder_date'] = strtotime( '+' . $remind_date, time() );
193 $values['is_autogenerate_keys'] = $is_autogen;
194 update_site_option( 'defender_security_tweaks_' . $this->security_key->slug, $values );
195 if ( true === $is_autogen ) {
196 $this->security_key->cron_schedule();
197 } else {
198 $this->security_key->cron_unschedule();
199 }
200 }
201 }
202
203 if ( 'replace-admin-username' === $slug && ! empty( $data['username'] ) ) {
204 $_POST['username'] = $data['username'];
205 }
206
207 if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) {
208 $current_server = $data['current_server'] ?? false;
209 if ( ! $current_server ) {
210 return new Response(
211 false,
212 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
213 );
214 }
215
216 $ret = $tweak->process( $current_server );
217 } else {
218 $ret = $tweak->process();
219 }
220
221 if ( true === $ret ) {
222 Config_Hub_Helper::set_clear_active_flag();
223 $this->model->mark( self::STATUS_RESOLVE, $slug );
224 // Track.
225 $this->track_tweak( $tweak->get_label(), 'Actioned' );
226 // Response.
227 $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ) );
228 }
229 if ( $ret instanceof Response && true === $ret->status ) {
230 Config_Hub_Helper::set_clear_active_flag();
231 $this->model->mark( self::STATUS_RESOLVE, $slug );
232 $this->track_tweak( $tweak->get_label(), 'Actioned' );
233 return $ret;
234 }
235 if ( is_wp_error( $ret ) ) {
236 $this->ajax_response( $ret->get_error_message(), false );
237 }
238
239 return new Response(
240 false,
241 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
242 );
243 }
244
245 /**
246 * Revert.
247 *
248 * @param Request $request Request object.
249 *
250 * @return Response
251 * @defender_route
252 */
253 public function revert( Request $request ) {
254 $data = $request->get_data(
255 array(
256 'slug' => array(
257 'type' => 'string',
258 'sanitize' => 'sanitize_text_field',
259 ),
260 'current_server' => array(
261 'type' => 'string',
262 'sanitize' => 'sanitize_text_field',
263 ),
264 )
265 );
266 $slug = $data['slug'] ?? false;
267 $tweak = $this->get_tweak( $slug );
268 $invalid = array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) );
269 if ( ! is_object( $tweak ) ) {
270 return new Response(
271 false,
272 $invalid
273 );
274 }
275 if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) {
276 $current_server = $data['current_server'] ?? false;
277 if ( ! $current_server ) {
278 return new Response(
279 false,
280 $invalid
281 );
282 }
283 $ret = $tweak->revert( $current_server );
284 } else {
285 $ret = $tweak->revert();
286 }
287
288 if ( is_wp_error( $ret ) ) {
289 $this->ajax_response( $ret->get_error_message(), false );
290 }
291 if ( true === $ret ) {
292 Config_Hub_Helper::set_clear_active_flag();
293 $this->model->mark( self::STATUS_ISSUES, $slug );
294 // Track.
295 $this->track_tweak( $tweak->get_label(), 'Reverted' );
296 // Response.
297 $this->ajax_response( esc_html__( 'Security recommendation successfully reverted.', 'defender-security' ) );
298 }
299
300 return new Response(
301 false,
302 $invalid
303 );
304 }
305
306 /**
307 * Ignore.
308 *
309 * @param Request $request Request object.
310 *
311 * @return Response|void
312 * @defender_route
313 */
314 public function ignore( Request $request ) {
315 $data = $request->get_data(
316 array(
317 'slug' => array(
318 'type' => 'string',
319 'sanitize' => 'sanitize_text_field',
320 ),
321 )
322 );
323 $slug = $data['slug'] ?? false;
324 $tweak = $this->get_tweak( $slug );
325 if ( ! is_object( $tweak ) ) {
326 return new Response(
327 false,
328 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
329 );
330 }
331 $this->model->mark( self::STATUS_IGNORE, $slug );
332 // Track.
333 $this->track_tweak( $tweak->get_label(), 'Ignored' );
334 if ( 'security-key' === $slug ) {
335 $this->security_key->cron_unschedule();
336 }
337
338 $this->ajax_response( esc_html__( 'Security recommendation successfully ignored.', 'defender-security' ) );
339 }
340
341 /**
342 * Restore.
343 *
344 * @param Request $request Request object.
345 *
346 * @return Response|void
347 * @defender_route
348 */
349 public function restore( Request $request ) {
350 $data = $request->get_data(
351 array(
352 'slug' => array(
353 'type' => 'string',
354 'sanitize' => 'sanitize_text_field',
355 ),
356 )
357 );
358 $slug = $data['slug'] ?? false;
359 $tweak = $this->get_tweak( $slug );
360 if ( ! is_object( $tweak ) ) {
361 return new Response(
362 false,
363 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
364 );
365 }
366 $this->model->mark( self::STATUS_RESTORE, $slug );
367 // Track.
368 $this->track_tweak( $tweak->get_label(), 'Restored' );
369
370 if ( $this->security_key->get_is_autogenerate_keys() ) {
371 $this->security_key->cron_schedule();
372 }
373
374 $this->ajax_response( esc_html__( 'Security recommendation successfully restored.', 'defender-security' ) );
375 }
376
377 /**
378 * Recheck.
379 *
380 * @param Request $request Request object.
381 *
382 * @return Response
383 * @defender_route
384 */
385 public function recheck( Request $request ): Response {
386 $data = $request->get_data(
387 array(
388 'slug' => array(
389 'type' => 'string',
390 'sanitize' => 'sanitize_text_field',
391 ),
392 )
393 );
394 $slug = $data['slug'] ?? false;
395 $tweak = $this->get_tweak( $slug );
396
397 if ( ! is_object( $tweak ) ) {
398 return new Response(
399 false,
400 array(
401 'message' => esc_html__(
402 'The status cannot be verified as the request contains an invalid slug.',
403 'defender-security'
404 ),
405 )
406 );
407 }
408
409 $ret = $tweak->check();
410
411 if ( true === $ret ) {
412 $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ), true, 1 );
413 }
414
415 if ( is_wp_error( $ret ) ) {
416 return new Response(
417 false,
418 array( 'message' => $ret->get_error_message() )
419 );
420 }
421
422 $msg = sprintf(
423 /* translators: %s: Tweak title. */
424 esc_html__( '%s is not actioned. Please ensure that all the instructions are followed.', 'defender-security' ),
425 $tweak->get_label() ?? ''
426 );
427
428 return new Response(
429 false,
430 array( 'message' => $msg )
431 );
432 }
433
434 /**
435 * Update security reminder.
436 *
437 * @param Request $request Request object.
438 *
439 * @return Response
440 * @defender_route
441 */
442 public function update_security_reminder( Request $request ): Response {
443 $data = $request->get_data();
444 $remind_date = $data['remind_date'] ?? false;
445
446 $is_autogen_flag = isset( $data['is_autogenerate_keys'] ) ?
447 filter_var( $data['is_autogenerate_keys'], FILTER_VALIDATE_BOOLEAN ) :
448 false;
449
450 if ( ! $remind_date ) {
451 return new Response(
452 false,
453 array( 'message' => esc_html__( 'Invalid Reminder frequency', 'defender-security' ) )
454 );
455 }
456
457 $values = array(
458 'reminder_duration' => $remind_date,
459 'reminder_date' => strtotime( '+' . $remind_date, time() ),
460 'is_autogenerate_keys' => $is_autogen_flag,
461 );
462
463 if ( update_site_option( 'defender_security_tweaks_' . $this->security_key->slug, $values ) ) {
464
465 if ( true === $is_autogen_flag ) {
466 $this->security_key->cron_schedule();
467 } else {
468 $this->security_key->cron_unschedule();
469 }
470
471 return new Response(
472 true,
473 array( 'message' => esc_html__( 'Security recommendation successfully updated.', 'defender-security' ) )
474 );
475 } else {
476 return new Response(
477 false,
478 array( 'message' => esc_html__( 'Error while updating.', 'defender-security' ) )
479 );
480 }
481 }
482
483 /**
484 * Returns tweaks for a given status formatted for the React frontend.
485 *
486 * Adds `category` and remaps the backend status string to the value the
487 * frontend HARDENING_STATUS constants expect ('issue', 'resolved', 'ignored').
488 *
489 * @param string $status One of STATUS_ISSUES, STATUS_RESOLVE, STATUS_IGNORE.
490 *
491 * @return array
492 */
493 private function format_tweaks_for_frontend( string $status ): array {
494 $status_map = array(
495 self::STATUS_ISSUES => 'issue',
496 self::STATUS_RESOLVE => 'resolved',
497 self::STATUS_IGNORE => 'ignored',
498 );
499
500 $tweaks = $this->init_tweaks( $status, 'array' );
501 $frontend_status = $status_map[ $status ] ?? $status;
502
503 foreach ( $tweaks as &$tweak ) {
504 $tweak['status'] = $frontend_status;
505 $tweak['category'] = self::TWEAK_CATEGORIES[ $tweak['slug'] ] ?? 'access';
506 }
507 unset( $tweak );
508
509 return $tweaks;
510 }
511
512 /**
513 * AJAX Response handler.
514 *
515 * @param string $message The message to be displayed in the response.
516 * @param bool $is_success Whether the response should have a success status.
517 * @param bool|int $interval The cron interval for the security key autogeneration. Default is false.
518 *
519 * @return Response The AJAX response.
520 */
521 private function ajax_response( $message, $is_success = true, $interval = false ): Response {
522 global $wp_version;
523
524 // Creating a new Model_Security_Tweaks() here risks reading stale data from the
525 // DB when the WP options cache hasn't flushed the write yet.
526 $data = array(
527 'message' => $message,
528 'summary' => array(
529 'issues_count' => count( $this->model->issues ),
530 'fixed_count' => count( $this->model->fixed ),
531 'ignore_count' => count( $this->model->ignore ),
532 'php_version' => PHP_VERSION,
533 'wp_version' => $wp_version,
534 ),
535 'issues' => $this->build_tweaks_response( $this->model->issues, 'issue' ),
536 'fixed' => $this->build_tweaks_response( $this->model->fixed, 'resolved' ),
537 'ignored' => $this->build_tweaks_response( $this->model->ignore, 'ignored' ),
538 'issues_slugs' => $this->model->issues,
539 );
540 if ( $interval ) {
541 $data['interval'] = $interval;
542 }
543
544 return new Response( $is_success, $data );
545 }
546
547 /**
548 * Builds the frontend-shaped tweak array from a list of slugs.
549 *
550 * Uses the in-memory tweak object cache rather than re-querying the model,
551 * so the result is always consistent with the state after mark() was called.
552 *
553 * @param array $slugs Slugs to include.
554 * @param string $frontend_status Frontend status string ('issue', 'resolved', 'ignored').
555 *
556 * @return array
557 */
558 private function build_tweaks_response( array $slugs, string $frontend_status ): array {
559 $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
560 $result = array();
561
562 foreach ( $slugs as $slug ) {
563 if ( ! isset( $tweaks[ $slug ] ) ) {
564 continue;
565 }
566 $arr = $tweaks[ $slug ]->to_array();
567 $arr['status'] = $frontend_status;
568 $arr['category'] = self::TWEAK_CATEGORIES[ $slug ] ?? 'access';
569 $result[ $slug ] = $arr;
570 }
571
572 return $result;
573 }
574
575 /**
576 * Enqueues scripts and styles for this page.
577 * Only enqueues assets if the page is active.
578 */
579 public function enqueue_assets() {
580 if ( ! $this->is_page_active() ) {
581 return;
582 }
583
584 $handle = 'defender-ui-hardening';
585 wp_enqueue_script(
586 $handle,
587 WP_DEFENDER_BASE_URL . 'assets/js/hardening-ui.js',
588 array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
589 DEFENDER_VERSION,
590 true
591 );
592 wp_set_script_translations( $handle, 'wpdef' );
593
594 wp_localize_script(
595 $handle,
596 'defenderUIData',
597 array_merge(
598 $this->get_shared_data(),
599 $this->data_frontend()
600 )
601 );
602
603 wp_enqueue_style(
604 $handle,
605 WP_DEFENDER_BASE_URL . 'assets/css/showcase.css',
606 array(),
607 DEFENDER_VERSION
608 );
609
610 $this->enqueue_main_assets();
611 }
612
613 /**
614 * Provides data for the dashboard widget.
615 *
616 * @return array
617 */
618 public function dashboard_widget(): array {
619 $tweak_arr = $this->model->get_tweak_types();
620 $data = array();
621 foreach ( $this->init_tweaks( self::STATUS_ISSUES ) as $slug => $tweak ) {
622 $data[] = array(
623 'title' => $tweak->get_label(),
624 'slug' => $slug,
625 );
626 }
627
628 return array(
629 'summary' => array(
630 'fixed_count' => $tweak_arr['count_fixed'],
631 'ignore_count' => $tweak_arr['count_ignored'],
632 'issues_count' => $tweak_arr['count_issues'],
633 ),
634 'issues' => $data,
635 );
636 }
637
638 /**
639 * Provides data for the frontend.
640 *
641 * @return array An array of data for the frontend.
642 */
643 public function data_frontend(): array {
644 $this->refresh_tweaks_status();
645 global $wp_version;
646
647 $not_allowed_bulk = array(
648 'php-version',
649 'replace-admin-username',
650 );
651 if ( 'nginx' === Server::get_current_server() ) {
652 $not_allowed_bulk[] = 'protect-information';
653 $not_allowed_bulk[] = 'prevent-php-executed';
654 }
655
656 $tweak_arr = $this->model->get_tweak_types();
657
658 $hardening_data = array(
659 'summary' => array(
660 'fixed_count' => $tweak_arr['count_fixed'],
661 'ignore_count' => $tweak_arr['count_ignored'],
662 'issues_count' => $tweak_arr['count_issues'],
663 'php_version' => PHP_VERSION,
664 'wp_version' => $wp_version,
665 ),
666 'issues' => $this->format_tweaks_for_frontend( self::STATUS_ISSUES ),
667 'fixed' => $this->format_tweaks_for_frontend( self::STATUS_RESOLVE ),
668 'ignored' => $this->format_tweaks_for_frontend( self::STATUS_IGNORE ),
669 'not_allowed_bulk' => $not_allowed_bulk,
670 'is_autogenerate_keys' => $this->security_key->get_is_autogenerate_keys(),
671 'reminder_frequencies' => $this->security_key->reminder_frequencies(),
672 'enabled_user_enums' => $this->prevent_enum_users->get_enabled_user_enums(),
673 );
674
675 return array(
676 'hardening' => array_merge( $hardening_data, $this->dump_routes_and_nonces() ),
677 );
678 }
679
680 /**
681 * Renders the main view for this page.
682 */
683 public function main_view() {
684 $this->render( 'main' );
685 }
686
687 /**
688 * Bulk action for security tweaks.
689 * Handles bulk resolving, ignoring, or reverting security tweaks.
690 *
691 * @param Request $request The request object.
692 *
693 * @return Response
694 * @defender_route
695 */
696 public function bulk_action( Request $request ) {
697 $data = $request->get_data(
698 array(
699 'slugs' => array(
700 'type' => 'array',
701 'sanitize' => 'sanitize_text_field',
702 ),
703 'intention' => array(
704 'type' => 'string',
705 'sanitize' => 'sanitize_text_field',
706 ),
707 )
708 );
709 $slugs = $data['slugs'] ?? array();
710 $intention = $data['intention'] ?? false;
711 // Get processed and unprocessed tweaks.
712 [ $processed, $unprocessed, $error_message ] = $this->security_tweaks_auto_action( $slugs, $intention );
713
714 if ( null !== $error_message ) {
715 return $this->ajax_response( $error_message, false );
716 }
717
718 $message = sprintf(
719 /* translators: 1: Either ignored or resolved, 2: Count security recommendations */
720 esc_html__( 'You have bulk %1$s %2$s security recommendations.', 'defender-security' ),
721 'ignore' === $intention ? 'ignored' : 'resolved',
722 $processed
723 );
724
725 if ( isset( $unprocessed ) && $unprocessed > 0 ) {
726 // If we have this case this mean the intention is resolved.
727 $message = sprintf(
728 /* translators: %d: Count security tweaks */
729 esc_html__(
730 'You have bulk actioned %d security recommendations. You still have a few unresolved security recommendations, which cannot be bulk actioned automatically, so please address them below.',
731 'defender-security'
732 ),
733 $processed
734 );
735
736 Config_Hub_Helper::set_clear_active_flag();
737 }
738
739 return $this->ajax_response( $message );
740 }
741
742 /**
743 * Mass processing.
744 *
745 * @param array $slugs Tweak slugs.
746 * @param string $intention Status.
747 *
748 * @return array
749 */
750 public function security_tweaks_auto_action( $slugs, $intention ): array {
751 $processed = 0;
752 $unprocessed = 0;
753 $error_message = null;
754
755 foreach ( $slugs as $slug ) {
756 $tweak = $this->get_tweak( $slug );
757 if ( 'ignore' === $intention ) {
758 $this->model->mark( self::STATUS_IGNORE, $slug );
759 // Track.
760 $this->track_tweak( $tweak->get_label(), 'Ignored' );
761 } elseif ( 'resolve' === $intention ) {
762 $wont_do = array(
763 'replace-admin-username',
764 'prevent-php-executed',
765 'wp-version',
766 'php-version',
767 'protect-information',
768 );
769 if ( in_array( $slug, $wont_do, true ) ) {
770 ++$unprocessed;
771 continue;
772 }
773 if ( $tweak->has_method( 'bulk_process' ) ) {
774 $ret = $tweak->bulk_process();
775 } else {
776 $ret = $tweak->process();
777 }
778 if ( is_wp_error( $ret ) ) {
779 $error_message = sprintf(
780 /* translators: 1: Security tweak title, 2: Error message */
781 esc_html__(
782 'There is an error while processing recommendation %1$s, error message: %2$s',
783 'defender-security'
784 ),
785 $tweak->get_label(),
786 $ret->get_error_message()
787 );
788 ++$unprocessed;
789 // Don't mark a failed tweak as resolved; stop on first error (same as original behavior).
790 break;
791 }
792 $this->model->mark( self::STATUS_RESOLVE, $slug );
793 // Track.
794 $this->track_tweak( $tweak->get_label(), 'Actioned' );
795 } elseif ( 'restore' === $intention ) {
796 // New bulk action since V6.
797 $this->model->mark( self::STATUS_ISSUES, $slug );
798 // Track.
799 $this->track_tweak( $tweak->get_label(), 'Restored' );
800 }
801 ++$processed;
802 }
803
804 return array( $processed, $unprocessed, $error_message );
805 }
806
807 /**
808 * Refresh the tweak status and save their state.
809 *
810 * @return void
811 */
812 public function refresh_tweaks_status() {
813 $tweaks = $this->init_tweaks();
814 $settings = new Model_Security_Tweaks();
815 $fixed = array();
816 $issues = array();
817
818 foreach ( $tweaks as $slug => $class ) {
819 if ( $settings->is_tweak_ignore( $slug ) ) {
820 continue;
821 }
822
823 $is_resolved = $class->check();
824
825 if ( $is_resolved ) {
826 $fixed[] = $slug;
827 } else {
828 $issues[] = $slug;
829 }
830 }
831
832 $settings->fixed = $fixed;
833 $settings->issues = $issues;
834 $settings->save();
835 }
836
837 /**
838 * This function for shield every active tweaks up, we will use the cached result.
839 * No check function trigger in this init runtime.
840 */
841 private function boot() {
842 $tweaks = $this->init_tweaks( self::STATUS_RESOLVE );
843 foreach ( $tweaks as $tweak ) {
844 $tweak->shield_up();
845 }
846 }
847
848 /**
849 * Instance all the tweaks, happen one time in init runtime.
850 *
851 * @param string $type Type of tweaks.
852 * @param string $format Object for internal use, array for frontend use.
853 *
854 * @return array
855 */
856 public function init_tweaks( $type = '', $format = 'object' ): array {
857 $classes = array(
858 Disable_XML_RPC::class,
859 WP_Version::class,
860 Hide_Error::class,
861 PHP_Version::class,
862 Change_Admin::class,
863 Security_Key::class,
864 Login_Duration::class,
865 Disable_Trackback::class,
866 Prevent_Enum_Users::class,
867 Disable_File_Editor::class,
868 );
869 if ( ! defender_is_wp_cli() ) {
870 // We don't load this in cli, as clearly no server is running.
871 $classes = array_merge(
872 $classes,
873 array(
874 Protect_Information::class,
875 Prevent_PHP::class,
876 )
877 );
878 }
879
880 $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
881
882 if ( ! is_array( $tweaks ) ) {
883 foreach ( $classes as $class ) {
884 $obj = new $class();
885 $tweaks[ $obj->slug ] = $obj;
886 }
887 Array_Cache::set( 'tweaks', $tweaks, 'tweaks' );
888 }
889 $tmp = array();
890 if ( ! is_string( $type ) || '' === trim( $type ) ) {
891 $tmp = $tweaks;
892 } else {
893 $settings = new Model_Security_Tweaks();
894 $compare = $settings->$type;
895 foreach ( $compare as $slug ) {
896 if ( isset( $tweaks[ $slug ] ) ) {
897 $tmp[ $slug ] = $tweaks[ $slug ];
898 }
899 }
900 }
901
902 if ( 'array' === $format ) {
903 // We need to parse this as array.
904 foreach ( $tmp as $slug => $obj ) {
905 $arr = $obj->to_array();
906 $arr['status'] = $type;
907 $tmp[ $slug ] = $arr;
908 }
909 }
910
911 return $tmp;
912 }
913
914 /**
915 * Get tweak object from cache by slug.
916 *
917 * @param string $slug Tweak slug.
918 *
919 * @return mixed Tweak object if exists, else null.
920 */
921 private function get_tweak( $slug ) {
922 $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
923
924 return $tweaks[ $slug ] ?? null;
925 }
926
927 /**
928 * Converts the current object state to an array.
929 *
930 * @return array The array representation of the object.
931 */
932 public function to_array(): array {
933 $this->refresh_tweaks_status();
934 $settings = new Model_Security_Tweaks();
935
936 return array(
937 'count' => array(
938 'issues' => count( $settings->issues ),
939 'resolved' => count( $settings->fixed ),
940 'total' => count( $this->init_tweaks() ),
941 ),
942 );
943 }
944
945 /**
946 * Removes settings for all submodules.
947 */
948 public function remove_settings() {
949 // Revert it first.
950 $tweaks = $this->init_tweaks( self::STATUS_RESOLVE );
951 // Assign this so internal can use the current server.
952 $_POST['current_server'] = Server::get_current_server();
953 foreach ( $tweaks as $tweak ) {
954 $tweak->revert();
955
956 if ( method_exists( $tweak, 'delete_all_option' ) ) {
957 $tweak->delete_all_option();
958 }
959 }
960
961 ( new Model_Security_Tweaks() )->delete();
962
963 delete_site_transient( Server::CACHE_CURRENT_SERVER );
964 delete_site_transient( \WP_Defender\Component\Security_Tweaks\Servers\Apache::CACHE_APACHE_VERSION );
965 $this->security_key->cron_unschedule();
966 }
967
968 /**
969 * Delete all the data & the cache.
970 */
971 public function remove_data() {
972 // Remove cached data.
973 Array_Cache::remove( 'tweaks', 'tweaks' );
974 delete_site_option( Security_Key::REGENERATE_SALT_NEXT_RUN_OPTION );
975 }
976
977 /**
978 * Automate the process of resolving, reverting, and ignoring security tweaks based on the provided data.
979 *
980 * @param array $data The data containing information about fixed, issues, and ignored security tweaks.
981 * @param string $request_reason The reason for the request, e.g., 'hub'.
982 *
983 * @return mixed Whether a reauthentication is needed after processing the tweaks.
984 * @since 2.8.1 Add $request_reason param. If there's a request from Hub the plugin doesn't send the error message.
985 */
986 public function automate( $data, $request_reason ) {
987 $this->refresh_tweaks_status();
988 $need_reauth = false;
989 // Resolve tweaks.
990 if ( isset( $data['fixed'] ) && is_array( $data['fixed'] ) && array() !== $data['fixed'] ) {
991 // There are some tweak that need manual apply, as files based, or change admin.
992 $manual_done = array(
993 'replace-admin-username',
994 'prevent-php-executed',
995 'wp-version',
996 'php-version',
997 'protect-information',
998 );
999 if ( 'hub' === $request_reason ) {
1000 $manual_done[] = 'security-key';
1001 }
1002
1003 $diff_keys = array_diff( $data['fixed'], $this->model->fixed, $manual_done );
1004 if ( array() !== $diff_keys ) {
1005 foreach ( $diff_keys as $slug ) {
1006 $tweak = $this->get_tweak( $slug );
1007 if ( $tweak->has_method( 'bulk_process' ) ) {
1008 $ret = $tweak->bulk_process();
1009 } else {
1010 $ret = $tweak->process();
1011 }
1012
1013 if ( is_wp_error( $ret ) ) {
1014 if ( 'hub' === $request_reason ) {
1015 continue;
1016 }
1017
1018 return sprintf(
1019 /* translators: 1: Security tweak title, 2: Error message */
1020 esc_html__(
1021 'There is an error while processing recommendation %1$s, error message: %2$s',
1022 'defender-security'
1023 ),
1024 $tweak->get_label(),
1025 $ret->get_error_message()
1026 );
1027 }
1028
1029 $this->model->mark( self::STATUS_RESOLVE, $slug );
1030 }
1031 if ( in_array( 'security-key', $diff_keys, true ) ) {
1032 $need_reauth = true;
1033 }
1034 }
1035 }
1036 // Revert tweaks.
1037 if ( isset( $data['issues'] ) && is_array( $data['issues'] ) && array() !== $data['issues'] ) {
1038 $diff_keys = array_diff( $data['issues'], $this->model->issues );
1039
1040 if ( array() !== $diff_keys ) {
1041 // Issues.
1042 foreach ( $diff_keys as $slug ) {
1043 $tweak = $this->get_tweak( $slug );
1044 $ret = $tweak->revert();
1045 if ( is_wp_error( $ret ) ) {
1046 if ( 'hub' === $request_reason ) {
1047 continue;
1048 }
1049
1050 return sprintf(
1051 /* translators: 1: Security tweak title, 2: Error message */
1052 esc_html__(
1053 'There is an error while processing recommendation %1$s, error message: %2$s',
1054 'defender-security'
1055 ),
1056 $tweak->get_label(),
1057 $ret->get_error_message()
1058 );
1059 }
1060 $this->model->mark( self::STATUS_ISSUES, $slug );
1061 }
1062 }
1063 }
1064 // Ignore tweaks.
1065 if ( isset( $data['ignore'] ) && is_array( $data['ignore'] ) && array() !== $data['ignore'] ) {
1066 $diff_keys = array_diff( $data['ignore'], $this->model->ignore );
1067 if ( array() !== $diff_keys ) {
1068 foreach ( $diff_keys as $slug ) {
1069 $this->model->mark( self::STATUS_IGNORE, $slug );
1070 }
1071 }
1072 }
1073
1074 return $need_reauth;
1075 }
1076
1077 /**
1078 * Imports data into the model.
1079 *
1080 * @param array $data Data to be imported into the model.
1081 */
1082 public function import_data( array $data ) {
1083 $enabled_user_enums = array();
1084
1085 if ( isset( $data['enabled_user_enums'] ) ) {
1086 $enabled_user_enums = (array) $data['enabled_user_enums'];
1087
1088 unset( $data['enabled_user_enums'] );
1089 }
1090
1091 $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums );
1092
1093 if ( isset( $data['security_key'] ) && is_array( $data['security_key'] ) && array() !== $data['security_key'] ) {
1094 $this->security_key->update_all_option( $data['security_key'] );
1095 }
1096
1097 $model = new Model_Security_Tweaks();
1098
1099 $model->import( $data );
1100 if ( $model->validate() ) {
1101 $model->save();
1102 }
1103 }
1104
1105 /**
1106 * Exports strings.
1107 *
1108 * @return array An array of strings.
1109 */
1110 public function export_strings(): array {
1111 $this->refresh_tweaks_status();
1112 $settings = new Model_Security_Tweaks();
1113 $strings = array();
1114 $count_all = count( $settings->fixed ) + count( $settings->issues ) + count( $settings->ignore );
1115
1116 if ( ! is_array( $settings->issues ) || array() === $settings->issues ) {
1117 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
1118 } else {
1119 $strings[] = sprintf(
1120 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
1121 esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ),
1122 count( $settings->fixed ),
1123 $count_all
1124 );
1125 }
1126
1127 return $strings;
1128 }
1129
1130 /**
1131 * Config strings.
1132 *
1133 * @param array $config Settings.
1134 *
1135 * @return array Strings to be displayed in the settings page.
1136 */
1137 public function config_strings( $config ): array {
1138 $strings = array();
1139 if (
1140 ! isset( $config['issues'] )
1141 || ! ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable )
1142 || 0 === count( $config['issues'] )
1143 ) {
1144 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
1145 } else {
1146 $strings[] = sprintf(
1147 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
1148 esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ),
1149 is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0,
1150 ( is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0 )
1151 + ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable ? count( $config['issues'] ) : 0 )
1152 + ( is_array( $config['ignore'] ) || $config['ignore'] instanceof Countable ? count( $config['ignore'] ) : 0 )
1153 );
1154 }
1155 if ( 'enabled' === $config['notification'] ) {
1156 $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
1157 }
1158
1159 return $strings;
1160 }
1161
1162 /**
1163 * Update auto generate flag.
1164 *
1165 * @param Request $request Request object.
1166 *
1167 * @defender_route
1168 */
1169 public function update_autogenerate_flag( Request $request ): Response {
1170 $data = $request->get_data();
1171
1172 $is_autogen_flag = isset( $data['is_autogenerate_keys'] ) ?
1173 filter_var( $data['is_autogenerate_keys'], FILTER_VALIDATE_BOOLEAN ) :
1174 false;
1175
1176 $is_success = false;
1177 $message = esc_html__( 'An error occurred, try again.', 'defender-security' );
1178
1179 if ( $this->security_key->set_is_autogenrate_keys( $is_autogen_flag ) ) {
1180 $is_success = true;
1181
1182 if ( $is_autogen_flag ) {
1183 $this->security_key->cron_schedule();
1184 $message = esc_html__( 'Security key/salt autogenerate enabled.', 'defender-security' );
1185 } else {
1186 $this->security_key->cron_unschedule();
1187 $message = esc_html__( 'Security key/salt autogenerate disabled.', 'defender-security' );
1188 }
1189 }
1190
1191 return new Response(
1192 $is_success,
1193 array( 'message' => $message )
1194 );
1195 }
1196
1197 /**
1198 * Get component security key instance.
1199 *
1200 * @return Security_Key
1201 */
1202 public function get_security_key() {
1203 return $this->security_key;
1204 }
1205
1206 /**
1207 * Update enabled user enums list.
1208 *
1209 * @param Request $request Request object.
1210 *
1211 * @return Response
1212 * @defender_route
1213 */
1214 public function update_enabled_user_enums( Request $request ): Response {
1215 $data = $request->get_data();
1216 $data = ! is_array( $data ) ? (array) $data : $data;
1217 $enabled_user_enums = $data['enabled_user_enums'];
1218 $is_success = false;
1219 $message = esc_html__( 'An error occurred, try again.', 'defender-security' );
1220
1221 if ( $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums ) ) {
1222 $is_success = true;
1223 $message = esc_html__( 'User enumeration option(s) updated successfully.', 'defender-security' );
1224 }
1225
1226 return new Response(
1227 $is_success,
1228 array( 'message' => $message )
1229 );
1230 }
1231
1232 /**
1233 * Check XML-RPC status.
1234 *
1235 * @return Response
1236 * @defender_route
1237 */
1238 public function check_xml_rpc() {
1239 $tweak_xml_rpc = wd_di()->get( Disable_XML_RPC::class );
1240 // If this tweak is ON, block XML-RPC.
1241 if ( $tweak_xml_rpc->recheck() ) {
1242 $tweak_xml_rpc->add_hooks();
1243 }
1244
1245 return new Response( true, array() );
1246 }
1247
1248 /**
1249 * Tracks a tweak recommendation.
1250 *
1251 * @param string $title The title of the tweak recommendation.
1252 * @param string $status The status of the tweak recommendation.
1253 *
1254 * @return void
1255 */
1256 private function track_tweak( string $title, string $status ) {
1257 if ( ! defender_is_wp_cli() ) {
1258 $this->track_feature(
1259 'def_recommendation_applied',
1260 array(
1261 'Recommendation Name' => $title,
1262 'Status' => $status,
1263 )
1264 );
1265 }
1266 }
1267 }
1268