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

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

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