PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.1.0
Defender Security – Malware Scanner, Login Security & Firewall v3.1.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 / scan.php

scan.php in Defender Security – Malware Scanner, Login Security & Firewall 3.1.0, at src/controller/scan.php

761 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WP_Defender\Controller;
4
5 use Calotes\Component\Request;
6 use Calotes\Component\Response;
7 use WP_Defender\Component\Config\Config_Hub_Helper;
8 use WP_Defender\Controller;
9 use WP_Defender\Model\Notification\Malware_Report;
10 use Valitron\Validator;
11 use WP_Defender\Model\Scan as Model_Scan;
12 use WP_Defender\Traits\Formats;
13
14 class Scan extends Controller {
15 use Formats;
16
17 protected $slug = 'wdf-scan';
18
19 /**
20 * @var \WP_Defender\Model\Setting\Scan;
21 */
22 protected $model;
23
24 /**
25 * @var \WP_Defender\Component\Scan
26 */
27 protected $service;
28
29 /**
30 * Scan constructor.
31 */
32 public function __construct() {
33 $this->register_page(
34 esc_html__( 'Malware Scanning', 'wpdef' ),
35 $this->slug,
36 [
37 &$this,
38 'main_view',
39 ],
40 $this->parent_slug
41 );
42 $this->model = new \WP_Defender\Model\Setting\Scan();
43 $this->service = new \WP_Defender\Component\Scan();
44 $this->register_routes();
45 add_action( 'defender_enqueue_assets', [ &$this, 'enqueue_assets' ] );
46 add_action( 'wp_ajax_defender_process_scan', [ &$this, 'process' ] );
47 add_action( 'wp_ajax_nopriv_defender_process_scan', [ &$this, 'process' ] );
48 add_action( 'defender/async_scan', [ &$this, 'process' ] );
49 // Clean up data after successful core update.
50 add_action( '_core_updated_successfully', [ &$this, 'clean_up_data' ] );
51
52 global $pagenow;
53 // @since 2.6.2
54 if (
55 is_admin() &&
56 'plugins.php' === $pagenow &&
57 apply_filters( 'wd_display_vulnerability_warnings', true )
58 ) {
59 $this->service->display_vulnerability_warnings();
60 }
61
62 // Schedule a time to clear completed action scheduler logs.
63 if ( ! wp_next_scheduled( 'wpdef_clear_scan_logs' ) ) {
64 wp_schedule_event( time(), 'weekly', 'wpdef_clear_scan_logs' );
65 }
66 add_action( 'wpdef_clear_scan_logs', [ $this, 'clear_scan_logs' ] );
67 }
68
69 /**
70 * Clean up data after core updating.
71 */
72 public function clean_up_data() {
73 $this->service->clean_up();
74 }
75
76 /**
77 * Start a scan.
78 *
79 * @param Request $request
80 *
81 * @return Response
82 * @throws \Exception
83 * @defender_route
84 */
85 public function start( Request $request ): Response {
86 $model = Model_Scan::create();
87 if ( is_object( $model ) && ! is_wp_error( $model ) ) {
88 $this->log( 'Initial ping self', 'scan.log' );
89 $this->do_async_scan( 'scan' );
90
91 return new Response(
92 true,
93 [
94 'status' => $model->status,
95 'status_text' => $model->get_status_text(),
96 'percent' => 0,
97 ]
98 );
99 }
100
101 return new Response(
102 false,
103 [
104 'message' => __( 'A scan is already in progress', 'wpdef' ),
105 ]
106 );
107 }
108
109 /**
110 * Use this for self ping, so it can both run in background and active mode with good performance.
111 *
112 * @return mixed
113 * @throws \ReflectionException
114 * @defender_route
115 * @is_public
116 */
117 public function process() {
118 if ( $this->service->has_lock() ) {
119 $this->log( 'Fallback as already a process is running', 'scan.log' );
120
121 return;
122 }
123
124 // This creates file lock, for make sure only 1 process run as a time.
125 $this->service->create_lock();
126 // Check if the ping is from self or not.
127 $ret = $this->service->process();
128 $this->log( 'process done, queue for next', 'scan.log' );
129 if ( false === $ret ) {
130 // Ping self.
131 $this->log( 'Scan not done, pinging', 'scan.log' );
132 $this->service->remove_lock();
133 $this->process();
134 } else {
135 if ( ! wp_next_scheduled( 'defender_hub_sync' ) ) {
136 wp_schedule_single_event( time(), 'defender_hub_sync' );
137 }
138
139 $this->service->remove_lock();
140 }
141 }
142
143 /**
144 * Query status.
145 *
146 * @return Response
147 * @defender_route
148 */
149 public function status(): Response {
150 $idle_scan = wd_di()->get( Model_Scan::class )->get_idle();
151
152 if ( is_object( $idle_scan ) ) {
153 $this->service->update_idle_scan_status();
154
155 return new Response( false, $idle_scan->to_array() );
156 }
157
158 $scan = Model_Scan::get_active();
159 if ( is_object( $scan ) ) {
160
161 return new Response( false, $scan->to_array() );
162 }
163 $scan = Model_Scan::get_last();
164 if ( is_object( $scan ) && ! is_wp_error( $scan ) ) {
165
166 return new Response( true, $scan->to_array() );
167 }
168
169 return new Response(
170 false,
171 [
172 'message' => __( 'Error during scanning', 'wpdef' ),
173 ]
174 );
175 }
176
177 /**
178 * Cancel current scan.
179 *
180 * @return Response
181 * @defender_route
182 */
183 public function cancel(): Response {
184 $component = new \WP_Defender\Component\Scan();
185 $component->cancel_a_scan();
186 $last = Model_Scan::get_last();
187 if ( is_object( $last ) && ! is_wp_error( $last ) ) {
188 $last = $last->to_array();
189 }
190
191 return new Response(
192 true,
193 [
194 'scan' => $last,
195 ]
196 );
197 }
198
199 /**
200 * A central controller to pass any request from frontend to scan item.
201 *
202 * @param Request $request
203 *
204 * @return Response
205 * @defender_route
206 */
207 public function item_hub( Request $request ): Response {
208 $data = $request->get_data(
209 [
210 'id' => [
211 'type' => 'int',
212 'sanitize' => 'sanitize_text_field',
213 ],
214 'intention' => [
215 'type' => 'string',
216 'sanitize' => 'sanitize_text_field',
217 ],
218 ]
219 );
220 $id = $data['id'] ?? false;
221 $intention = $data['intention'] ?? false;
222 if ( false === $id || false === $intention || ! in_array(
223 $intention,
224 [
225 'pull_src',
226 'resolve',
227 'ignore',
228 'delete',
229 'unignore',
230 ]
231 ) ) {
232 wp_die();
233 }
234
235 $scan = Model_Scan::get_last();
236 $item = $scan->get_issue( $id );
237 if ( is_object( $item ) && $item->has_method( $intention ) ) {
238 $result = $item->$intention();
239
240 if ( is_wp_error( $result ) ) {
241 return new Response(
242 false,
243 [
244 'message' => $result->get_error_message(),
245 ]
246 );
247 } elseif ( isset( $result['type_notice'] ) ) {
248 return new Response(
249 true,
250 $result
251 );
252 }
253
254 if ( ! wp_next_scheduled( 'defender_hub_sync' ) ) {
255 wp_schedule_single_event( time(), 'defender_hub_sync' );
256 }
257
258 // Refresh scan instance.
259 $scan = Model_Scan::get_last();
260 $result['scan'] = $scan->to_array();
261
262 return new Response( true, $result );
263 }
264
265 return new Response( false, [] );
266 }
267
268 /**
269 * Process for bulk action.
270 * There is no Update-intention because it is a lengthy process. There may not be enough execution time.
271 *
272 * @param Request $request
273 *
274 * @defender_route
275 * @return Response
276 */
277 public function bulk_hub( Request $request ): Response {
278 $data = $request->get_data(
279 [
280 'items' => [
281 'type' => 'array',
282 'sanitize' => 'sanitize_text_field',
283 ],
284 'bulk' => [
285 'type' => 'string',
286 'sanitize' => 'sanitize_text_field',
287 ],
288 ]
289 );
290 $items = $data['items'] ?? [];
291 $intention = $data['bulk'] ?? false;
292
293 if (
294 empty( $items )
295 || ! is_array( $items )
296 || false === $intention
297 || ! in_array( $intention, [ 'ignore', 'unignore', 'delete' ], true )
298 ) {
299
300 return new Response( false, [] );
301 }
302 $scan = Model_Scan::get_last();
303 if ( ! is_object( $scan ) ) {
304 return new Response( false, [] );
305 }
306
307 foreach ( $items as $id ) {
308 if ( 'ignore' === $intention ) {
309 $scan->ignore_issue( (int) $id );
310 } elseif ( 'unignore' === $intention ) {
311 $scan->unignore_issue( (int) $id );
312 } elseif ( 'delete' === $intention ) {
313 $item = $scan->get_issue( (int) $id );
314 if ( is_object( $item ) ) {
315 $item->delete();
316 }
317 }
318 }
319
320 return new Response(
321 true,
322 [
323 'scan' => $scan->to_array(),
324 ]
325 );
326 }
327
328 /**
329 * Endpoint for saving data.
330 * @since 2.7.0 Add Scheduled Scanning to Malware settings and hide it on Malware Scanning - Reporting.
331 * Also, the backward compatibility of settings for Scan and Malware_Report models.
332 *
333 * @param Request $request
334 *
335 * @defender_route
336 * @return Response
337 */
338 public function save_settings( Request $request ): Response {
339 $data = $request->get_data_by_model( $this->model );
340 // Case#1: enable all child options, if parent and all child options are disabled, so that there is no notice when saving.
341 if (
342 ! $data['integrity_check']
343 && ! $data['check_core']
344 && ! $data['check_plugins']
345 ) {
346 $data['check_core'] = true;
347 $data['check_plugins'] = true;
348 }
349
350 // Case#2: Suspicious code is activated BUT File change detection is deactivated then show the notice.
351 if ( $data['scan_malware'] && ! $data['integrity_check'] ) {
352 $response = [
353 'type_notice' => 'info',
354 'message' => __( 'To reduce false-positive results, we recommend enabling' .
355 ' <strong>File change detection</strong> options for all scan types while the' .
356 ' <strong>Suspicious code</strong> option is enabled.', 'wpdef' ),
357 ];
358 } else {
359 // Prepare response message for usual successful case.
360 $response = [
361 'message' => __( 'Your settings have been updated.', 'wpdef' ),
362 ];
363 }
364 // Additional cases are in the Scan model.
365 $report_change = false;
366 // If 'Scheduled Scanning' is checked then need to change Malware_Report.
367 if ( true === $data['scheduled_scanning'] ) {
368 $report = new Malware_Report();
369 $report_change = true;
370 $report->frequency = $data['frequency'];
371 $report->day = $data['day'];
372 $report->day_n = $data['day_n'];
373 $report->time = $data['time'];
374 // Disable 'Scheduled Scanning'.
375 } elseif ( true === $this->model->scheduled_scanning && false === $data['scheduled_scanning'] ) {
376 $report = new Malware_Report();
377 $report_change = true;
378 $report->status = \WP_Defender\Model\Notification::STATUS_DISABLED;
379 }
380
381 $this->model->import( $data );
382 if ( $this->model->validate() ) {
383 // Todo: need to disable Malware_Notification & Malware_Report if all scan settings are deactivated?
384 $this->model->save();
385 // Save Report's changes.
386 if ( $report_change ) {
387 $report->save();
388 }
389 Config_Hub_Helper::set_clear_active_flag();
390
391 return new Response(
392 true,
393 array_merge( $response, $this->data_frontend() )
394 );
395 } else {
396 return new Response(
397 false,
398 array_merge(
399 [
400 'message' => $this->model->get_formatted_errors(),
401 ],
402 $this->data_frontend()
403 )
404 );
405 }
406 }
407
408 /**
409 * Get the issues mainly for pagination request.
410 *
411 * @param Request $request
412 *
413 * @return Response
414 * @defender_route
415 */
416 public function get_issues( Request $request ): Response {
417 $data = $request->get_data(
418 [
419 'scenario' => [
420 'type' => 'string',
421 'sanitize' => 'sanitize_text_field',
422 ],
423 'type' => [
424 'type' => 'string',
425 'sanitize' => 'sanitize_text_field',
426 ],
427 'per_page' => [
428 'type' => 'string',
429 'sanitize' => 'sanitize_text_field',
430 ],
431 'paged' => [
432 'type' => 'int',
433 'sanitize' => 'sanitize_text_field',
434 ],
435 ]
436 );
437
438 // Validate the request.
439 $v = new Validator( $data, [] );
440 $v->rule( 'required', [ 'scenario', 'type', 'per_page', 'paged' ] );
441 if ( ! $v->validate() ) {
442 return new Response(
443 false,
444 [
445 'message' => '',
446 ]
447 );
448 }
449
450 $scan = Model_Scan::get_last();
451 $issues = $scan->to_array( $data['per_page'], $data['paged'], $data['type'] );
452
453 return new Response(
454 true,
455 [
456 'issue' => $issues['issues_items'],
457 'ignored' => $issues['ignored_items'],
458 'paging' => $issues['paging'],
459 'count' => $issues['count'],
460 ]
461 );
462 }
463
464 /**
465 * Render main page.
466 */
467 public function main_view() {
468 $this->render( 'main' );
469 }
470
471 /**
472 * Enqueue assets.
473 */
474 public function enqueue_assets() {
475 if ( ! $this->is_page_active() ) {
476 return;
477 }
478 wp_localize_script( 'def-scan', 'scan', $this->data_frontend() );
479 wp_enqueue_script( 'def-scan' );
480 wp_enqueue_script( 'clipboard' );
481 $this->enqueue_main_assets();
482 wp_enqueue_script( 'def-codemirror', defender_asset_url( '/assets/js/vendor/codemirror/codemirror.js' ) );
483 wp_enqueue_script( 'def-codemirror-xml', defender_asset_url( '/assets/js/vendor/codemirror/xml/xml.js' ) );
484 wp_enqueue_script(
485 'def-codemirror-clike',
486 defender_asset_url( '/assets/js/vendor/codemirror/clike/clike.js' )
487 );
488 wp_enqueue_script( 'def-codemirror-css', defender_asset_url( '/assets/js/vendor/codemirror/css/css.js' ) );
489 wp_enqueue_script(
490 'def-codemirror-javascript',
491 defender_asset_url( '/assets/js/vendor/codemirror/javascript/javascript.js' )
492 );
493 wp_enqueue_script(
494 'def-codemirror-htmlmixed',
495 defender_asset_url( '/assets/js/vendor/codemirror/htmlmixed/htmlmixed.js' )
496 );
497 wp_enqueue_script( 'def-codemirror-php', defender_asset_url( '/assets/js/vendor/codemirror/php/php.js' ) );
498 wp_enqueue_script(
499 'def-codemirror-merge',
500 defender_asset_url( '/assets/js/vendor/codemirror/merge/merge.js' )
501 );
502 wp_enqueue_script( 'def-diff-match-patch', defender_asset_url( '/assets/js/vendor/diff-match-patch.js' ) );
503 wp_enqueue_script(
504 'def-codemirror-annotatescrollbar',
505 defender_asset_url( '/assets/js/vendor/codemirror/scroll/annotatescrollbar.js' )
506 );
507 wp_enqueue_script(
508 'def-codemirror-simplescrollbars',
509 defender_asset_url( '/assets/js/vendor/codemirror/scroll/simplescrollbars.js' )
510 );
511 wp_enqueue_script(
512 'def-codemirror-searchcursor',
513 defender_asset_url( '/assets/js/vendor/codemirror/search/searchcursor.js' )
514 );
515 wp_enqueue_script(
516 'def-codemirror-matchonscrollbars',
517 defender_asset_url( '/assets/js/vendor/codemirror/search/matchesonscrollbar.js' )
518 );
519
520 wp_enqueue_style( 'def-codemirror', defender_asset_url( '/assets/js/vendor/codemirror/codemirror.css' ) );
521 wp_enqueue_style( 'def-codemirror-dracula', defender_asset_url( '/assets/js/vendor/codemirror/dracula.css' ) );
522 wp_enqueue_style(
523 'def-codemirror-merge',
524 defender_asset_url( '/assets/js/vendor/codemirror/merge/merge.css' )
525 );
526 wp_enqueue_style(
527 'def-codemirror-matchonscrollbars',
528 defender_asset_url( '/assets/js/vendor/codemirror/search/matchesonscrollbar.css' )
529 );
530 wp_enqueue_style(
531 'def-codemirror-simplescrollbars',
532 defender_asset_url( '/assets/js/vendor/codemirror/scroll/simplescrollbars.css' )
533 );
534 }
535
536 /**
537 * @return array[]
538 */
539 public function to_array() {
540 $scan = Model_Scan::get_active();
541 $last = Model_Scan::get_last();
542 if ( ! is_object( $scan ) && ! is_object( $last ) ) {
543 $scan = null;
544 } else {
545 $scan = is_object( $scan ) ? $scan->to_array() : $last->to_array();
546 }
547
548 return array_merge(
549 [
550 'scan' => $scan,
551 'report' => [
552 'enabled' => true,
553 'frequency' => 'weekly',
554 ],
555 ],
556 $this->dump_routes_and_nonces()
557 );
558 }
559
560 public function remove_settings() {
561 ( new \WP_Defender\Model\Setting\Scan() )->delete();
562 }
563
564 public function remove_data() {
565 delete_site_option( Model_Scan::IGNORE_INDEXER );
566 }
567
568 /**
569 * This should set up optimizing configurations for this module.
570 * Todo: need?
571 */
572 public function optimize_configs() {
573 $settings = new \WP_Defender\Model\Setting\Scan();
574 $settings->save();
575 // Schedule it.
576 $report = new Malware_Report();
577 $report->save();
578 }
579
580 /**
581 * @return array
582 */
583 public function data_frontend(): array {
584 $scan = Model_Scan::get_active();
585 $last = Model_Scan::get_last();
586 $per_page = 10;
587 $paged = 1;
588 if ( ! is_object( $scan ) && ! is_object( $last ) ) {
589 $scan = null;
590 } else {
591 $scan = is_object( $scan ) ? $scan->to_array( $per_page, $paged ) : $last->to_array( $per_page, $paged );
592 }
593 $settings = new \WP_Defender\Model\Setting\Scan();
594 $report = wd_di()->get( Malware_Report::class );
595 $report_text = __( 'Automatic scans are disabled', 'wpdef' );
596 if ( $settings->scheduled_scanning && isset( $settings->frequency ) ) {
597 $report_text = sprintf( __( 'Automatic scans are <br/>running %s', 'wpdef' ), $settings->frequency );
598 }
599 $misc = ( new \WP_Defender\Behavior\WPMUDEV() )->is_pro()
600 ? [
601 'days_of_week' => $this->get_days_of_week(),
602 'times_of_day' => $this->get_times(),
603 'timezone_text' => sprintf(
604 /* translators: %s - timezone, %s - time */
605 __(
606 'Your timezone is set to <strong>%1$s</strong>, so your current time is <strong>%2$s</strong>.',
607 'wpdef'
608 ),
609 wp_timezone_string(),
610 date( 'H:i', current_time( 'timestamp' ) )// phpcs:ignore
611 ),
612 'show_notice' => ! $settings->scheduled_scanning
613 && isset( $_GET['enable'] ) && 'scheduled_scanning' === $_GET['enable'],
614 ]
615 : [];
616 // Todo: add logic for deactivated scan settings. Maybe display some notice.
617 $data = [
618 'scan' => $scan,
619 'settings' => $settings->export(),
620 'report' => $report_text,
621 'active_tools' => [
622 'integrity_check' => $settings->integrity_check,
623 'check_known_vuln' => $settings->check_known_vuln,
624 'scan_malware' => $settings->scan_malware,
625 'scheduled_scanning' => $settings->scheduled_scanning,
626 ],
627 'notification' => $report->to_string(),
628 'next_run' => $report->get_next_run_as_string(),
629 'misc' => $misc,
630 ];
631
632 return array_merge( $data, $this->dump_routes_and_nonces() );
633 }
634
635 public function import_data( $data ) {
636 $model = $this->model;
637
638 $model->import( $data );
639 if ( $model->validate() ) {
640 $model->save();
641 }
642 }
643
644 /**
645 * @param bool $is_pro
646 *
647 * @return bool
648 */
649 private function is_any_active( bool $is_pro ): bool {
650 $settings = new \WP_Defender\Model\Setting\Scan();
651 $integrity_check = $settings->is_any_filetypes_checked();
652 if ( ! $integrity_check && ! $is_pro ) {
653 return false;
654 } elseif (
655 ( ! $integrity_check && ! $settings->check_known_vuln && ! $settings->scan_malware )
656 && ! $is_pro
657 ) {
658 return false;
659 }
660
661 return true;
662 }
663
664 /**
665 * @return array
666 */
667 public function export_strings(): array {
668 $strings = [];
669 $is_pro = ( new \WP_Defender\Behavior\WPMUDEV() )->is_pro();
670 if ( $this->is_any_active( $is_pro ) ) {
671 $strings[] = __( 'Active', 'wpdef' );
672 } else {
673 $strings[] = __( 'Inactive', 'wpdef' );
674 }
675
676 $scan_report = new Malware_Report();
677 $scan_notification = new \WP_Defender\Model\Notification\Malware_Notification();
678 if ( 'enabled' === $scan_notification->status ) {
679 $strings[] = __( 'Email notifications active', 'wpdef' );
680 }
681 if ( $is_pro && 'enabled' === $scan_report->status ) {
682 $strings[] = sprintf(
683 __( 'Email reports sending %s', 'wpdef' ),
684 $scan_report->frequency
685 );
686 } elseif ( ! $is_pro ) {
687 $strings[] = sprintf(
688 __( 'Email report inactive %s', 'wpdef' ),
689 '<span class="sui-tag sui-tag-pro">Pro</span>'
690 );
691 }
692
693 return $strings;
694 }
695
696 /**
697 * @param array $config
698 * @param bool $is_pro
699 *
700 * @return array
701 */
702 public function config_strings( array $config, bool $is_pro ): array {
703 $strings = [];
704 $strings[] = $this->service->is_any_scan_active( $config, $is_pro )
705 ? __( 'Active', 'wpdef' )
706 : __( 'Inactive', 'wpdef' );
707
708 if ( 'enabled' === $config['notification'] ) {
709 $strings[] = __( 'Email notifications active', 'wpdef' );
710 }
711 if ( $is_pro && 'enabled' === $config['report'] ) {
712 $strings[] = sprintf(
713 /* translators: ... */
714 __( 'Email reports sending %s', 'wpdef' ),
715 $config['frequency']
716 );
717 } elseif ( ! $is_pro ) {
718 $strings[] = sprintf(
719 /* translators: ... */
720 __( 'Email report inactive %s', 'wpdef' ),
721 '<span class="sui-tag sui-tag-pro">Pro</span>'
722 );
723 }
724
725 return $strings;
726 }
727
728 /**
729 * Triggers the asynchronous scan.
730 *
731 * @param string $type Denotes type of the scan from the following four possible values scan, install, hub or report.
732 *
733 * @return void
734 */
735 public function do_async_scan( string $type ): void {
736 wd_di()->get( Model_Scan::class )->delete_idle();
737
738 as_enqueue_async_action(
739 'defender/async_scan',
740 [
741 'type' => $type,
742 ],
743 'defender'
744 );
745 }
746
747 /**
748 * Clear completed action scheduler logs.
749 *
750 * @return void
751 * @since 2.6.5
752 */
753 public function clear_scan_logs(): void {
754 $result = \WP_Defender\Component\Scan::clear_logs();
755
756 if ( isset( $result['error'] ) ) {
757 $this->log( 'WP CRON Error : ' . $result['error'], 'scan.log' );
758 }
759 }
760 }
761