PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.4.0
Defender Security – Malware Scanner, Login Security & Firewall v5.4.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-security-tweaks.php

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

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