iThemes Security (formerly Better WP Security) - Version 7.0.1

Version Description

  • Bug Fix: Fixed an "Uncaught Error: Call to undefined function esc_like()" error that could occur when exporting or erasing personal data.
  • Bug Fix: Skip recovery if File Change storage is empty.
Download this release

Release Info

Developer TimothyBlynJacobs
Plugin Icon 128x128 iThemes Security (formerly Better WP Security)
Version 7.0.1
Comparing to
See all releases

Code changes from version 6.9.2 to 7.0.1

Files changed (75) hide show
  1. better-wp-security.php +1 -1
  2. core/admin-pages/css/style.css +14 -11
  3. core/admin-pages/init.php +5 -36
  4. core/admin-pages/logs-list-table.php +2 -2
  5. core/core.php +13 -3
  6. core/history.txt +46 -0
  7. core/lib.php +124 -44
  8. core/lib/class-itsec-lib-directory.php +3 -0
  9. core/lib/class-itsec-lib-distributed-storage.php +632 -0
  10. core/lib/class-itsec-lib-file.php +96 -76
  11. core/lib/class-itsec-lib-login-interstitial.php +729 -0
  12. core/lib/class-itsec-lib-password-requirements.php +61 -278
  13. core/lib/class-itsec-scheduler-cron.php +62 -8
  14. core/lib/class-itsec-scheduler-page-load.php +10 -3
  15. core/lib/class-itsec-scheduler.php +43 -6
  16. core/lib/debug.php +25 -3
  17. core/lib/log.php +146 -1
  18. core/lib/schema.php +11 -0
  19. core/modules/away-mode/class-itsec-away-mode.php +18 -1
  20. core/modules/backup/class-itsec-backup.php +2 -0
  21. core/modules/backup/privacy.php +46 -0
  22. core/modules/file-change/activate.php +2 -4
  23. core/modules/file-change/admin.php +77 -39
  24. core/modules/file-change/class-itsec-file-change.php +149 -11
  25. core/modules/file-change/deactivate.php +2 -1
  26. core/modules/file-change/js/file-scanner.js +153 -0
  27. core/modules/file-change/js/script.js +33 -11
  28. core/modules/file-change/js/settings-page.js +16 -49
  29. core/modules/file-change/lib/chunk-scanner.php +154 -0
  30. core/modules/file-change/lib/hash-comparator-chain.php +141 -0
  31. core/modules/file-change/lib/hash-comparator-loadable.php +30 -0
  32. core/modules/file-change/lib/hash-comparator-managed-files.php +38 -0
  33. core/modules/file-change/lib/hash-comparator.php +39 -0
  34. core/modules/file-change/lib/hash-loading-failed-exception.php +50 -0
  35. core/modules/file-change/lib/index.php +1 -0
  36. core/modules/file-change/lib/package-core.php +52 -0
  37. core/modules/file-change/lib/package-factory.php +309 -0
  38. core/modules/file-change/lib/package-plugin.php +108 -0
  39. core/modules/file-change/lib/package-system.php +42 -0
  40. core/modules/file-change/lib/package-theme.php +114 -0
  41. core/modules/file-change/lib/package-unknown.php +42 -0
  42. core/modules/file-change/lib/package.php +49 -0
  43. core/modules/file-change/logs.php +40 -1
  44. core/modules/file-change/scanner.php +797 -294
  45. core/modules/file-change/settings-page.php +46 -43
  46. core/modules/file-change/settings.php +15 -25
  47. core/modules/file-change/setup.php +196 -6
  48. core/modules/file-change/sync-verbs/itsec-perform-file-scan.php +1 -1
  49. core/modules/file-change/sync-verbs/itsec-ping-file-scan.php +27 -0
  50. core/modules/file-change/validator.php +4 -35
  51. core/modules/global/active.php +16 -22
  52. core/modules/global/js/settings-page.js +3 -0
  53. core/modules/global/privacy.php +45 -0
  54. core/modules/global/settings-page.php +10 -2
  55. core/modules/global/settings.php +1 -1
  56. core/modules/global/validator.php +3 -2
  57. core/modules/hide-backend/privacy.php +24 -0
  58. core/modules/ipcheck/privacy.php +25 -0
  59. core/modules/malware/privacy.php +17 -0
  60. core/modules/notification-center/class-notification-center.php +1 -1
  61. core/modules/notification-center/validator.php +1 -1
  62. core/modules/privacy/active.php +5 -0
  63. core/modules/privacy/class-itsec-privacy.php +51 -0
  64. core/modules/privacy/index.php +1 -0
  65. core/modules/privacy/util.php +196 -0
  66. core/modules/security-check/scanner.php +3 -0
  67. core/modules/ssl/class-itsec-ssl.php +1 -1
  68. core/modules/strong-passwords/js/script.js +1 -0
  69. core/modules/system-tweaks/config-generators.php +2 -2
  70. core/modules/system-tweaks/setup.php +4 -5
  71. core/notify.php +19 -100
  72. core/response.php +1 -0
  73. core/setup.php +4 -0
  74. history.txt +36 -0
  75. readme.txt +42 -4
better-wp-security.php CHANGED
@@ -6,7 +6,7 @@
6
  * Description: Take the guesswork out of WordPress security. iThemes Security offers 30+ ways to lock down WordPress in an easy-to-use WordPress security plugin.
7
  * Author: iThemes
8
  * Author URI: https://ithemes.com
9
- * Version: 6.9.2
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
6
  * Description: Take the guesswork out of WordPress security. iThemes Security offers 30+ ways to lock down WordPress in an easy-to-use WordPress security plugin.
7
  * Author: iThemes
8
  * Author URI: https://ithemes.com
9
+ * Version: 7.0.1
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
core/admin-pages/css/style.css CHANGED
@@ -252,13 +252,13 @@ body.itsec-modal-open {
252
  background: #efefef;
253
  cursor: pointer;
254
  }
255
- .itsec-modal-navigation button.itsec-left:before {
256
  content: '\f341';
257
  }
258
- .itsec-modal-navigation button.itsec-right:before {
259
  content: '\f345';
260
  }
261
- .itsec-modal-navigation button.itsec-close-modal:before {
262
  content: '\f335';
263
  }
264
  .itsec-modal-content-footer {
@@ -318,7 +318,7 @@ body.itsec-modal-open {
318
  padding: .35em 0;
319
  }
320
  .itsec-settings-view-toggle a,
321
- .itsec-settings-view-toggle a:before {
322
  color: #b4b9be;
323
  padding: 0 .25em 0 0;
324
  }
@@ -641,7 +641,7 @@ body.itsec-modal-open {
641
  position: relative;
642
  }
643
 
644
- .itsec-security-check-container:before {
645
  font-family: 'Dashicons';
646
  font-size: 1.75em;
647
  position: absolute;
@@ -650,20 +650,20 @@ body.itsec-modal-open {
650
  margin: -9px 0 0 0;
651
  }
652
 
653
- .itsec-security-check-container-action-taken:before,
654
- .itsec-security-check-container-complete:before,
655
- .itsec-security-check-container-confirmation:before {
656
  content: "\f147";
657
  color: #46b450;
658
  }
659
 
660
- .itsec-security-check-container-incomplete:before,
661
- .itsec-security-check-container-call-to-action:before {
662
  content: "\f534";
663
  color: #f2dd28;
664
  }
665
 
666
- .itsec-security-check-container-error:before {
667
  content: "\f534";
668
  color: #dc3232;
669
  }
@@ -689,6 +689,9 @@ body.itsec-modal-open {
689
  .itsec-two-factor .dashicons {
690
  cursor: default;
691
  }
 
 
 
692
  .itsec-two-factor .dashicons.dashicons-unlock {
693
  color: #dc3232;
694
  }
252
  background: #efefef;
253
  cursor: pointer;
254
  }
255
+ .itsec-modal-navigation button.itsec-left::before {
256
  content: '\f341';
257
  }
258
+ .itsec-modal-navigation button.itsec-right::before {
259
  content: '\f345';
260
  }
261
+ .itsec-modal-navigation button.itsec-close-modal::before {
262
  content: '\f335';
263
  }
264
  .itsec-modal-content-footer {
318
  padding: .35em 0;
319
  }
320
  .itsec-settings-view-toggle a,
321
+ .itsec-settings-view-toggle a::before {
322
  color: #b4b9be;
323
  padding: 0 .25em 0 0;
324
  }
641
  position: relative;
642
  }
643
 
644
+ .itsec-security-check-container::before {
645
  font-family: 'Dashicons';
646
  font-size: 1.75em;
647
  position: absolute;
650
  margin: -9px 0 0 0;
651
  }
652
 
653
+ .itsec-security-check-container-action-taken::before,
654
+ .itsec-security-check-container-complete::before,
655
+ .itsec-security-check-container-confirmation::before {
656
  content: "\f147";
657
  color: #46b450;
658
  }
659
 
660
+ .itsec-security-check-container-incomplete::before,
661
+ .itsec-security-check-container-call-to-action::before {
662
  content: "\f534";
663
  color: #f2dd28;
664
  }
665
 
666
+ .itsec-security-check-container-error::before {
667
  content: "\f534";
668
  color: #dc3232;
669
  }
689
  .itsec-two-factor .dashicons {
690
  cursor: default;
691
  }
692
+ .itsec-two-factor .dashicons.not-configured {
693
+ color: #F56E28;
694
+ }
695
  .itsec-two-factor .dashicons.dashicons-unlock {
696
  color: #dc3232;
697
  }
core/admin-pages/init.php CHANGED
@@ -4,7 +4,6 @@
4
  final class ITSEC_Admin_Page_Loader {
5
  private $page_refs = array();
6
  private $page_id;
7
- private $translations = array();
8
 
9
 
10
  public function __construct() {
@@ -24,47 +23,13 @@ final class ITSEC_Admin_Page_Loader {
24
  }
25
 
26
  public function add_scripts() {
27
- $this->set_translation_strings();
28
-
29
- $vars = array(
30
- 'ajax_action' => 'itsec_settings_page',
31
- 'ajax_nonce' => wp_create_nonce( 'itsec-settings-nonce' ),
32
- 'translations' => $this->translations,
33
- );
34
-
35
- wp_enqueue_script( 'itsec-util-script', plugins_url( 'js/util.js', __FILE__ ), array(), ITSEC_Core::get_plugin_build(), true );
36
- wp_localize_script( 'itsec-util-script', 'itsec_util', $vars );
37
  }
38
 
39
  public function add_styles() {
40
  wp_enqueue_style( 'itsec-settings-page-style', plugins_url( 'css/style.css', __FILE__ ), array(), ITSEC_Core::get_plugin_build() );
41
  }
42
 
43
- private function set_translation_strings() {
44
- $this->translations = array(
45
- 'ajax_invalid' => new WP_Error( 'itsec-settings-page-invalid-ajax-response', __( 'An "invalid format" error prevented the request from completing as expected. The format of data returned could not be recognized. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
46
-
47
- 'ajax_forbidden' => new WP_Error( 'itsec-settings-page-forbidden-ajax-response: %1$s "%2$s"', __( 'A "request forbidden" error prevented the request from completing as expected. The server returned a 403 status code, indicating that the server configuration is prohibiting this request. This could be due to a plugin/theme conflict or a server configuration issue. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings or server configuration that could account for this AJAX request being blocked.', 'better-wp-security' ) ),
48
-
49
- 'ajax_not_found' => new WP_Error( 'itsec-settings-page-not-found-ajax-response: %1$s "%2$s"', __( 'A "not found" error prevented the request from completing as expected. The server returned a 404 status code, indicating that the server was unable to find the requested admin-ajax.php file. This could be due to a plugin/theme conflict, a server configuration issue, or an incomplete WordPress installation. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings, alter server configurations, or reinstall WordPress.', 'better-wp-security' ) ),
50
-
51
- 'ajax_server_error' => new WP_Error( 'itsec-settings-page-server-error-ajax-response: %1$s "%2$s"', __( 'A "internal server" error prevented the request from completing as expected. The server returned a 500 status code, indicating that the server was unable to complete the request due to a fatal PHP error or a server problem. This could be due to a plugin/theme conflict, a server configuration issue, a temporary hosting issue, or invalid custom PHP modifications. Please check your server\'s error logs for details about the source of the error and contact your hosting company for assistance if required.', 'better-wp-security' ) ),
52
-
53
- 'ajax_unknown' => new WP_Error( 'itsec-settings-page-ajax-error-unknown: %1$s "%2$s"', __( 'An unknown error prevented the request from completing as expected. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
54
-
55
- 'ajax_timeout' => new WP_Error( 'itsec-settings-page-ajax-error-timeout: %1$s "%2$s"', __( 'A timeout error prevented the request from completing as expected. The site took too long to respond. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
56
-
57
- 'ajax_parsererror' => new WP_Error( 'itsec-settings-page-ajax-error-parsererror: %1$s "%2$s"', __( 'A parser error prevented the request from completing as expected. The site sent a response that jQuery could not process. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
58
- );
59
-
60
- foreach ( $this->translations as $key => $message ) {
61
- if ( is_wp_error( $message ) ) {
62
- $messages = ITSEC_Response::get_error_strings( $message );
63
- $this->translations[$key] = $messages[0];
64
- }
65
- }
66
- }
67
-
68
  public function add_admin_pages() {
69
  $capability = ITSEC_Core::get_required_cap();
70
  $page_refs = array();
@@ -72,6 +37,9 @@ final class ITSEC_Admin_Page_Loader {
72
  add_menu_page( __( 'Settings', 'better-wp-security' ), __( 'Security', 'better-wp-security' ), $capability, 'itsec', array( $this, 'show_page' ) );
73
  $page_refs[] = add_submenu_page( 'itsec', __( 'iThemes Security Settings', 'better-wp-security' ), __( 'Settings', 'better-wp-security' ), $capability, 'itsec', array( $this, 'show_page' ) );
74
  $page_refs[] = add_submenu_page( 'itsec', '', __( 'Security Check', 'better-wp-security' ), $capability, 'itsec-security-check', array( $this, 'show_page' ) );
 
 
 
75
  $page_refs[] = add_submenu_page( 'itsec', __( 'iThemes Security Logs', 'better-wp-security' ), __( 'Logs', 'better-wp-security' ), $capability, 'itsec-logs', array( $this, 'show_page' ) );
76
 
77
  if ( ! ITSEC_Core::is_pro() ) {
@@ -146,6 +114,7 @@ final class ITSEC_Admin_Page_Loader {
146
  $id = str_replace( '_', '-', $id );
147
 
148
  $file = dirname( __FILE__ ) . '/' . sprintf( $file, $id );
 
149
 
150
  if ( is_file( $file ) ) {
151
  require_once( $file );
4
  final class ITSEC_Admin_Page_Loader {
5
  private $page_refs = array();
6
  private $page_id;
 
7
 
8
 
9
  public function __construct() {
23
  }
24
 
25
  public function add_scripts() {
26
+ ITSEC_Lib::enqueue_util();
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
  public function add_styles() {
30
  wp_enqueue_style( 'itsec-settings-page-style', plugins_url( 'css/style.css', __FILE__ ), array(), ITSEC_Core::get_plugin_build() );
31
  }
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  public function add_admin_pages() {
34
  $capability = ITSEC_Core::get_required_cap();
35
  $page_refs = array();
37
  add_menu_page( __( 'Settings', 'better-wp-security' ), __( 'Security', 'better-wp-security' ), $capability, 'itsec', array( $this, 'show_page' ) );
38
  $page_refs[] = add_submenu_page( 'itsec', __( 'iThemes Security Settings', 'better-wp-security' ), __( 'Settings', 'better-wp-security' ), $capability, 'itsec', array( $this, 'show_page' ) );
39
  $page_refs[] = add_submenu_page( 'itsec', '', __( 'Security Check', 'better-wp-security' ), $capability, 'itsec-security-check', array( $this, 'show_page' ) );
40
+
41
+ $page_refs = apply_filters( 'itsec-admin-page-refs', $page_refs, $capability, array( $this, 'show_page' ) );
42
+
43
  $page_refs[] = add_submenu_page( 'itsec', __( 'iThemes Security Logs', 'better-wp-security' ), __( 'Logs', 'better-wp-security' ), $capability, 'itsec-logs', array( $this, 'show_page' ) );
44
 
45
  if ( ! ITSEC_Core::is_pro() ) {
114
  $id = str_replace( '_', '-', $id );
115
 
116
  $file = dirname( __FILE__ ) . '/' . sprintf( $file, $id );
117
+ $file = apply_filters( "itsec-admin-page-file-path-$id", $file );
118
 
119
  if ( is_file( $file ) ) {
120
  require_once( $file );
core/admin-pages/logs-list-table.php CHANGED
@@ -319,7 +319,7 @@ final class ITSEC_Logs_List_Table extends ITSEC_WP_List_Table {
319
  'important' => esc_html__( 'Important Events (%s)', 'better-wp-security' ),
320
  'all' => esc_html__( 'All Events (%s)', 'better-wp-security' ),
321
  'critical-issue' => esc_html__( 'Critical Issues (%s)', 'better-wp-security' ),
322
- 'fatal-error' => esc_html__( 'Fatal Errors (%s)', 'better-wp-security' ),
323
  'error' => esc_html__( 'Errors (%s)', 'better-wp-security' ),
324
  'warning' => esc_html__( 'Warnings (%s)', 'better-wp-security' ),
325
  'action' => esc_html__( 'Actions (%s)', 'better-wp-security' ),
@@ -350,7 +350,7 @@ final class ITSEC_Logs_List_Table extends ITSEC_WP_List_Table {
350
 
351
  $views[$type] = sprintf( $description, $counts[$type] );
352
 
353
- if ( in_array( $type, array( 'critical-issue', 'fatal-error', 'error', 'warning' ) ) ) {
354
  $important_count += $counts[$type];
355
  }
356
 
319
  'important' => esc_html__( 'Important Events (%s)', 'better-wp-security' ),
320
  'all' => esc_html__( 'All Events (%s)', 'better-wp-security' ),
321
  'critical-issue' => esc_html__( 'Critical Issues (%s)', 'better-wp-security' ),
322
+ 'fatal' => esc_html__( 'Fatal Errors (%s)', 'better-wp-security' ),
323
  'error' => esc_html__( 'Errors (%s)', 'better-wp-security' ),
324
  'warning' => esc_html__( 'Warnings (%s)', 'better-wp-security' ),
325
  'action' => esc_html__( 'Actions (%s)', 'better-wp-security' ),
350
 
351
  $views[$type] = sprintf( $description, $counts[$type] );
352
 
353
+ if ( in_array( $type, array( 'critical-issue', 'fatal', 'error', 'warning' ) ) ) {
354
  $important_count += $counts[$type];
355
  }
356
 
core/core.php CHANGED
@@ -24,7 +24,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
24
  *
25
  * @access private
26
  */
27
- private $plugin_build = 4087;
28
 
29
  /**
30
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
@@ -120,6 +120,8 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
120
  require( $this->plugin_dir . 'core/response.php' );
121
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-user-activity.php' );
122
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-password-requirements.php' );
 
 
123
 
124
  require( $this->plugin_dir . 'core/lib/class-itsec-scheduler.php' );
125
  require( $this->plugin_dir . 'core/lib/class-itsec-job.php' );
@@ -168,6 +170,9 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
168
  $pass_requirements->run();
169
  }
170
 
 
 
 
171
  if ( defined( 'ITSEC_USE_CRON' ) && ITSEC_USE_CRON !== ITSEC_Lib::use_cron() ) {
172
  ITSEC_Modules::set_setting( 'global', 'use_cron', ITSEC_USE_CRON );
173
  }
@@ -303,6 +308,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
303
  ITSEC_Modules::register_module( 'security-check', "$path/modules/security-check", 'always-active' );
304
  ITSEC_Modules::register_module( 'global', "$path/modules/global", 'always-active' );
305
  ITSEC_Modules::register_module( 'notification-center', "$path/modules/notification-center", 'always-active' );
 
306
  ITSEC_Modules::register_module( '404-detection', "$path/modules/404-detection" );
307
  ITSEC_Modules::register_module( 'admin-user', "$path/modules/admin-user", 'always-active' );
308
  ITSEC_Modules::register_module( 'away-mode', "$path/modules/away-mode" );
@@ -604,7 +610,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
604
  $url = network_admin_url( 'admin.php?page=itsec-logs' );
605
 
606
  if ( ! empty( $filter ) ) {
607
- $url = add_query_arg( array( 'filter' => $filter ), $url );
608
  }
609
 
610
  return $url;
@@ -802,7 +808,11 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
802
  $home_path = parse_url( get_option( 'home' ), PHP_URL_PATH );
803
  $home_path = trim( $home_path, '/' );
804
 
805
- $rest_api_path = "/$home_path/" . rest_get_url_prefix() . '/';
 
 
 
 
806
 
807
  if ( 0 === strpos( $_SERVER['REQUEST_URI'], $rest_api_path ) ) {
808
  $GLOBALS['__itsec_core_is_rest_api_request'] = true;
24
  *
25
  * @access private
26
  */
27
+ private $plugin_build = 4095;
28
 
29
  /**
30
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
120
  require( $this->plugin_dir . 'core/response.php' );
121
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-user-activity.php' );
122
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-password-requirements.php' );
123
+ require( $this->plugin_dir . 'core/lib/class-itsec-lib-login-interstitial.php' );
124
+ require( $this->plugin_dir . 'core/lib/class-itsec-lib-distributed-storage.php' );
125
 
126
  require( $this->plugin_dir . 'core/lib/class-itsec-scheduler.php' );
127
  require( $this->plugin_dir . 'core/lib/class-itsec-job.php' );
170
  $pass_requirements->run();
171
  }
172
 
173
+ $login_interstitial = new ITSEC_Lib_Login_Interstitial();
174
+ $login_interstitial->run();
175
+
176
  if ( defined( 'ITSEC_USE_CRON' ) && ITSEC_USE_CRON !== ITSEC_Lib::use_cron() ) {
177
  ITSEC_Modules::set_setting( 'global', 'use_cron', ITSEC_USE_CRON );
178
  }
308
  ITSEC_Modules::register_module( 'security-check', "$path/modules/security-check", 'always-active' );
309
  ITSEC_Modules::register_module( 'global', "$path/modules/global", 'always-active' );
310
  ITSEC_Modules::register_module( 'notification-center', "$path/modules/notification-center", 'always-active' );
311
+ ITSEC_Modules::register_module( 'privacy', "$path/modules/privacy", 'always-active' );
312
  ITSEC_Modules::register_module( '404-detection', "$path/modules/404-detection" );
313
  ITSEC_Modules::register_module( 'admin-user', "$path/modules/admin-user", 'always-active' );
314
  ITSEC_Modules::register_module( 'away-mode', "$path/modules/away-mode" );
610
  $url = network_admin_url( 'admin.php?page=itsec-logs' );
611
 
612
  if ( ! empty( $filter ) ) {
613
+ $url = add_query_arg( array( 'filters' => rawurlencode( "module|{$filter}" ) ), $url );
614
  }
615
 
616
  return $url;
808
  $home_path = parse_url( get_option( 'home' ), PHP_URL_PATH );
809
  $home_path = trim( $home_path, '/' );
810
 
811
+ if ( '' === $home_path ) {
812
+ $rest_api_path = '/' . rest_get_url_prefix() . '/';
813
+ } else {
814
+ $rest_api_path = "/$home_path/" . rest_get_url_prefix() . '/';
815
+ }
816
 
817
  if ( 0 === strpos( $_SERVER['REQUEST_URI'], $rest_api_path ) ) {
818
  $GLOBALS['__itsec_core_is_rest_api_request'] = true;
core/history.txt CHANGED
@@ -652,3 +652,49 @@
652
  Bug Fix: Fixed situation that could cause lockout notifications being sent for whitelisted IPs.
653
  Bug Fix: Fixed issue where saving Global Settings would be blocked by an unwritable "Path to Log Files" path when the "Log Type" is set to "Database Only".
654
  Bug Fix: Fixed issue that prevented log database entries from purging and log file entries from rotating on a schedule.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
  Bug Fix: Fixed situation that could cause lockout notifications being sent for whitelisted IPs.
653
  Bug Fix: Fixed issue where saving Global Settings would be blocked by an unwritable "Path to Log Files" path when the "Log Type" is set to "Database Only".
654
  Bug Fix: Fixed issue that prevented log database entries from purging and log file entries from rotating on a schedule.
655
+ 4.1.6 - 2018-03-20 - Chris Jean & Timothy Jacobs
656
+ Bug Fix: Added ability to show object data for classes that are not loaded to the Logs page.
657
+ Bug Fix: Fixed logging system references to "fatal-error" that should be "fatal".
658
+ Bug Fix: Prevent PHP warning when completing database backups that are not emailed to any recipients.
659
+ Bug Fix: Prevent PHP warning about converting an array to a string when adding notification data.
660
+ 4.2.0 - 2018-03-29 - Chris Jean & Timothy Jacobs
661
+ Enhancement: File Change Scan uses a new batching mechanism to prevent crashing on hosts but still generating only one report per-day.
662
+ Minor: Updated list of File Change excluded file types to include more media extensions.
663
+ Minor: File Scan "chunk" option is removed.
664
+ Minor: Specifying a manual file scan list has been removed.
665
+ Minor: Security Digest now includes all lockouts that have occurred since the last email.
666
+ 4.2.1 - 2018-03-30 - Chris Jean & Timothy Jacobs
667
+ Minor: Track raw memory used by the file change scanner as well.
668
+ Minor: Page Load Scheduler: Unschedule single events before running them. This mirrors the behavior of the WP Cron scheduler.
669
+ 4.2.2 - 2018-04-04 - Chris Jean & Timothy Jacobs
670
+ Minor: Shrink storage size of file scans.
671
+ Minor: Make recovering file scan log smaller.
672
+ 4.3.0 - 2018-04-12 - Chris Jean & Timothy Jacobs
673
+ Bug Fix: Ensure all users with the `manage_options` capability are available when selecting contacts in the Notification Center.
674
+ Enhancement: Added minimal API for adding additional entries to the Security admin menu.
675
+ 4.3.1 - 2018-04-17 - Chris Jean & Timothy Jacobs
676
+ Tweak: Add description for File Change recovery related logs.
677
+ Tweak: Don't report removed files if the removal is caused by a new file extension being excluded.
678
+ Bug Fix: Improved detection of REST API requests on sites without a home dir.
679
+ Bug Fix: Improve File Change recovery system on high-traffic websites.
680
+ Bug Fix: Fix warnings on debug file change log items.
681
+ 4.4.0 - 2018-04-19 - Chris Jean & Timothy Jacobs
682
+ Enhancement: Introduced Login Interstitial framework to consolidate code between Password Requirements & Two Factor.
683
+ Bug Fix: Resolve warnings when upgrading file change settings.
684
+ 4.4.1 - 2018-04-25 - Chris Jean & Timothy Jacobs
685
+ Misc: Added comment to prevent Tide from marking the plugin as not compatible with PHP 5.3.
686
+ Bug Fix: Improve clearing of previous File Change file hashes.
687
+ Bug Fix: Internal links to a filtered logs page.
688
+ 4.4.2 - 2018-05-02 - Chris Jean & Timothy Jacobs
689
+ Tweak: File Change: Only scan a maximum of 10 plugins in a single chunk.
690
+ Tweak: File Change: Move "latest_changes" entry to a separate storage bucket to improve performance on large sites.
691
+ Bug Fix: Properly enforce strong passwords when on the WP Login Reset Password page.
692
+ Bug Fix: Fix clearing or previous file scans results.
693
+ 4.4.3 - 2018-05-22 - Chris Jean & Timothy Jacobs
694
+ Enhancement: Introduce Distributed Storage framework for reducing the amount of data stored in the WordPress options table. This should improve performance for large sites using File Change.
695
+ 4.5.0 - 2018-05-24 - Chris Jean & Timothy Jacobs
696
+ New Feature: Added support for the new WordPress privacy features.
697
+ Bug Fix: Changed the rules generated by the Filter Suspicious Query Strings feature in order to avoid blocking privacy export/erasure request confirmations.
698
+ 4.5.1 - 2018-05-25 - Chris Jean & Timothy Jacobs
699
+ Bug Fix: Fixed an "Uncaught Error: Call to undefined function esc_like()" error that could occur when exporting or erasing personal data.
700
+ Bug Fix: Skip recovery if File Change storage is empty.
core/lib.php CHANGED
@@ -112,45 +112,6 @@ final class ITSEC_Lib {
112
  return implode( '.', $host_parts );
113
  }
114
 
115
- /**
116
- * Get path to WordPress install.
117
- *
118
- * Get the absolute filesystem path to the root of the WordPress installation.
119
- *
120
- * @since 4.3.0
121
- *
122
- * @return string Full filesystem path to the root of the WordPress installation
123
- */
124
- public static function get_home_path() {
125
-
126
- $home = set_url_scheme( get_option( 'home' ), 'http' );
127
- $siteurl = set_url_scheme( get_option( 'siteurl' ), 'http' );
128
-
129
- if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) {
130
-
131
- $wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */
132
- $pos = strripos( str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ), trailingslashit( $wp_path_rel_to_home ) );
133
-
134
- if ( $pos === false ) {
135
-
136
- $home_path = dirname( $_SERVER['SCRIPT_FILENAME'] );
137
-
138
- } else {
139
-
140
- $home_path = substr( $_SERVER['SCRIPT_FILENAME'], 0, $pos );
141
-
142
- }
143
-
144
- } else {
145
-
146
- $home_path = ABSPATH;
147
-
148
- }
149
-
150
- return trailingslashit( str_replace( '\\', '/', $home_path ) );
151
-
152
- }
153
-
154
  /**
155
  * Returns the root of the WordPress install.
156
  *
@@ -832,6 +793,31 @@ final class ITSEC_Lib {
832
  }
833
  }
834
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
835
  /**
836
  * Clear any expired locks.
837
  *
@@ -1060,11 +1046,11 @@ final class ITSEC_Lib {
1060
  return;
1061
  }
1062
 
1063
- $crons = _get_cron_array();
1064
-
1065
- foreach ( $crons as $timestamp => $cron ) {
1066
- if ( isset( $cron['itsec_cron_test'] ) ) {
1067
- return;
1068
  }
1069
  }
1070
 
@@ -1073,4 +1059,98 @@ final class ITSEC_Lib {
1073
  wp_schedule_single_event( $time, 'itsec_cron_test', array( $time ) );
1074
  ITSEC_Modules::set_setting( 'global', 'cron_test_time', $time );
1075
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1076
  }
112
  return implode( '.', $host_parts );
113
  }
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  /**
116
  * Returns the root of the WordPress install.
117
  *
793
  }
794
  }
795
 
796
+ public static function has_lock( $name ) {
797
+
798
+ /** @var \wpdb $wpdb */
799
+ global $wpdb;
800
+ $main_options = $wpdb->base_prefix . 'options';
801
+
802
+ $lock = "itsec-lock-{$name}";
803
+
804
+ if ( is_multisite() ) {
805
+ $result = $wpdb->get_var( $wpdb->prepare( "SELECT `option_value` FROM `{$main_options}` WHERE `option_name` = %s", $lock ) );
806
+ } else {
807
+ $result = $wpdb->get_var( $wpdb->prepare( "SELECT `option_value` FROM `$wpdb->options` WHERE `option_name` = %s", $lock ) );
808
+ }
809
+
810
+ if ( ! $result ) {
811
+ return false;
812
+ }
813
+
814
+ if ( (int) $result < ITSEC_Core::get_current_time_gmt() ) {
815
+ return false;
816
+ }
817
+
818
+ return true;
819
+ }
820
+
821
  /**
822
  * Clear any expired locks.
823
  *
1046
  return;
1047
  }
1048
 
1049
+ if ( $crons = _get_cron_array() ) {
1050
+ foreach ( $crons as $timestamp => $cron ) {
1051
+ if ( isset( $cron['itsec_cron_test'] ) ) {
1052
+ return;
1053
+ }
1054
  }
1055
  }
1056
 
1059
  wp_schedule_single_event( $time, 'itsec_cron_test', array( $time ) );
1060
  ITSEC_Modules::set_setting( 'global', 'cron_test_time', $time );
1061
  }
1062
+
1063
+ public static function fwdslash( $string ) {
1064
+ return '/' . ltrim( $string, '/' );
1065
+ }
1066
+
1067
+ /**
1068
+ * Enqueue the itsec_util script.
1069
+ *
1070
+ * Will only be included once per page.
1071
+ */
1072
+ public static function enqueue_util() {
1073
+
1074
+ static $enqueued = false;
1075
+
1076
+ if ( $enqueued ) {
1077
+ return;
1078
+ }
1079
+
1080
+ $translations = array(
1081
+ 'ajax_invalid' => new WP_Error( 'itsec-settings-page-invalid-ajax-response', __( 'An "invalid format" error prevented the request from completing as expected. The format of data returned could not be recognized. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
1082
+ 'ajax_forbidden' => new WP_Error( 'itsec-settings-page-forbidden-ajax-response: %1$s "%2$s"', __( 'A "request forbidden" error prevented the request from completing as expected. The server returned a 403 status code, indicating that the server configuration is prohibiting this request. This could be due to a plugin/theme conflict or a server configuration issue. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings or server configuration that could account for this AJAX request being blocked.', 'better-wp-security' ) ),
1083
+ 'ajax_not_found' => new WP_Error( 'itsec-settings-page-not-found-ajax-response: %1$s "%2$s"', __( 'A "not found" error prevented the request from completing as expected. The server returned a 404 status code, indicating that the server was unable to find the requested admin-ajax.php file. This could be due to a plugin/theme conflict, a server configuration issue, or an incomplete WordPress installation. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings, alter server configurations, or reinstall WordPress.', 'better-wp-security' ) ),
1084
+ 'ajax_server_error' => new WP_Error( 'itsec-settings-page-server-error-ajax-response: %1$s "%2$s"', __( 'A "internal server" error prevented the request from completing as expected. The server returned a 500 status code, indicating that the server was unable to complete the request due to a fatal PHP error or a server problem. This could be due to a plugin/theme conflict, a server configuration issue, a temporary hosting issue, or invalid custom PHP modifications. Please check your server\'s error logs for details about the source of the error and contact your hosting company for assistance if required.', 'better-wp-security' ) ),
1085
+ 'ajax_unknown' => new WP_Error( 'itsec-settings-page-ajax-error-unknown: %1$s "%2$s"', __( 'An unknown error prevented the request from completing as expected. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
1086
+ 'ajax_timeout' => new WP_Error( 'itsec-settings-page-ajax-error-timeout: %1$s "%2$s"', __( 'A timeout error prevented the request from completing as expected. The site took too long to respond. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
1087
+ 'ajax_parsererror' => new WP_Error( 'itsec-settings-page-ajax-error-parsererror: %1$s "%2$s"', __( 'A parser error prevented the request from completing as expected. The site sent a response that jQuery could not process. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ),
1088
+ );
1089
+
1090
+ foreach ( $translations as $i => $translation ) {
1091
+ $messages = ITSEC_Response::get_error_strings( $translation );
1092
+
1093
+ if ( $messages ) {
1094
+ $translations[ $i ] = $messages[0];
1095
+ }
1096
+ }
1097
+
1098
+ wp_enqueue_script( 'itsec-util', plugins_url( 'admin-pages/js/util.js', __FILE__ ), array( 'jquery' ), ITSEC_Core::get_plugin_build(), true );
1099
+ wp_localize_script( 'itsec-util', 'itsec_util', array(
1100
+ 'ajax_action' => 'itsec_settings_page',
1101
+ 'ajax_nonce' => wp_create_nonce( 'itsec-settings-nonce' ),
1102
+ 'translations' => $translations,
1103
+ ) );
1104
+
1105
+ $enqueued = true;
1106
+ }
1107
+
1108
+ /**
1109
+ * Replace the prefix of a target string with another prefix.
1110
+ *
1111
+ * If the given target does not start with the current prefix, the string
1112
+ * will be returned unmodified.
1113
+ *
1114
+ * @param string $target String to perform replacement on.
1115
+ * @param string $current The current prefix.
1116
+ * @param string $replacement The new prefix.
1117
+ *
1118
+ * @return string
1119
+ */
1120
+ public static function replace_prefix( $target, $current, $replacement ) {
1121
+ if ( 0 !== strpos( $target, $current ) ) {
1122
+ return $target;
1123
+ }
1124
+
1125
+ $stripped = substr( $target, strlen( $current ) );
1126
+
1127
+ return $replacement . $stripped;
1128
+ }
1129
+
1130
+ /**
1131
+ * Convert an iterator to an array.
1132
+ *
1133
+ * @param iterable $iterator
1134
+ *
1135
+ * @return array
1136
+ */
1137
+ public static function iterator_to_array( $iterator ) {
1138
+
1139
+ if ( is_array( $iterator ) ) {
1140
+ return $iterator;
1141
+ }
1142
+
1143
+ // Available since PHP 5.1, but SPL which isn't guaranteed.
1144
+ if ( function_exists( 'iterator_to_array' ) ) {
1145
+ return iterator_to_array( $iterator );
1146
+ }
1147
+
1148
+ $array = array();
1149
+
1150
+ foreach ( $iterator as $key => $value ) {
1151
+ $array[ $key ] = $value;
1152
+ }
1153
+
1154
+ return $array;
1155
+ }
1156
  }
core/lib/class-itsec-lib-directory.php CHANGED
@@ -104,6 +104,7 @@ if ( ! class_exists( 'ITSEC_Lib_Directory' ) ) {
104
  return true;
105
  }
106
 
 
107
  @clearstatcache( true, $dir );
108
 
109
  return @is_dir( $dir );
@@ -198,6 +199,7 @@ if ( ! class_exists( 'ITSEC_Lib_Directory' ) ) {
198
  }
199
 
200
  $result = rmdir( $dir );
 
201
  @clearstatcache( true, $dir );
202
 
203
  if ( $result ) {
@@ -285,6 +287,7 @@ if ( ! class_exists( 'ITSEC_Lib_Directory' ) ) {
285
 
286
 
287
  $dir = rtrim( $dir, '/' );
 
288
  @clearstatcache( true, $dir );
289
 
290
  return fileperms( $dir ) & 0777;
104
  return true;
105
  }
106
 
107
+ // phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
108
  @clearstatcache( true, $dir );
109
 
110
  return @is_dir( $dir );
199
  }
200
 
201
  $result = rmdir( $dir );
202
+ // phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
203
  @clearstatcache( true, $dir );
204
 
205
  if ( $result ) {
287
 
288
 
289
  $dir = rtrim( $dir, '/' );
290
+ // phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
291
  @clearstatcache( true, $dir );
292
 
293
  return fileperms( $dir ) & 0777;
core/lib/class-itsec-lib-distributed-storage.php ADDED
@@ -0,0 +1,632 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Lib_Distributed_Storage
5
+ */
6
+ class ITSEC_Lib_Distributed_Storage {
7
+
8
+ /* --- Config --- */
9
+
10
+ /** @var string */
11
+ private $name;
12
+
13
+ /** @var array */
14
+ private $config = array();
15
+
16
+ /* --- Instance --- */
17
+
18
+ /** @var array */
19
+ private $data = array();
20
+
21
+ /**
22
+ * ITSEC_Lib_Distributed_Storage constructor.
23
+ *
24
+ * @param string $name
25
+ * @param array $config
26
+ */
27
+ public function __construct( $name, array $config ) {
28
+ $this->name = $name;
29
+
30
+ foreach ( $config as $key => $value ) {
31
+ $valid = false;
32
+
33
+ if ( array_key_exists( 'serialize', $value ) || array_key_exists( 'unserialize', $value ) ) {
34
+ if ( ! isset( $value['serialize'] ) ) {
35
+ _doing_it_wrong( __CLASS__, 'iThemes Security: Serialize function required when using unserialize.', '4.5.0' );
36
+ } elseif ( ! is_callable( $value['serialize'] ) ) {
37
+ _doing_it_wrong( __CLASS__, 'iThemes Security: Serialize function must be callable.', '4.5.0' );
38
+ } else {
39
+ $valid = true;
40
+ }
41
+
42
+ if ( ! isset( $value['unserialize'] ) ) {
43
+ _doing_it_wrong( __CLASS__, 'iThemes Security: Unserialize function required when using serialize.', '4.5.0' );
44
+ } elseif ( ! is_callable( $value['unserialize'] ) ) {
45
+ _doing_it_wrong( __CLASS__, 'iThemes Security: Unserialize function must be callable.', '4.5.0' );
46
+ } else {
47
+ $valid = true;
48
+ }
49
+ } else {
50
+ $valid = true;
51
+ }
52
+
53
+ if ( $valid ) {
54
+ $this->config[ $key ] = wp_parse_args( $value, array(
55
+ 'split' => false,
56
+ 'default' => null,
57
+ 'serialize' => 'serialize',
58
+ 'unserialize' => 'unserialize',
59
+ 'chunk' => false,
60
+ ) );
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Get the value for a given key.
67
+ *
68
+ * @param string $key
69
+ *
70
+ * @return mixed|false
71
+ */
72
+ public function get( $key ) {
73
+
74
+ if ( ! isset( $this->config[ $key ] ) ) {
75
+ _doing_it_wrong( __METHOD__, "iThemes Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
76
+
77
+ return false;
78
+ }
79
+
80
+ if ( array_key_exists( $key, $this->data ) ) {
81
+ return $this->data[ $key ];
82
+ }
83
+
84
+ $this->load( $key );
85
+
86
+ return $this->data[ $key ];
87
+ }
88
+
89
+ /**
90
+ * Get a cursor to paginate over a chunked resource.
91
+ *
92
+ * @param string $key
93
+ *
94
+ * @return ITSEC_Lib_Distributed_Storage_Cursor|null
95
+ */
96
+ public function get_cursor( $key ) {
97
+
98
+ if ( ! isset( $this->config[ $key ] ) ) {
99
+ _doing_it_wrong( __METHOD__, "iThemes Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
100
+
101
+ return null;
102
+ }
103
+
104
+ if ( ! $this->config[ $key ]['chunk'] ) {
105
+ return null;
106
+ }
107
+
108
+ $data = $this->_load_chunk( $key, 0 );
109
+ $data = null === $data ? array() : $data;
110
+
111
+ return new ITSEC_Lib_Distributed_Storage_Cursor( $this, $key, $data );
112
+ }
113
+
114
+ /**
115
+ * Set the value for a given key.
116
+ *
117
+ * @param string $key
118
+ * @param mixed $value
119
+ *
120
+ * @return bool
121
+ */
122
+ public function set( $key, $value ) {
123
+
124
+ global $wpdb;
125
+
126
+ if ( ! isset( $this->config[ $key ] ) ) {
127
+ _doing_it_wrong( __METHOD__, "iThemes Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
128
+
129
+ return false;
130
+ }
131
+
132
+ $this->data[ $key ] = $value;
133
+
134
+ $config = $this->config[ $key ];
135
+
136
+ if ( ! $config['split'] ) {
137
+ $update = array();
138
+
139
+ foreach ( $this->config as $config_key => $config_value ) {
140
+ if ( ! $config_value['split'] ) {
141
+ if ( $key === $config_key ) {
142
+ $update[ $key ] = $value;
143
+ } else {
144
+ $update[ $config_key ] = $this->get( $config_key );
145
+ }
146
+ }
147
+ }
148
+
149
+ return $this->update_row( serialize( $update ) );
150
+ }
151
+
152
+ if ( $value === $config['default'] ) {
153
+ $wpdb->query( $wpdb->prepare(
154
+ "DELETE FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s",
155
+ $this->name, $key
156
+ ) );
157
+
158
+ return $wpdb->last_error ? false : true;
159
+ }
160
+
161
+ if ( ! $config['chunk'] ) {
162
+ return $this->update_row( call_user_func( $config['serialize'], $value ), $key );
163
+ }
164
+
165
+ $r = true;
166
+ $highest = 0;
167
+
168
+ foreach ( array_chunk( $value, $config['chunk'], true ) as $i => $chunk ) {
169
+ $r_ = $this->update_row( call_user_func( $config['serialize'], $chunk ), $key, $i );
170
+
171
+ $highest = $i;
172
+ $r = $r && $r_;
173
+ }
174
+
175
+ $this->clean_chunk_options( $key, $highest );
176
+
177
+ return $r;
178
+ }
179
+
180
+ /**
181
+ * Append values to the end of a chunked storage key.
182
+ *
183
+ * @param string $key
184
+ * @param array $value
185
+ *
186
+ * @return bool
187
+ */
188
+ public function append( $key, $value ) {
189
+
190
+ if ( ! isset( $this->config[ $key ] ) ) {
191
+ _doing_it_wrong( __METHOD__, "iThemes Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
192
+
193
+ return false;
194
+ }
195
+
196
+ $config = $this->config[ $key ];
197
+
198
+ if ( ! $config['chunk'] ) {
199
+ _doing_it_wrong( __METHOD__, "iThemes Security: Cannot append to non-chunked key '{$key}' for '{$this->name}' storage.", '4.5.0' );
200
+
201
+ return false;
202
+ }
203
+
204
+ if ( array_key_exists( $key, $this->data ) ) {
205
+ $this->data[ $key ] = array_merge( $this->data[ $key ], $value );
206
+ }
207
+
208
+ global $wpdb;
209
+
210
+ $last_chunk = $wpdb->get_results( $wpdb->prepare(
211
+ "SELECT `storage_chunk`, `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s ORDER BY `storage_chunk` DESC LIMIT 1",
212
+ $this->name, $key
213
+ ) );
214
+
215
+ if ( empty( $last_chunk ) ) {
216
+ return $this->update_row( call_user_func( $config['serialize'], $value ), $key );
217
+ }
218
+
219
+ $last_chunk_num = $last_chunk[0]->storage_chunk;
220
+ $last_chunk_data = call_user_func( $config['unserialize'], $last_chunk[0]->storage_data );
221
+
222
+ if ( count( $last_chunk_data ) === $config['chunk'] ) {
223
+ return $this->update_row( call_user_func( $config['serialize'], $value ), $key, $last_chunk_num + 1 );
224
+ }
225
+
226
+ $to_fill = $config['chunk'] - count( $last_chunk_data );
227
+
228
+ $append = array_slice( $value, 0, $to_fill, true );
229
+ $merged = array_merge( $last_chunk_data, $append );
230
+
231
+ if ( ! $this->update_row( call_user_func( $config['serialize'], $merged ), $key, $last_chunk_num ) ) {
232
+ return false;
233
+ }
234
+
235
+ if ( ! $new = array_slice( $value, $to_fill, null, true ) ) {
236
+ return true;
237
+ }
238
+
239
+ $r = true;
240
+
241
+ foreach ( array_chunk( $new, $config['chunk'], true ) as $i => $chunk ) {
242
+ $r_ = $this->update_row( call_user_func( $config['serialize'], $chunk ), $key, $last_chunk_num + 1 + $i );
243
+ $r = $r && $r_;
244
+ }
245
+
246
+ return $r;
247
+ }
248
+
249
+ /**
250
+ * Update a chunked option from an iterator.
251
+ *
252
+ * This will be more performant than using ::set() and iterator_to_array() as the whole
253
+ * array won't be loaded into memory. Instead, it will continuously iterate over the values
254
+ * and persist the data to the database whenever it hits the chunk size.
255
+ *
256
+ * @param string $key
257
+ * @param iterable $iterator
258
+ *
259
+ * @return bool
260
+ */
261
+ public function set_from_iterator( $key, $iterator ) {
262
+ if ( ! isset( $this->config[ $key ] ) ) {
263
+ _doing_it_wrong( __METHOD__, "iThemes Security: Unsupported key '{$key}' for '{$this->name}' storage.", '4.5.0' );
264
+
265
+ return false;
266
+ }
267
+
268
+ $config = $this->config[ $key ];
269
+
270
+ if ( ! $config['chunk'] ) {
271
+ _doing_it_wrong( __METHOD__, "iThemes Security: Cannot set from iterator to non-chunked key '{$key}' for '{$this->name}' storage.", '4.5.0' );
272
+
273
+ return false;
274
+ }
275
+
276
+ unset( $this->data[ $key ] );
277
+
278
+ $i = 0;
279
+ $chunk = 0;
280
+ $chunked = array();
281
+
282
+ $r = true;
283
+
284
+ foreach ( $iterator as $item => $value ) {
285
+ $i ++;
286
+
287
+ $chunked[ $item ] = $value;
288
+
289
+ if ( $i === $config['chunk'] ) {
290
+ $r_ = $this->update_row( call_user_func( $config['serialize'], $chunked ), $key, $chunk );
291
+ $r = $r && $r_;
292
+ $chunked = array();
293
+ $chunk ++;
294
+ $i = 0;
295
+ }
296
+ }
297
+
298
+ if ( $chunked ) {
299
+ $this->update_row( call_user_func( $config['serialize'], $chunked ), $key, $chunk );
300
+ } else {
301
+ // The last chunk allocated was not used.
302
+ $chunk --;
303
+ }
304
+
305
+ $this->clean_chunk_options( $key, $chunk );
306
+
307
+ return $r;
308
+ }
309
+
310
+ /**
311
+ * Get the most recent time any key in this storage set has been updated.
312
+ *
313
+ * @return int|false
314
+ */
315
+ public function health_check() {
316
+
317
+ global $wpdb;
318
+
319
+ $date = $wpdb->get_var( $wpdb->prepare(
320
+ "SELECT `storage_updated` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s ORDER BY `storage_updated` DESC LIMIT 1",
321
+ $this->name
322
+ ) );
323
+
324
+ if ( $date ) {
325
+ return strtotime( $date );
326
+ }
327
+
328
+ return false;
329
+ }
330
+
331
+ /**
332
+ * Clear the entire storage bucket.
333
+ *
334
+ * @return bool
335
+ */
336
+ public function clear() {
337
+ if ( self::clear_group( $this->name ) ) {
338
+ $this->data = array();
339
+
340
+ return true;
341
+ }
342
+
343
+ return false;
344
+ }
345
+
346
+ /**
347
+ * check if there are any recorded values in storage.
348
+ *
349
+ * @return bool
350
+ */
351
+ public function is_empty() {
352
+ global $wpdb;
353
+
354
+ return ! $wpdb->get_var( $wpdb->prepare(
355
+ "SELECT `storage_id` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s LIMIT 1",
356
+ $this->name
357
+ ) );
358
+ }
359
+
360
+ /**
361
+ * Perform an insert or update to the distributed storage data.
362
+ *
363
+ * @param string $serialized
364
+ * @param string $key
365
+ * @param int $chunk
366
+ *
367
+ * @return bool
368
+ */
369
+ private function update_row( $serialized, $key = '', $chunk = 0 ) {
370
+
371
+ global $wpdb;
372
+
373
+ $wpdb->query( $wpdb->prepare(
374
+ "INSERT INTO {$wpdb->base_prefix}itsec_distributed_storage (`storage_group`, `storage_key`, `storage_chunk`, `storage_data`, `storage_updated`) VALUES (%s, %s, %d, %s, %s) " .
375
+ 'ON DUPLICATE KEY UPDATE `storage_group` = %s, `storage_key` = %s, `storage_chunk` = %d, `storage_data` = %s, `storage_updated` = %s',
376
+ $this->name, $key, $chunk, $serialized, date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
377
+ $this->name, $key, $chunk, $serialized, date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() )
378
+ ) );
379
+
380
+ return $wpdb->last_error ? false : true;
381
+ }
382
+
383
+ /**
384
+ * Remove unused chunks.
385
+ *
386
+ * @param string $key The chunked key to clean.
387
+ * @param int $after_chunk Delete all rows with a chunk value higher than this.
388
+ */
389
+ private function clean_chunk_options( $key, $after_chunk ) {
390
+
391
+ global $wpdb;
392
+
393
+ $wpdb->query( $wpdb->prepare(
394
+ "DELETE FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s AND `storage_chunk` > %d",
395
+ $this->name, $key, $after_chunk
396
+ ) );
397
+ }
398
+
399
+ /**
400
+ * Load the values into memory for a given key.
401
+ *
402
+ * @param string $key
403
+ */
404
+ private function load( $key ) {
405
+
406
+ $config = $this->config[ $key ];
407
+
408
+ if ( $config['split'] ) {
409
+ $this->load_split_option( $key );
410
+
411
+ return;
412
+ }
413
+
414
+ global $wpdb;
415
+
416
+ $option = $wpdb->get_var( $wpdb->prepare(
417
+ "SELECT `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s",
418
+ $this->name, ''
419
+ ) );
420
+
421
+ if ( is_serialized( $option ) ) {
422
+ $option = unserialize( $option );
423
+ } else {
424
+ $option = array();
425
+ }
426
+
427
+ foreach ( $this->config as $config_key => $config_value ) {
428
+ if ( ! $config_value['split'] ) {
429
+ if ( is_array( $option ) && array_key_exists( $config_key, $option ) ) {
430
+ $this->data[ $config_key ] = $option[ $config_key ];
431
+ } elseif ( ! array_key_exists( $config_key, $this->data ) ) {
432
+ $this->data[ $config_key ] = $config_value['default'];
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Load a split option into memory.
440
+ *
441
+ * Will automatically iterate all chunks into memory as well.
442
+ *
443
+ * @param string $key
444
+ * @param int $chunk
445
+ */
446
+ private function load_split_option( $key, $chunk = 0 ) {
447
+
448
+ global $wpdb;
449
+
450
+ $config = $this->config[ $key ];
451
+
452
+ if ( $chunk ) {
453
+ $option = $wpdb->get_var( $wpdb->prepare(
454
+ "SELECT `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s AND `storage_chunk` = %d",
455
+ $this->name, $key, $chunk
456
+ ) );
457
+ } else {
458
+ $option = $wpdb->get_var( $wpdb->prepare(
459
+ "SELECT `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s",
460
+ $this->name, $key
461
+ ) );
462
+ }
463
+
464
+ if ( null === $option ) {
465
+ if ( ! array_key_exists( $key, $this->data ) ) {
466
+ $this->data[ $key ] = $config['default'];
467
+ }
468
+
469
+ return;
470
+ }
471
+
472
+ $option = call_user_func( $config['unserialize'], $option );
473
+
474
+ if ( ! $config['chunk'] ) {
475
+ $this->data[ $key ] = $option;
476
+
477
+ return;
478
+ }
479
+
480
+ if ( ! is_array( $option ) ) {
481
+ trigger_error( "iThemes Security: Non-array value encountered for chunked key '{$key}' in storage '{$this->name}'." );
482
+
483
+ return;
484
+ }
485
+
486
+ if ( array_key_exists( $key, $this->data ) ) {
487
+ $this->data[ $key ] = array_merge( $this->data[ $key ], $option );
488
+ } else {
489
+ $this->data[ $key ] = $option;
490
+ }
491
+
492
+ // Greater than should never occur, bu to be safe
493
+ if ( count( $option ) >= $config['chunk'] ) {
494
+ $this->load_split_option( $key, $chunk + 1 );
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Load data for a specific chunk.
500
+ *
501
+ * Ideally this would be replaced with a closure passed to the storage cursor.
502
+ *
503
+ * @param string $key
504
+ * @param int $chunk
505
+ *
506
+ * @return mixed|null
507
+ */
508
+ public function _load_chunk( $key, $chunk ) {
509
+ global $wpdb;
510
+
511
+ $option = $wpdb->get_var( $wpdb->prepare(
512
+ "SELECT `storage_data` FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s AND `storage_key` = %s AND `storage_chunk` = %d",
513
+ $this->name, $key, $chunk
514
+ ) );
515
+
516
+ if ( null === $option ) {
517
+ return null;
518
+ }
519
+
520
+ $config = $this->config[ $key ];
521
+
522
+ return call_user_func( $config['unserialize'], $option );
523
+ }
524
+
525
+ /**
526
+ * Clear all the storage for a given group name.
527
+ *
528
+ * @param string $name
529
+ *
530
+ * @return bool
531
+ */
532
+ public static function clear_group( $name ) {
533
+
534
+ global $wpdb;
535
+
536
+ $wpdb->query( $wpdb->prepare(
537
+ "DELETE FROM {$wpdb->base_prefix}itsec_distributed_storage WHERE `storage_group` = %s",
538
+ $name
539
+ ) );
540
+
541
+ return $wpdb->last_error ? false : true;
542
+ }
543
+ }
544
+
545
+ class ITSEC_Lib_Distributed_Storage_Cursor implements Iterator {
546
+
547
+ /** @var ITSEC_Lib_Distributed_Storage */
548
+ private $storage;
549
+
550
+ /** @var string */
551
+ private $key;
552
+
553
+ /** @var int */
554
+ private $chunk = 0;
555
+
556
+ /** @var array */
557
+ private $data;
558
+
559
+ /** @var int */
560
+ private $iterated_count = 0;
561
+
562
+ /**
563
+ * ITSEC_Lib_Distributed_Storage_Cursor constructor.
564
+ *
565
+ * @param ITSEC_Lib_Distributed_Storage $storage
566
+ * @param string $key
567
+ * @param array $data
568
+ */
569
+ public function __construct( ITSEC_Lib_Distributed_Storage $storage, $key, array $data ) {
570
+ $this->storage = $storage;
571
+ $this->key = $key;
572
+ $this->data = $data;
573
+ }
574
+
575
+ /**
576
+ * @inheritDoc
577
+ */
578
+ public function current() {
579
+ return current( $this->data );
580
+ }
581
+
582
+ /**
583
+ * @inheritDoc
584
+ */
585
+ public function next() {
586
+
587
+ if ( $this->iterated_count === count( $this->data ) - 1 ) {
588
+ $data = $this->storage->_load_chunk( $this->key, $this->chunk + 1 );
589
+
590
+ if ( null !== $data ) {
591
+ $this->data = $data;
592
+ $this->iterated_count = 0;
593
+ $this->chunk ++;
594
+
595
+ return;
596
+ }
597
+ }
598
+
599
+ $this->iterated_count ++;
600
+ next( $this->data );
601
+ }
602
+
603
+ /**
604
+ * @inheritDoc
605
+ */
606
+ public function key() {
607
+ return key( $this->data );
608
+ }
609
+
610
+ /**
611
+ * @inheritDoc
612
+ */
613
+ public function valid() {
614
+ return $this->iterated_count < count( $this->data );
615
+ }
616
+
617
+ /**
618
+ * @inheritDoc
619
+ */
620
+ public function rewind() {
621
+
622
+ $this->iterated_count = 0;
623
+
624
+ if ( 0 === $this->chunk ) {
625
+ reset( $this->data );
626
+ } else {
627
+ $data = $this->storage->_load_chunk( $this->key, 0 );
628
+ $this->data = null === $data ? array() : $data;
629
+ $this->chunk = 0;
630
+ }
631
+ }
632
+ }
core/lib/class-itsec-lib-file.php CHANGED
@@ -28,24 +28,24 @@ class ITSEC_Lib_File {
28
  if ( ! self::is_file( $file ) ) {
29
  return new WP_Error( 'itsec-lib-file-read-non-file', sprintf( __( '%s could not be read. It does not appear to be a file.', 'better-wp-security' ), $file ) );
30
  }
31
-
32
-
33
  $callable = array();
34
-
35
  if ( ITSEC_Lib_Utility::is_callable_function( 'file_get_contents' ) ) {
36
  $callable[] = 'file_get_contents';
37
  }
38
  if ( ITSEC_Lib_Utility::is_callable_function( 'fopen' ) && ITSEC_Lib_Utility::is_callable_function( 'feof' ) && ITSEC_Lib_Utility::is_callable_function( 'fread' ) && ITSEC_Lib_Utility::is_callable_function( 'flock' ) ) {
39
  $callable[] = 'fopen';
40
  }
41
-
42
  if ( empty( $callable ) ) {
43
  return new WP_Error( 'itsec-lib-file-read-no-callable-functions', sprintf( __( '%s could not be read. Both the fopen/feof/fread/flock and file_get_contents functions are disabled on the server.', 'better-wp-security' ), $file ) );
44
  }
45
-
46
-
47
  $contents = false;
48
-
49
  // Different permissions to try in case the starting set of permissions are prohibiting read.
50
  $trial_perms = array(
51
  false,
@@ -53,54 +53,54 @@ class ITSEC_Lib_File {
53
  0664,
54
  0666,
55
  );
56
-
57
-
58
  foreach ( $trial_perms as $perms ) {
59
  if ( false !== $perms ) {
60
  if ( ! isset( $original_file_perms ) ) {
61
  $original_file_perms = self::get_permissions( $file );
62
  }
63
-
64
  self::chmod( $file, $perms );
65
  }
66
-
67
-
68
-
69
-
70
  if ( in_array( 'fopen', $callable ) ) {
71
  if ( false !== ( $fh = fopen( $file, 'rb' ) ) ) {
72
  flock( $fh, LOCK_SH );
73
-
74
  $contents = '';
75
-
76
  while ( ! feof( $fh ) ) {
77
  $contents .= fread( $fh, 1024 );
78
  }
79
-
80
  flock( $fh, LOCK_UN );
81
  fclose( $fh );
82
  }
83
  }
84
-
85
  if ( ( false === $contents ) && in_array( 'file_get_contents', $callable ) ) {
86
  $contents = file_get_contents( $file );
87
  }
88
-
89
-
90
  if ( false !== $contents ) {
91
  if ( isset( $original_file_perms ) && is_int( $original_file_perms ) ) {
92
  // Reset the original file permissions if they were modified.
93
  self::chmod( $file, $original_file_perms );
94
  }
95
-
96
  return $contents;
97
  }
98
  }
99
-
100
-
101
  return new WP_Error( 'itsec-lib-file-read-cannot-read', sprintf( __( '%s could not be read due to an unknown error.', 'better-wp-security' ), $file ) );
102
  }
103
-
104
  /**
105
  * Update or append the requested file with the supplied contents.
106
  *
@@ -113,35 +113,35 @@ class ITSEC_Lib_File {
113
  */
114
  public static function write( $file, $contents, $append = false ) {
115
  $callable = array();
116
-
117
  if ( ITSEC_Lib_Utility::is_callable_function( 'fopen' ) && ITSEC_Lib_Utility::is_callable_function( 'fwrite' ) && ITSEC_Lib_Utility::is_callable_function( 'flock' ) ) {
118
  $callable[] = 'fopen';
119
  }
120
  if ( ITSEC_Lib_Utility::is_callable_function( 'file_put_contents' ) ) {
121
  $callable[] = 'file_put_contents';
122
  }
123
-
124
  if ( empty( $callable ) ) {
125
  return new WP_Error( 'itsec-lib-file-write-no-callable-functions', sprintf( __( '%s could not be written. Both the fopen/fwrite/flock and file_put_contents functions are disabled on the server. This is a server configuration issue that must be resolved before iThemes Security can write files.', 'better-wp-security' ), $file ) );
126
  }
127
-
128
-
129
  if ( ITSEC_Lib_Directory::is_dir( $file ) ) {
130
  return new WP_Error( 'itsec-lib-file-write-path-exists-as-directory', sprintf( __( '%s could not be written as a file. The requested path already exists as a directory. The directory must be removed or a new file name must be chosen before the file can be written.', 'better-wp-security' ), $file ) );
131
  }
132
-
133
  if ( ! ITSEC_Lib_Directory::is_dir( dirname( $file ) ) ) {
134
  $result = ITSEC_Lib_Directory::create( dirname( $file ) );
135
-
136
  if ( is_wp_error( $result ) ) {
137
  return $result;
138
  }
139
  }
140
-
141
-
142
  $file_existed = self::is_file( $file );
143
  $success = false;
144
-
145
  // Different permissions to try in case the starting set of permissions are prohibiting write.
146
  $trial_perms = array(
147
  false,
@@ -149,62 +149,62 @@ class ITSEC_Lib_File {
149
  0664,
150
  0666,
151
  );
152
-
153
-
154
  foreach ( $trial_perms as $perms ) {
155
  if ( false !== $perms ) {
156
  if ( ! isset( $original_file_perms ) ) {
157
  $original_file_perms = self::get_permissions( $file );
158
  }
159
-
160
  self::chmod( $file, $perms );
161
  }
162
-
163
  if ( in_array( 'fopen', $callable ) ) {
164
  if ( $append ) {
165
  $mode = 'ab';
166
  } else {
167
  $mode = 'wb';
168
  }
169
-
170
  if ( false !== ( $fh = @fopen( $file, $mode ) ) ) {
171
  flock( $fh, LOCK_EX );
172
-
173
  mbstring_binary_safe_encoding();
174
-
175
  $data_length = strlen( $contents );
176
  $bytes_written = @fwrite( $fh, $contents );
177
-
178
  reset_mbstring_encoding();
179
-
180
  @flock( $fh, LOCK_UN );
181
  @fclose( $fh );
182
-
183
  if ( $data_length === $bytes_written ) {
184
  $success = true;
185
  }
186
  }
187
  }
188
-
189
  if ( ! $success && in_array( 'file_put_contents', $callable ) ) {
190
  if ( $append ) {
191
  $flags = FILE_APPEND;
192
  } else {
193
  $flags = 0;
194
  }
195
-
196
  mbstring_binary_safe_encoding();
197
-
198
  $data_length = strlen( $contents );
199
  $bytes_written = @file_put_contents( $file, $contents, $flags );
200
-
201
  reset_mbstring_encoding();
202
-
203
  if ( $data_length === $bytes_written ) {
204
  $success = true;
205
  }
206
  }
207
-
208
  if ( $success ) {
209
  if ( ! $file_existed ) {
210
  // Set default file permissions for the new file.
@@ -213,20 +213,28 @@ class ITSEC_Lib_File {
213
  // Reset the original file permissions if they were modified.
214
  self::chmod( $file, $original_file_perms );
215
  }
216
-
 
 
 
 
 
 
 
 
217
  return true;
218
  }
219
-
220
  if ( ! $file_existed ) {
221
  // If the file is new, there is no point attempting different permissions.
222
  break;
223
  }
224
  }
225
-
226
-
227
  return new WP_Error( 'itsec-lib-file-write-file-put-contents-failed', sprintf( __( '%s could not be written. This could be due to a permissions issue. Ensure that PHP runs as a user that has permission to write to this location.', 'better-wp-security' ), $file ) );
228
  }
229
-
230
  /**
231
  * Create or append the requested file with the supplied contents.
232
  *
@@ -239,7 +247,7 @@ class ITSEC_Lib_File {
239
  public static function append( $file, $contents ) {
240
  return self::write( $file, $contents, true );
241
  }
242
-
243
  /**
244
  * Remove the supplied file.
245
  *
@@ -251,22 +259,31 @@ class ITSEC_Lib_File {
251
  if ( ! self::exists( $file ) ) {
252
  return true;
253
  }
254
-
255
  if ( ! ITSEC_Lib_Utility::is_callable_function( 'unlink' ) ) {
256
  return new WP_Error( 'itsec-lib-file-remove-unlink-is-disabled', sprintf( __( 'The file %s could not be removed as the unlink() function is disabled. This is a system configuration issue.', 'better-wp-security' ), $file ) );
257
  }
258
-
259
-
260
  $result = @unlink( $file );
 
261
  @clearstatcache( true, $file );
262
-
263
  if ( $result ) {
 
 
 
 
 
 
 
 
264
  return true;
265
  }
266
-
267
  return new WP_Error( 'itsec-lib-file-remove-unknown-error', sprintf( __( 'Unable to remove %s due to an unknown error.', 'better-wp-security' ), $file ) );
268
  }
269
-
270
  /**
271
  * Change file permissions.
272
  *
@@ -281,14 +298,14 @@ class ITSEC_Lib_File {
281
  if ( ! is_int( $perms ) ) {
282
  return new WP_Error( 'itsec-lib-file-chmod-invalid-perms', sprintf( __( 'The file %1$s could not have its permissions updated as non-integer permissions were sent: (%2$s) %3$s', 'better-wp-security' ), $file, gettype( $perms ), $perms ) );
283
  }
284
-
285
  if ( ! ITSEC_Lib_Utility::is_callable_function( 'chmod' ) ) {
286
  return new WP_Error( 'itsec-lib-file-chmod-chmod-is-disabled', sprintf( __( 'The file %s could not have its permissions updated as the chmod() function is disabled. This is a system configuration issue.', 'better-wp-security' ), $file ) );
287
  }
288
-
289
  return @chmod( $file, $perms );
290
  }
291
-
292
  /**
293
  * Determine if a file or directory exists.
294
  *
@@ -298,11 +315,12 @@ class ITSEC_Lib_File {
298
  * @return bool|WP_Error Boolean true if it exists, false if it does not.
299
  */
300
  public static function exists( $file ) {
 
301
  @clearstatcache( true, $file );
302
-
303
  return @file_exists( $file );
304
  }
305
-
306
  /**
307
  * Determine if a file exists.
308
  *
@@ -312,11 +330,12 @@ class ITSEC_Lib_File {
312
  * @return bool|WP_Error Boolean true if it exists, false if it does not.
313
  */
314
  public static function is_file( $file ) {
 
315
  @clearstatcache( true, $file );
316
-
317
  return @is_file( $file );
318
  }
319
-
320
  /**
321
  * Get file permissions from the requested file.
322
  *
@@ -329,17 +348,18 @@ class ITSEC_Lib_File {
329
  if ( ! self::is_file( $file ) ) {
330
  return new WP_Error( 'itsec-lib-file-get-permissions-missing-file', sprintf( __( 'Permissions for the file %s could not be read as the file could not be found.', 'better-wp-security' ), $file ) );
331
  }
332
-
333
  if ( ! ITSEC_Lib_Utility::is_callable_function( 'fileperms' ) ) {
334
  return new WP_Error( 'itsec-lib-file-get-permissions-fileperms-is-disabled', sprintf( __( 'Permissions for the file %s could not be read as the fileperms() function is disabled. This is a system configuration issue.', 'better-wp-security' ), $file ) );
335
  }
336
-
337
-
 
338
  @clearstatcache( true, $file );
339
-
340
  return fileperms( $file ) & 0777;
341
  }
342
-
343
  /**
344
  * Get default file permissions to use for new files.
345
  *
@@ -352,13 +372,13 @@ class ITSEC_Lib_File {
352
  if ( defined( 'FS_CHMOD_FILE' ) ) {
353
  return FS_CHMOD_FILE;
354
  }
355
-
356
  $perms = self::get_permissions( ABSPATH . 'index.php' );
357
-
358
  if ( ! is_wp_error( $perms ) ) {
359
  return $perms;
360
  }
361
-
362
  return 0644;
363
  }
364
  }
28
  if ( ! self::is_file( $file ) ) {
29
  return new WP_Error( 'itsec-lib-file-read-non-file', sprintf( __( '%s could not be read. It does not appear to be a file.', 'better-wp-security' ), $file ) );
30
  }
31
+
32
+
33
  $callable = array();
34
+
35
  if ( ITSEC_Lib_Utility::is_callable_function( 'file_get_contents' ) ) {
36
  $callable[] = 'file_get_contents';
37
  }
38
  if ( ITSEC_Lib_Utility::is_callable_function( 'fopen' ) && ITSEC_Lib_Utility::is_callable_function( 'feof' ) && ITSEC_Lib_Utility::is_callable_function( 'fread' ) && ITSEC_Lib_Utility::is_callable_function( 'flock' ) ) {
39
  $callable[] = 'fopen';
40
  }
41
+
42
  if ( empty( $callable ) ) {
43
  return new WP_Error( 'itsec-lib-file-read-no-callable-functions', sprintf( __( '%s could not be read. Both the fopen/feof/fread/flock and file_get_contents functions are disabled on the server.', 'better-wp-security' ), $file ) );
44
  }
45
+
46
+
47
  $contents = false;
48
+
49
  // Different permissions to try in case the starting set of permissions are prohibiting read.
50
  $trial_perms = array(
51
  false,
53
  0664,
54
  0666,
55
  );
56
+
57
+
58
  foreach ( $trial_perms as $perms ) {
59
  if ( false !== $perms ) {
60
  if ( ! isset( $original_file_perms ) ) {
61
  $original_file_perms = self::get_permissions( $file );
62
  }
63
+
64
  self::chmod( $file, $perms );
65
  }
66
+
67
+
68
+
69
+
70
  if ( in_array( 'fopen', $callable ) ) {
71
  if ( false !== ( $fh = fopen( $file, 'rb' ) ) ) {
72
  flock( $fh, LOCK_SH );
73
+
74
  $contents = '';
75
+
76
  while ( ! feof( $fh ) ) {
77
  $contents .= fread( $fh, 1024 );
78
  }
79
+
80
  flock( $fh, LOCK_UN );
81
  fclose( $fh );
82
  }
83
  }
84
+
85
  if ( ( false === $contents ) && in_array( 'file_get_contents', $callable ) ) {
86
  $contents = file_get_contents( $file );
87
  }
88
+
89
+
90
  if ( false !== $contents ) {
91
  if ( isset( $original_file_perms ) && is_int( $original_file_perms ) ) {
92
  // Reset the original file permissions if they were modified.
93
  self::chmod( $file, $original_file_perms );
94
  }
95
+
96
  return $contents;
97
  }
98
  }
99
+
100
+
101
  return new WP_Error( 'itsec-lib-file-read-cannot-read', sprintf( __( '%s could not be read due to an unknown error.', 'better-wp-security' ), $file ) );
102
  }
103
+
104
  /**
105
  * Update or append the requested file with the supplied contents.
106
  *
113
  */
114
  public static function write( $file, $contents, $append = false ) {
115
  $callable = array();
116
+
117
  if ( ITSEC_Lib_Utility::is_callable_function( 'fopen' ) && ITSEC_Lib_Utility::is_callable_function( 'fwrite' ) && ITSEC_Lib_Utility::is_callable_function( 'flock' ) ) {
118
  $callable[] = 'fopen';
119
  }
120
  if ( ITSEC_Lib_Utility::is_callable_function( 'file_put_contents' ) ) {
121
  $callable[] = 'file_put_contents';
122
  }
123
+
124
  if ( empty( $callable ) ) {
125
  return new WP_Error( 'itsec-lib-file-write-no-callable-functions', sprintf( __( '%s could not be written. Both the fopen/fwrite/flock and file_put_contents functions are disabled on the server. This is a server configuration issue that must be resolved before iThemes Security can write files.', 'better-wp-security' ), $file ) );
126
  }
127
+
128
+
129
  if ( ITSEC_Lib_Directory::is_dir( $file ) ) {
130
  return new WP_Error( 'itsec-lib-file-write-path-exists-as-directory', sprintf( __( '%s could not be written as a file. The requested path already exists as a directory. The directory must be removed or a new file name must be chosen before the file can be written.', 'better-wp-security' ), $file ) );
131
  }
132
+
133
  if ( ! ITSEC_Lib_Directory::is_dir( dirname( $file ) ) ) {
134
  $result = ITSEC_Lib_Directory::create( dirname( $file ) );
135
+
136
  if ( is_wp_error( $result ) ) {
137
  return $result;
138
  }
139
  }
140
+
141
+
142
  $file_existed = self::is_file( $file );
143
  $success = false;
144
+
145
  // Different permissions to try in case the starting set of permissions are prohibiting write.
146
  $trial_perms = array(
147
  false,
149
  0664,
150
  0666,
151
  );
152
+
153
+
154
  foreach ( $trial_perms as $perms ) {
155
  if ( false !== $perms ) {
156
  if ( ! isset( $original_file_perms ) ) {
157
  $original_file_perms = self::get_permissions( $file );
158
  }
159
+
160
  self::chmod( $file, $perms );
161
  }
162
+
163
  if ( in_array( 'fopen', $callable ) ) {
164
  if ( $append ) {
165
  $mode = 'ab';
166
  } else {
167
  $mode = 'wb';
168
  }
169
+
170
  if ( false !== ( $fh = @fopen( $file, $mode ) ) ) {
171
  flock( $fh, LOCK_EX );
172
+
173
  mbstring_binary_safe_encoding();
174
+
175
  $data_length = strlen( $contents );
176
  $bytes_written = @fwrite( $fh, $contents );
177
+
178
  reset_mbstring_encoding();
179
+
180
  @flock( $fh, LOCK_UN );
181
  @fclose( $fh );
182
+
183
  if ( $data_length === $bytes_written ) {
184
  $success = true;
185
  }
186
  }
187
  }
188
+
189
  if ( ! $success && in_array( 'file_put_contents', $callable ) ) {
190
  if ( $append ) {
191
  $flags = FILE_APPEND;
192
  } else {
193
  $flags = 0;
194
  }
195
+
196
  mbstring_binary_safe_encoding();
197
+
198
  $data_length = strlen( $contents );
199
  $bytes_written = @file_put_contents( $file, $contents, $flags );
200
+
201
  reset_mbstring_encoding();
202
+
203
  if ( $data_length === $bytes_written ) {
204
  $success = true;
205
  }
206
  }
207
+
208
  if ( $success ) {
209
  if ( ! $file_existed ) {
210
  // Set default file permissions for the new file.
213
  // Reset the original file permissions if they were modified.
214
  self::chmod( $file, $original_file_perms );
215
  }
216
+
217
+ /**
218
+ * Fires when iThemes Security writes to a managed file.
219
+ *
220
+ * @param string $file The path to the file.
221
+ * @param string $contents The contents written.
222
+ */
223
+ do_action( 'itsec_lib_write_to_file', $file, $contents );
224
+
225
  return true;
226
  }
227
+
228
  if ( ! $file_existed ) {
229
  // If the file is new, there is no point attempting different permissions.
230
  break;
231
  }
232
  }
233
+
234
+
235
  return new WP_Error( 'itsec-lib-file-write-file-put-contents-failed', sprintf( __( '%s could not be written. This could be due to a permissions issue. Ensure that PHP runs as a user that has permission to write to this location.', 'better-wp-security' ), $file ) );
236
  }
237
+
238
  /**
239
  * Create or append the requested file with the supplied contents.
240
  *
247
  public static function append( $file, $contents ) {
248
  return self::write( $file, $contents, true );
249
  }
250
+
251
  /**
252
  * Remove the supplied file.
253
  *
259
  if ( ! self::exists( $file ) ) {
260
  return true;
261
  }
262
+
263
  if ( ! ITSEC_Lib_Utility::is_callable_function( 'unlink' ) ) {
264
  return new WP_Error( 'itsec-lib-file-remove-unlink-is-disabled', sprintf( __( 'The file %s could not be removed as the unlink() function is disabled. This is a system configuration issue.', 'better-wp-security' ), $file ) );
265
  }
266
+
267
+
268
  $result = @unlink( $file );
269
+ // phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
270
  @clearstatcache( true, $file );
271
+
272
  if ( $result ) {
273
+
274
+ /**
275
+ * Fires when iThemes Security removes a managed file.
276
+ *
277
+ * @param string $file
278
+ */
279
+ do_action( 'itsec_lib_delete_file', $file );
280
+
281
  return true;
282
  }
283
+
284
  return new WP_Error( 'itsec-lib-file-remove-unknown-error', sprintf( __( 'Unable to remove %s due to an unknown error.', 'better-wp-security' ), $file ) );
285
  }
286
+
287
  /**
288
  * Change file permissions.
289
  *
298
  if ( ! is_int( $perms ) ) {
299
  return new WP_Error( 'itsec-lib-file-chmod-invalid-perms', sprintf( __( 'The file %1$s could not have its permissions updated as non-integer permissions were sent: (%2$s) %3$s', 'better-wp-security' ), $file, gettype( $perms ), $perms ) );
300
  }
301
+
302
  if ( ! ITSEC_Lib_Utility::is_callable_function( 'chmod' ) ) {
303
  return new WP_Error( 'itsec-lib-file-chmod-chmod-is-disabled', sprintf( __( 'The file %s could not have its permissions updated as the chmod() function is disabled. This is a system configuration issue.', 'better-wp-security' ), $file ) );
304
  }
305
+
306
  return @chmod( $file, $perms );
307
  }
308
+
309
  /**
310
  * Determine if a file or directory exists.
311
  *
315
  * @return bool|WP_Error Boolean true if it exists, false if it does not.
316
  */
317
  public static function exists( $file ) {
318
+ // phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
319
  @clearstatcache( true, $file );
320
+
321
  return @file_exists( $file );
322
  }
323
+
324
  /**
325
  * Determine if a file exists.
326
  *
330
  * @return bool|WP_Error Boolean true if it exists, false if it does not.
331
  */
332
  public static function is_file( $file ) {
333
+ // phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
334
  @clearstatcache( true, $file );
335
+
336
  return @is_file( $file );
337
  }
338
+
339
  /**
340
  * Get file permissions from the requested file.
341
  *
348
  if ( ! self::is_file( $file ) ) {
349
  return new WP_Error( 'itsec-lib-file-get-permissions-missing-file', sprintf( __( 'Permissions for the file %s could not be read as the file could not be found.', 'better-wp-security' ), $file ) );
350
  }
351
+
352
  if ( ! ITSEC_Lib_Utility::is_callable_function( 'fileperms' ) ) {
353
  return new WP_Error( 'itsec-lib-file-get-permissions-fileperms-is-disabled', sprintf( __( 'Permissions for the file %s could not be read as the fileperms() function is disabled. This is a system configuration issue.', 'better-wp-security' ), $file ) );
354
  }
355
+
356
+
357
+ // phpcs:ignore -- Have Tide ignore the following line. We use arguments that don't exist in early versions, but these versions ignore the arguments.
358
  @clearstatcache( true, $file );
359
+
360
  return fileperms( $file ) & 0777;
361
  }
362
+
363
  /**
364
  * Get default file permissions to use for new files.
365
  *
372
  if ( defined( 'FS_CHMOD_FILE' ) ) {
373
  return FS_CHMOD_FILE;
374
  }
375
+
376
  $perms = self::get_permissions( ABSPATH . 'index.php' );
377
+
378
  if ( ! is_wp_error( $perms ) ) {
379
  return $perms;
380
  }
381
+
382
  return 0644;
383
  }
384
  }
core/lib/class-itsec-lib-login-interstitial.php ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Lib_Login_Interstitial
5
+ */
6
+ class ITSEC_Lib_Login_Interstitial {
7
+
8
+ const SHOW_AFTER_LOGIN = 'itsec_after_interstitial';
9
+ const META_KEY = '_itsec_login_interstitial_token';
10
+ const AJAX = 'itsec-login-interstitial-ajax';
11
+
12
+ private $registered = array();
13
+
14
+ /** @var WP_Error */
15
+ private $error;
16
+
17
+ /** @var string */
18
+ private $session_token;
19
+
20
+ /**
21
+ * Initialize the module.
22
+ *
23
+ * This registers hooks and filters.
24
+ */
25
+ public function run() {
26
+
27
+ /**
28
+ * Fires when the Login Interstitial framework is initialized.
29
+ *
30
+ * @param ITSEC_Lib_Login_Interstitial
31
+ */
32
+ do_action( 'itsec_login_interstitial_init', $this );
33
+
34
+ if ( ! $this->registered ) {
35
+ return;
36
+ }
37
+
38
+ $this->registered = wp_list_sort( $this->registered, 'priority', 'ASC', true );
39
+
40
+ add_action( 'wp_login', array( $this, 'wp_login' ), 10, 2 );
41
+ add_action( 'wp_login_errors', array( $this, 'handle_token_expired' ) );
42
+ add_action( 'login_init', array( $this, 'force_interstitial' ) );
43
+ add_action( 'login_form', array( $this, 'ferry_after_login' ) );
44
+ add_filter( 'auth_cookie', array( $this, 'capture_session_token' ), 10, 5 );
45
+
46
+ $added_ajax = false;
47
+
48
+ foreach ( $this->registered as $id => $opts ) {
49
+ if ( ! empty( $opts['submit'] ) ) {
50
+ add_action( "login_form_itsec-{$id}", array( $this, 'submit' ), 9 );
51
+ }
52
+
53
+ add_action( "login_form_itsec-{$id}", array( $this, 'display' ) );
54
+
55
+ if ( ! $added_ajax && ! empty( $opts['ajax_handler'] ) ) {
56
+ add_action( 'wp_ajax_' . self::AJAX, array( $this, 'ajax_handler' ) );
57
+ add_action( 'wp_ajax_nopriv_' . self::AJAX, array( $this, 'ajax_handler' ) );
58
+
59
+ $added_ajax = true;
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Register an interstitial.
66
+ *
67
+ * @api
68
+ *
69
+ * @param string $action
70
+ * @param callable $render
71
+ * @param array $opts
72
+ *
73
+ * @return bool
74
+ */
75
+ public function register( $action, $render, $opts ) {
76
+ $opts = wp_parse_args( $opts, array(
77
+ 'force_completion' => true, // Will logout the user's session before displaying the interstitial.
78
+ 'show_to_user' => true, // Boolean or callable.
79
+ 'wp_login_only' => false, // Only show the interstitial if the login form is submitted from wp-login.php,
80
+ 'submit' => false, // Callable called with user when submitting the form.
81
+ 'info_message' => false,
82
+ 'after_submit' => false,
83
+ 'ajax_handler' => false,
84
+ 'priority' => 5,
85
+ ) );
86
+
87
+ $opts['render'] = $render;
88
+
89
+ if ( ! is_bool( $opts['show_to_user'] ) && ! is_callable( $opts['show_to_user'] ) ) {
90
+ return false;
91
+ }
92
+
93
+ if ( ! is_bool( $opts['force_completion'] ) && ! is_callable( $opts['force_completion'] ) ) {
94
+ return false;
95
+ }
96
+
97
+ $this->registered[ $action ] = $opts;
98
+
99
+ return true;
100
+ }
101
+
102
+ /**
103
+ * Show the interstitial.
104
+ *
105
+ * @api
106
+ *
107
+ * @param string $action
108
+ * @param WP_User $user
109
+ *
110
+ * @return void
111
+ */
112
+ public function show_interstitial( $action, $user ) {
113
+
114
+ if ( ! isset( $this->registered[ $action ] ) ) {
115
+ return;
116
+ }
117
+
118
+ $opts = $this->registered[ $action ];
119
+
120
+ if ( $this->result( $opts['force_completion'], array( $user ) ) ) {
121
+ $this->destroy_session( $user );
122
+ $token = $this->set_token( $user );
123
+ } else {
124
+ $token = null;
125
+ }
126
+
127
+ $this->login_html( $user, $action, $token );
128
+ die;
129
+ }
130
+
131
+ /**
132
+ * During the login process, check if we have any interstitials to display, and display them.
133
+ *
134
+ * @param string $username
135
+ * @param WP_User $user
136
+ */
137
+ public function wp_login( $username, $user = null ) {
138
+ $user = $user ? $user : wp_get_current_user();
139
+
140
+ if ( ! $user || ! $user->exists() ) {
141
+ return;
142
+ }
143
+
144
+ if ( isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) ) {
145
+
146
+ $action = $_REQUEST[ self::SHOW_AFTER_LOGIN ];
147
+
148
+ if ( isset( $this->registered[ $action ] ) && $this->is_interstitial_applicable( $action, $user ) ) {
149
+ $this->show_interstitial( $action, $user );
150
+ }
151
+ }
152
+
153
+ foreach ( $this->get_applicable_interstitials( $user ) as $action => $opts ) {
154
+ $this->show_interstitial( $action, $user );
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Add a message that the interstitial expired.
160
+ *
161
+ * @param WP_Error $errors
162
+ *
163
+ * @return WP_Error
164
+ */
165
+ public function handle_token_expired( $errors ) {
166
+
167
+ if ( ! empty( $_GET['itsec_interstitial_expired'] ) ) {
168
+ $errors->add(
169
+ 'itsec-login-interstitial-invalid-token',
170
+ esc_html__( 'Sorry, this request has expired. Please log in again.', 'better-wp-security' )
171
+ );
172
+ }
173
+
174
+ return $errors;
175
+ }
176
+
177
+ /**
178
+ * Force the requested interstitial to be displayed if the user is already logged-in.
179
+ */
180
+ public function force_interstitial() {
181
+
182
+ if ( empty( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) || ! isset( $this->registered[ $_REQUEST[ self::SHOW_AFTER_LOGIN ] ] ) ) {
183
+ return;
184
+ }
185
+
186
+ $action = $_REQUEST[ self::SHOW_AFTER_LOGIN ];
187
+
188
+ if ( 'POST' === $_SERVER['REQUEST_METHOD'] && ! empty( $_POST['action'] ) && "itsec-{$action}" === $_POST['action'] ) {
189
+ return;
190
+ }
191
+
192
+ if ( ! is_user_logged_in() ) {
193
+ return;
194
+ }
195
+
196
+ $user = wp_get_current_user();
197
+
198
+ if ( ! $this->result( $this->registered[ $action ]['show_to_user'], array( $user, true ) ) ) {
199
+ wp_safe_redirect( admin_url() );
200
+ die;
201
+ }
202
+
203
+ $this->show_interstitial( $action, $user );
204
+ }
205
+
206
+ /**
207
+ * Ferry the after login interstitial query var into the form.
208
+ */
209
+ public function ferry_after_login() {
210
+ if ( ! empty( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) && isset( $this->registered[ $_REQUEST[ self::SHOW_AFTER_LOGIN ] ] ) ) {
211
+ echo '<input type="hidden" name="' . esc_attr( self::SHOW_AFTER_LOGIN ) . '" value="' . esc_attr( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) . '">';
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Capture the session token to log out the user.
217
+ *
218
+ * @param string $cookie
219
+ * @param int $user_id
220
+ * @param int $expiration
221
+ * @param string $scheme
222
+ * @param string $token
223
+ *
224
+ * @return string
225
+ */
226
+ public function capture_session_token( $cookie, $user_id, $expiration, $scheme, $token ) {
227
+ $this->session_token = $token;
228
+
229
+ return $cookie;
230
+ }
231
+
232
+ /**
233
+ * Handle submitting the interstitial form.
234
+ */
235
+ public function submit() {
236
+ $action = substr( current_action(), strlen( 'login_form_itsec-' ) );
237
+
238
+ if ( empty( $this->registered[ $action ]['submit'] ) ) {
239
+ return;
240
+ }
241
+
242
+ if ( 'POST' !== $_SERVER['REQUEST_METHOD'] || empty( $_POST['action'] ) || "itsec-{$action}" !== $_POST['action'] ) {
243
+ return;
244
+ }
245
+
246
+ $opts = $this->registered[ $action ];
247
+
248
+ if ( ( ! $user = $this->get_user( $action ) ) || ! $this->result( $opts['show_to_user'], array( $user, isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) ) ) ) {
249
+ wp_safe_redirect( set_url_scheme( wp_login_url(), 'login_post' ) );
250
+ die;
251
+ }
252
+
253
+ $maybe_error = call_user_func( $opts['submit'], $user, $_POST );
254
+
255
+ if ( is_wp_error( $maybe_error ) ) {
256
+ $this->error = $maybe_error;
257
+
258
+ return;
259
+ }
260
+
261
+ if ( $next = $this->get_next_interstitial( $action, $user ) ) {
262
+ $this->show_interstitial( $next, $user );
263
+ }
264
+
265
+ if ( $this->result( $opts['force_completion'], array( $user ) ) ) {
266
+ $this->delete_token( $user );
267
+ wp_set_auth_cookie( $user->ID, ! empty( $_REQUEST['rememberme'] ) );
268
+ }
269
+
270
+ if ( $opts['after_submit'] ) {
271
+ call_user_func( $opts['after_submit'], $user, $_POST );
272
+ }
273
+
274
+ if ( ! get_user_meta( $user->ID, '_itsec_has_logged_in', true ) ) {
275
+ update_user_meta( $user->ID, '_itsec_has_logged_in', ITSEC_Core::get_current_time_gmt() );
276
+ }
277
+
278
+ /**
279
+ * Fires when a user is re-logged back in after submitting an interstitial.
280
+ *
281
+ * @param WP_User $user
282
+ */
283
+ do_action( 'itsec_login_interstitial_logged_in', $user );
284
+
285
+ if ( $GLOBALS['interim_login'] = isset( $_REQUEST['interim-login'] ) ) {
286
+ $this->interim_login();
287
+ }
288
+
289
+ if ( empty( $_REQUEST['redirect_to'] ) ) {
290
+ $redirect_to = admin_url( 'index.php' );
291
+ $requested = '';
292
+ } else {
293
+ $redirect_to = $requested = $_REQUEST['redirect_to'];
294
+ }
295
+
296
+ $redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested, $user );
297
+ wp_safe_redirect( $redirect_to );
298
+
299
+ die;
300
+ }
301
+
302
+ /**
303
+ * Ajax Handler.
304
+ */
305
+ public function ajax_handler() {
306
+
307
+ if ( empty( $_POST['itsec_interstitial_action'] ) ) {
308
+ return;
309
+ }
310
+
311
+ $action = $_POST['itsec_interstitial_action'];
312
+
313
+ if ( empty( $this->registered[ $action ]['ajax_handler'] ) ) {
314
+ wp_send_json_error( array( 'message' => esc_html__( 'Invalid Interstitial Action', 'better-wp-security' ) ) );
315
+ }
316
+
317
+ $opts = $this->registered[ $action ];
318
+
319
+ $user = $this->get_user( $action, true );
320
+
321
+ if ( is_wp_error( $user ) ) {
322
+ wp_send_json_error( array( 'message' => $user->get_error_message() ) );
323
+ }
324
+
325
+ if ( ! $this->result( $opts['show_to_user'], array( $user, isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) ) ) ) {
326
+ wp_send_json_error( array( 'message' => esc_html__( 'Unsupported Interstitial', 'better-wp-security' ) ) );
327
+ }
328
+
329
+ $data = $_POST;
330
+ unset( $data['itsec_interstitial_user'], $data['itsec_interstitial_token'] );
331
+
332
+ call_user_func( $opts['ajax_handler'], $user, $data );
333
+ }
334
+
335
+ /**
336
+ * Handle displaying the interstitial form.
337
+ */
338
+ public function display() {
339
+
340
+ $action = substr( current_action(), strlen( 'login_form_itsec-' ) );
341
+
342
+ if ( empty( $this->registered[ $action ] ) ) {
343
+ return;
344
+ }
345
+
346
+ $opts = $this->registered[ $action ];
347
+
348
+ $user = null;
349
+ $token = isset( $_REQUEST['itsec_interstitial_token'] ) ? $_REQUEST['itsec_interstitial_token'] : null;
350
+
351
+ $user = $this->get_user( $action );
352
+
353
+ if ( ! $user ) {
354
+ wp_safe_redirect( set_url_scheme( wp_login_url(), 'login_post' ) );
355
+ die;
356
+ }
357
+
358
+ if ( ! $this->result( $opts['show_to_user'], array( $user, isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) ) ) ) {
359
+ wp_safe_redirect( set_url_scheme( wp_login_url(), 'login_post' ) );
360
+ die;
361
+ }
362
+
363
+ $this->login_html( $user, $action, $token );
364
+ die;
365
+ }
366
+
367
+ /**
368
+ * Display an interstitial form during the login process.
369
+ *
370
+ * @param WP_User $user
371
+ * @param string $action
372
+ * @param string $token
373
+ */
374
+ protected function login_html( $user, $action, $token = null ) {
375
+
376
+ $wp_login_url = set_url_scheme( wp_login_url(), 'login_post' );
377
+ $wp_login_url = add_query_arg( 'action', "itsec-{$action}", $wp_login_url );
378
+
379
+ if ( isset( $_GET['wpe-login'] ) && ! preg_match( '/[&?]wpe-login=/', $wp_login_url ) ) {
380
+ $wp_login_url = add_query_arg( 'wpe-login', $_GET['wpe-login'], $wp_login_url );
381
+ }
382
+
383
+ $interim_login = isset( $_REQUEST['interim-login'] );
384
+ $redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : '';
385
+
386
+ $rememberme = ! empty( $_REQUEST['rememberme'] );
387
+
388
+ wp_enqueue_script( 'user-profile' );
389
+
390
+ // Prevent JetPack from attempting to SSO the update password form.
391
+ add_filter( 'jetpack_sso_allowed_actions', '__return_empty_array' );
392
+
393
+ if ( ! function_exists( 'login_header' ) ) {
394
+ require_once( dirname( __FILE__ ) . '/includes/function.login-header.php' );
395
+ }
396
+
397
+ $opts = $this->registered[ $action ];
398
+
399
+ login_header();
400
+ ?>
401
+
402
+ <?php if ( $this->error ) : ?>
403
+ <div id="login-error" class="message" style="border-left-color: #dc3232;">
404
+ <?php echo $this->error->get_error_message(); ?>
405
+ </div>
406
+ <?php elseif ( $message = $this->result( $opts['info_message'], array( $user ) ) ): ?>
407
+ <p class="message"><?php echo $message; ?></p>
408
+ <?php endif; ?>
409
+
410
+ <form name="itsec-<?php echo esc_attr( $action ); ?>" id="itsec-<?php echo esc_attr( $action ); ?>"
411
+ action="<?php echo esc_url( $wp_login_url ); ?>" method="post" autocomplete="off">
412
+
413
+ <?php call_user_func( $opts['render'], $user, compact( 'token', 'wp_login_url', 'redirect_to', 'rememberme' ) ); ?>
414
+
415
+ <?php if ( $interim_login ) : ?>
416
+ <input type="hidden" name="interim-login" value="1"/>
417
+ <?php else : ?>
418
+ <input type="hidden" name="redirect_to" value="<?php echo esc_url( $redirect_to ); ?>"/>
419
+ <?php endif; ?>
420
+
421
+ <input type="hidden" name="rememberme" id="rememberme" value="<?php echo esc_attr( $rememberme ); ?>"/>
422
+ <input type="hidden" name="action" value="<?php echo esc_attr( "itsec-{$action}" ); ?>">
423
+
424
+ <?php if ( null !== $token ): ?>
425
+ <input type="hidden" name="itsec_interstitial_user" value="<?php echo esc_attr( $user->ID ); ?>">
426
+ <input type="hidden" name="itsec_interstitial_token" value="<?php echo esc_attr( $token ); ?>">
427
+ <?php endif; ?>
428
+
429
+ <?php if ( isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ], $this->registered[ $_REQUEST[ self::SHOW_AFTER_LOGIN ] ] ) ) : ?>
430
+ <input type="hidden" name="<?php echo esc_attr( self::SHOW_AFTER_LOGIN ); ?>" value="<?php echo esc_attr( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ); ?>">
431
+ <?php endif; ?>
432
+ </form>
433
+
434
+ <p id="backtoblog">
435
+ <a href="<?php echo esc_url( home_url( '/' ) ); ?>" title="<?php esc_attr_e( 'Are you lost?', 'better-wp-security' ); ?>">
436
+ <?php echo esc_html( sprintf( __( '&larr; Back to %s', 'better-wp-security' ), get_bloginfo( 'title', 'display' ) ) ); ?>
437
+ </a>
438
+ </p>
439
+
440
+ </div>
441
+ <?php do_action( 'login_footer' ); ?>
442
+ <div class="clear"></div>
443
+ </body>
444
+ </html>
445
+ <?php
446
+ }
447
+
448
+ /**
449
+ * Handle the interim login screen.
450
+ */
451
+ private function interim_login() {
452
+
453
+ if ( ! function_exists( 'login_header' ) ) {
454
+ require_once( dirname( __FILE__ ) . '/includes/function.login-header.php' );
455
+ }
456
+
457
+ $GLOBALS['interim_login'] = 'success';
458
+ $customize_login = isset( $_REQUEST['customize-login'] );
459
+
460
+ if ( $customize_login ) {
461
+ wp_enqueue_script( 'customize-base' );
462
+ }
463
+
464
+ login_header( '', '<p class="message">' . __( 'You have logged in successfully.' ) . '</p>' );
465
+ ?>
466
+ </div>
467
+ <?php
468
+
469
+ do_action( 'login_footer' ); ?>
470
+
471
+ <?php if ( $customize_login ) : ?>
472
+ <script type="text/javascript">
473
+ setTimeout( function () {
474
+ new wp.customize.Messenger( {
475
+ url : '<?php echo wp_customize_url(); ?>',
476
+ channel: 'login'
477
+ } ).send( 'login' )
478
+ }, 1000 );
479
+ </script>
480
+ <?php endif; ?>
481
+
482
+ </body></html>
483
+ <?php die;
484
+ }
485
+
486
+ /**
487
+ * Get the next interstitial to be displayed.
488
+ *
489
+ * @param string $current The interstitial that was just submitted.
490
+ * @param WP_User $user
491
+ *
492
+ * @return string|false
493
+ */
494
+ private function get_next_interstitial( $current, $user ) {
495
+
496
+ $past_current = false;
497
+
498
+ foreach ( $this->get_applicable_interstitials( $user ) as $handler => $opts ) {
499
+ if ( $handler === $current ) {
500
+ $past_current = true;
501
+ continue;
502
+ }
503
+
504
+ if ( $past_current ) {
505
+ return $handler;
506
+ }
507
+ }
508
+
509
+ return false;
510
+ }
511
+
512
+ /**
513
+ * Get all handlers that are applicable to the given user.
514
+ *
515
+ * @param WP_User $user
516
+ *
517
+ * @return array
518
+ */
519
+ private function get_applicable_interstitials( $user ) {
520
+
521
+ $applicable = array();
522
+
523
+ foreach ( $this->registered as $action => $opts ) {
524
+ if ( $this->is_interstitial_applicable( $action, $user ) ) {
525
+ $applicable[ $action ] = $opts;
526
+ }
527
+ }
528
+
529
+ return $applicable;
530
+ }
531
+
532
+ /**
533
+ * Is the interstitial applicable to the given user.
534
+ *
535
+ * @param string $interstitial
536
+ *
537
+ * @param WP_User $user
538
+ *
539
+ * @return bool
540
+ */
541
+ private function is_interstitial_applicable( $interstitial, $user ) {
542
+
543
+ $opts = $this->registered[ $interstitial ];
544
+
545
+ if ( ! $this->result( $opts['show_to_user'], array( $user, isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) ) ) ) {
546
+ return false;
547
+ }
548
+
549
+ if ( ! did_action( 'login_init' ) && $this->result( $opts['wp_login_only'], array( $user ) ) ) {
550
+ return false;
551
+ }
552
+
553
+ return true;
554
+ }
555
+
556
+ /**
557
+ * Handle checking for and validating the token, if it does not exist, will redirect with error message.
558
+ *
559
+ * @param string $action
560
+ * @param bool $return_error
561
+ *
562
+ * @return WP_Error|array Array with token and user.
563
+ */
564
+ private function handle_token( $action, $return_error = false ) {
565
+
566
+ $is_valid = true;
567
+ $user = null;
568
+
569
+ if ( empty( $_REQUEST['itsec_interstitial_user'] ) || empty( $_REQUEST['itsec_interstitial_token'] ) ) {
570
+ $is_valid = false;
571
+ } elseif ( ( ! $user = get_userdata( $_REQUEST['itsec_interstitial_user'] ) ) || ! $this->verify_token( $user, $_REQUEST['itsec_interstitial_token'] ) ) {
572
+ $is_valid = false;
573
+ }
574
+
575
+ if ( ! $is_valid ) {
576
+ if ( $return_error ) {
577
+ return new WP_Error(
578
+ 'itsec-login-interstitial-invalid-token',
579
+ esc_html__( 'Sorry, this request has expired. Please log in again.', 'better-wp-security' )
580
+ );
581
+ }
582
+
583
+ $this->redirect_invalid_token( $action );
584
+ }
585
+
586
+ return array( $user, $_REQUEST['itsec_interstitial_token'] );
587
+ }
588
+
589
+ /**
590
+ * Get the current user for the interstitial.
591
+ *
592
+ * @param string $action
593
+ * @param bool $return_error
594
+ *
595
+ * @return WP_Error|WP_User|null
596
+ */
597
+ private function get_user( $action, $return_error = false ) {
598
+
599
+ $opts = $this->registered[ $action ];
600
+
601
+ if ( false === $opts['force_completion'] ) {
602
+ return is_user_logged_in() ? wp_get_current_user() : null;
603
+ }
604
+
605
+ if ( isset( $_REQUEST['itsec_interstitial_user'] ) || true === $opts['force_completion'] ) {
606
+ $maybe = $this->handle_token( $action, $return_error );
607
+
608
+ return is_wp_error( $maybe ) ? $maybe : $maybe[0];
609
+ }
610
+
611
+ $user = wp_get_current_user();
612
+
613
+ if ( $user && $user->exists() && ! call_user_func( $opts['force_completion'], $user ) ) {
614
+ return $user;
615
+ }
616
+
617
+ if ( $return_error ) {
618
+ return new WP_Error(
619
+ 'itsec-login-interstitial-invalid-token',
620
+ esc_html__( 'Sorry, this request has expired. Please log in again.', 'better-wp-security' )
621
+ );
622
+ }
623
+
624
+ $this->redirect_invalid_token( $action );
625
+ }
626
+
627
+ /**
628
+ * Redirect back to the login page with a message that the token is invalid.
629
+ *
630
+ * @param string $action
631
+ */
632
+ private function redirect_invalid_token( $action ) {
633
+ $redirect = add_query_arg( 'itsec_interstitial_expired', $action, wp_login_url() );
634
+ wp_safe_redirect( set_url_scheme( $redirect, 'login_post' ) );
635
+ die;
636
+ }
637
+
638
+ /**
639
+ * Destroy the session for a user.
640
+ *
641
+ * @param WP_User $user
642
+ */
643
+ private function destroy_session( $user ) {
644
+ WP_Session_Tokens::get_instance( $user->ID )->destroy( $this->session_token ? $this->session_token : wp_get_session_token() );
645
+ wp_clear_auth_cookie();
646
+ }
647
+
648
+ /**
649
+ * Verify that the token is valid.
650
+ *
651
+ * @param WP_User $user
652
+ * @param string $key
653
+ *
654
+ * @return bool
655
+ */
656
+ private function verify_token( $user, $key ) {
657
+ $expected = get_user_meta( $user->ID, self::META_KEY, true );
658
+
659
+ if ( ! $expected || ! is_array( $expected ) ) {
660
+ return false;
661
+ }
662
+
663
+ if ( empty( $expected['expires'] ) || $expected['expires'] < ITSEC_Core::get_current_time_gmt() ) {
664
+ return false;
665
+ }
666
+
667
+ return hash_equals( $expected['key'], $key );
668
+ }
669
+
670
+ /**
671
+ * Set the token for a user.
672
+ *
673
+ * @param WP_User $user
674
+ *
675
+ * @return string
676
+ */
677
+ private function set_token( $user ) {
678
+ $key = $this->generate_token();
679
+
680
+ update_user_meta( $user->ID, self::META_KEY, array(
681
+ 'key' => $key,
682
+ 'expires' => ITSEC_Core::get_current_time_gmt() + HOUR_IN_SECONDS
683
+ ) );
684
+
685
+ return $key;
686
+ }
687
+
688
+ /**
689
+ * Generate a token to be used to verify intent of submitting a login interstitial.
690
+ *
691
+ * We can't use nonces here because the WordPress Session Tokens won't be initialized yet.
692
+ *
693
+ * @return string
694
+ */
695
+ private function generate_token() {
696
+ return sha1( wp_generate_password( 32, true, true ) );
697
+ }
698
+
699
+ /**
700
+ * Delete the token for a user.
701
+ *
702
+ * @param WP_User $user
703
+ */
704
+ private function delete_token( $user ) {
705
+ delete_user_meta( $user->ID, self::META_KEY );
706
+ }
707
+
708
+ /**
709
+ * Try and get a value from the provider.
710
+ *
711
+ * If it is a function, will call the function with the provided args.
712
+ *
713
+ * @param bool|callable $provider
714
+ * @param array $args
715
+ *
716
+ * @return bool|mixed
717
+ */
718
+ private function result( $provider, $args = array() ) {
719
+ if ( is_bool( $provider ) ) {
720
+ return $provider;
721
+ }
722
+
723
+ if ( is_callable( $provider, true ) ) {
724
+ return call_user_func_array( $provider, $args );
725
+ }
726
+
727
+ return $provider;
728
+ }
729
+ }
core/lib/class-itsec-lib-password-requirements.php CHANGED
@@ -23,10 +23,7 @@ class ITSEC_Lib_Password_Requirements {
23
  add_action( 'validate_password_reset', array( $this, 'forward_reset_pass' ), 10, 2 );
24
  add_action( 'profile_update', array( $this, 'set_password_last_updated' ), 10, 2 );
25
 
26
- add_action( 'wp_login', array( $this, 'wp_login' ), 12, 2 );
27
- add_filter( 'wp_login_errors', array( $this, 'token_expired_message' ) );
28
- add_action( 'login_form_' . self::LOGIN_ACTION, array( $this, 'handle_update_password_form' ), 9 );
29
- add_action( 'login_form_' . self::LOGIN_ACTION, array( $this, 'display_update_password_form' ) );
30
  }
31
 
32
  /**
@@ -70,14 +67,14 @@ class ITSEC_Lib_Password_Requirements {
70
  */
71
  public function forward_reset_pass( $errors, $user ) {
72
 
73
- if ( ! isset( $_POST['pass1'] ) ) {
74
  // The validate_password_reset action fires when first rendering the reset page and when handling the form
75
  // submissions. Since the pass1 data is missing, this must be the initial page render. So, we don't need to
76
  // do anything yet.
77
  return;
78
  }
79
 
80
- self::validate_password( $user, $user->user_pass, array(
81
  'error' => $errors,
82
  'context' => 'reset-password',
83
  ) );
@@ -102,316 +99,102 @@ class ITSEC_Lib_Password_Requirements {
102
  }
103
 
104
  /**
105
- * Whenever a user logs in, check if their password needs to be changed. If so, mark that the user must change
106
- * their password.
107
  *
108
- * @since 1.8
109
- *
110
- * @param string $username the username attempted
111
- * @param WP_User $user wp_user the user
112
- *
113
- * @return void
114
- */
115
- public function wp_login( $username, $user = null ) {
116
-
117
- //Get a valid user or terminate the hook (all we care about is forcing the password change... Let brute force protection handle the rest
118
- if ( null !== $user ) {
119
- $current_user = $user;
120
- } elseif ( is_user_logged_in() ) {
121
- $current_user = wp_get_current_user();
122
- } else {
123
- return;
124
- }
125
-
126
- if ( ! self::password_change_required( $current_user ) ) {
127
- return;
128
- }
129
-
130
- $token = $this->set_update_password_key( $current_user );
131
- $this->destroy_session( $current_user );
132
-
133
- $this->login_html( $current_user, $token );
134
- exit;
135
- }
136
-
137
- /**
138
- * Add a message that the update password token has expired and they must login again.
139
- *
140
- * @param WP_Error $errors
141
- *
142
- * @return WP_Error
143
  */
144
- public function token_expired_message( $errors ) {
145
-
146
- if ( ! empty( $_GET['itsec_update_pass_expired'] ) ) {
147
- $errors->add(
148
- 'itsec_update_pass_expired',
149
- esc_html__( 'Sorry, the update password request has expired. Please log in again.', 'better-wp-security' )
150
- );
151
- }
152
-
153
- return $errors;
154
- }
155
-
156
- /**
157
- * Handle the request to update the user's password.
158
- */
159
- public function handle_update_password_form() {
160
-
161
- if ( empty( $_POST['itsec_update_password'] ) || empty( $_POST['itsec_update_password_user'] ) || empty( $_POST['pass1'] ) ) {
162
- return;
163
- }
164
-
165
- $user = get_userdata( $_POST['itsec_update_password_user'] );
166
-
167
- if ( ! $user || empty( $_POST['itsec_update_password_token'] ) || ! $this->verify_update_password_key( $user, $_POST['itsec_update_password_token'] ) ) {
168
-
169
- $url = add_query_arg( 'itsec_update_pass_expired', 1, wp_login_url() );
170
- wp_safe_redirect( set_url_scheme( $url, 'login_post' ) );
171
- die();
172
- }
173
-
174
- $error = self::validate_password( $user, $_POST['pass1'] );
175
-
176
- if ( $error->get_error_message() ) {
177
- $this->error_message = $error->get_error_message();
178
-
179
- return;
180
- }
181
-
182
- $error = wp_update_user( array(
183
- 'ID' => $user->ID,
184
- 'user_pass' => $_POST['pass1']
185
  ) );
186
-
187
- if ( is_wp_error( $error ) ) {
188
- $this->error_message = $error->get_error_message();
189
-
190
- return;
191
- }
192
-
193
- $this->delete_update_password_key( $user );
194
- wp_set_auth_cookie( $user->ID, ! empty( $_REQUEST['rememberme'] ) );
195
-
196
- if ( ! empty( $_REQUEST['redirect_to'] ) ) {
197
- $redirect_to = apply_filters( 'login_redirect', $_REQUEST['redirect_to'], $_REQUEST['redirect_to'], $user );
198
- wp_safe_redirect( $redirect_to );
199
- } else {
200
- wp_safe_redirect( admin_url( 'index.php' ) );
201
- }
202
-
203
- exit;
204
- }
205
-
206
- /**
207
- * When the login page is loaded with the 'itsec_update_password' action, maybe display the update password form,
208
- * or redirect to a standard login page.
209
- */
210
- public function display_update_password_form() {
211
-
212
- $user = null;
213
- $token = '';
214
-
215
- if ( is_user_logged_in() ) {
216
- $user = wp_get_current_user();
217
- $token = $this->set_update_password_key( $user );
218
- $this->destroy_session( $user );
219
- } elseif ( ! empty( $_POST['itsec_update_password_user'] ) ) {
220
- $user = get_userdata( $_POST['itsec_update_password_user'] );
221
- $token = empty( $_POST['itsec_update_password_token'] ) ? '' : $_POST['itsec_update_password_token'];
222
- }
223
-
224
- if ( ! $user ) {
225
- wp_safe_redirect( set_url_scheme( wp_login_url(), 'login_post' ) );
226
- die();
227
- }
228
-
229
- if ( ! self::password_change_required( $user ) ) {
230
- wp_safe_redirect( set_url_scheme( wp_login_url(), 'login_post' ) );
231
- die();
232
- }
233
-
234
- $this->login_html( $user, $token );
235
- exit;
236
- }
237
-
238
- /**
239
- * Destroy the session for a user.
240
- *
241
- * @param WP_User $user
242
- */
243
- private function destroy_session( $user ) {
244
- WP_Session_Tokens::get_instance( $user->ID )->destroy_all();
245
- wp_clear_auth_cookie();
246
  }
247
 
248
  /**
249
- * Verify that the update password key is valid.
250
  *
251
  * @param WP_User $user
252
- * @param string $key
253
- *
254
- * @return bool
255
  */
256
- private function verify_update_password_key( $user, $key ) {
257
- $expected = get_user_meta( $user->ID, self::META_KEY, true );
258
-
259
- if ( ! $expected || ! is_array( $expected ) ) {
260
- return false;
261
- }
262
 
263
- if ( empty( $expected['expires'] ) || $expected['expires'] < ITSEC_Core::get_current_time_gmt() ) {
264
- return false;
265
- }
266
 
267
- return hash_equals( $expected['key'], $key );
268
- }
 
 
 
 
 
 
269
 
270
- /**
271
- * Set the update password key for a user.
272
- *
273
- * @param WP_User $user
274
- *
275
- * @return string
276
- */
277
- private function set_update_password_key( $user ) {
278
- $key = $this->generate_update_password_key();
279
 
280
- update_user_meta( $user->ID, self::META_KEY, array(
281
- 'key' => $key,
282
- 'expires' => ITSEC_Core::get_current_time_gmt() + HOUR_IN_SECONDS
283
- ) );
284
 
285
- return $key;
286
- }
 
 
287
 
288
- /**
289
- * Generate a token to be used to verify intent of updating password.
290
- *
291
- * We can't use nonces here because the WordPress Session Tokens won't be initialized yet.
292
- *
293
- * @return string
294
- */
295
- private function generate_update_password_key() {
296
- return wp_generate_password( 32, true, false );
297
  }
298
 
299
  /**
300
- * Delete the update password key for a user.
301
  *
302
  * @param WP_User $user
303
- */
304
- private function delete_update_password_key( $user ) {
305
- delete_user_meta( $user->ID, self::META_KEY );
306
- }
307
-
308
- /**
309
- * Display an interstitial form during the login process to force a user to update their password.
310
  *
311
- * @param WP_User $user
312
- * @param string $token
313
  */
314
- protected function login_html( $user, $token ) {
315
 
316
- $wp_login_url = set_url_scheme( wp_login_url(), 'login_post' );
317
- $wp_login_url = add_query_arg( 'action', self::LOGIN_ACTION, $wp_login_url );
318
-
319
- if ( isset( $_GET['wpe-login'] ) && ! preg_match( '/[&?]wpe-login=/', $wp_login_url ) ) {
320
- $wp_login_url = add_query_arg( 'wpe-login', $_GET['wpe-login'], $wp_login_url );
321
  }
322
 
323
- $interim_login = isset( $_REQUEST['interim-login'] );
324
- $redirect_to = '';
325
-
326
- $rememberme = ! empty( $_REQUEST['rememberme'] );
327
-
328
- wp_enqueue_script( 'user-profile' );
329
 
330
- // Prevent JetPack from attempting to SSO the update password form.
331
- add_filter( 'jetpack_sso_allowed_actions', '__return_empty_array' );
332
-
333
- if ( ! function_exists( 'login_header' ) ) {
334
- require_once( dirname( __FILE__ ) . '/includes/function.login-header.php' );
335
  }
336
 
337
- login_header();
338
-
339
- $type = self::password_change_required( $user );
340
- // Modules are responsible for providing escaped reason messages
341
- $reason = $this->get_message_for_password_change_reason( $type );
342
- ?>
343
-
344
- <?php if ( $this->error_message ) : ?>
345
- <div id="login-error" class="message" style="border-left-color: #dc3232;">
346
- <?php echo $this->error_message; ?>
347
- </div>
348
- <?php else: ?>
349
- <p class="message"><?php echo $reason; ?></p>
350
- <?php endif; ?>
351
-
352
- <form name="resetpassform" id="resetpassform" action="<?php echo esc_url( $wp_login_url ); ?>" method="post"
353
- autocomplete="off">
354
-
355
- <div class="user-pass1-wrap">
356
- <p><label for="pass1"><?php _e( 'New Password', 'better-wp-security' ); ?></label></p>
357
- </div>
358
 
359
- <div class="wp-pwd">
360
- <span class="password-input-wrapper">
361
- <input type="password" data-reveal="1"
362
- data-pw="<?php echo esc_attr( wp_generate_password( 16 ) ); ?>" name="pass1" id="pass1"
363
- class="input" size="20" value="" autocomplete="off" aria-describedby="pass-strength-result"/>
364
- </span>
365
- <div id="pass-strength-result" class="hide-if-no-js" aria-live="polite"><?php _e( 'Strength indicator', 'better-wp-security' ); ?></div>
366
- </div>
367
-
368
- <p class="user-pass2-wrap">
369
- <label for="pass2"><?php _e( 'Confirm new password' ) ?></label><br/>
370
- <input type="password" name="pass2" id="pass2" class="input" size="20" value="" autocomplete="off"/>
371
- </p>
372
-
373
- <p class="description indicator-hint"><?php echo wp_get_password_hint(); ?></p>
374
- <br class="clear"/>
375
-
376
- <p class="submit">
377
- <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large"
378
- value="<?php esc_attr_e( 'Update Password', 'better-wp-security' ); ?>"/>
379
- </p>
380
-
381
- <?php if ( $interim_login ) : ?>
382
- <input type="hidden" name="interim-login" value="1"/>
383
- <?php else : ?>
384
- <input type="hidden" name="redirect_to" value="<?php echo esc_url( $redirect_to ); ?>"/>
385
- <?php endif; ?>
386
-
387
- <input type="hidden" name="rememberme" id="rememberme" value="<?php echo esc_attr( $rememberme ); ?>"/>
388
- <input type="hidden" name="itsec_update_password" value="1">
389
- <input type="hidden" name="itsec_update_password_token" value="<?php echo esc_attr( $token ); ?>">
390
- <input type="hidden" name="itsec_update_password_user" value="<?php echo esc_attr( $user->ID ); ?>">
391
- </form>
392
-
393
- <p id="backtoblog">
394
- <a href="<?php echo esc_url( home_url( '/' ) ); ?>" title="<?php esc_attr_e( 'Are you lost?', 'better-wp-security' ); ?>">
395
- <?php echo esc_html( sprintf( __( '&larr; Back to %s', 'better-wp-security' ), get_bloginfo( 'title', 'display' ) ) ); ?>
396
- </a>
397
- </p>
398
 
399
- </div>
400
- <?php do_action( 'login_footer' ); ?>
401
- <div class="clear"></div>
402
- </body>
403
- </html>
404
- <?php
405
  }
406
 
407
  /**
408
  * Get a message indicating to the user why a password change is required.
409
  *
410
- * @param string $reason
411
  *
412
  * @return string
413
  */
414
- protected function get_message_for_password_change_reason( $reason ) {
 
 
 
 
415
 
416
  /**
417
  * Retrieve a human readable description as to why a password change has been required for the current user.
23
  add_action( 'validate_password_reset', array( $this, 'forward_reset_pass' ), 10, 2 );
24
  add_action( 'profile_update', array( $this, 'set_password_last_updated' ), 10, 2 );
25
 
26
+ add_action( 'itsec_login_interstitial_init', array( $this, 'register_interstitial' ) );
 
 
 
27
  }
28
 
29
  /**
67
  */
68
  public function forward_reset_pass( $errors, $user ) {
69
 
70
+ if ( ! isset( $_POST['pass1'] ) || is_wp_error( $user ) ) {
71
  // The validate_password_reset action fires when first rendering the reset page and when handling the form
72
  // submissions. Since the pass1 data is missing, this must be the initial page render. So, we don't need to
73
  // do anything yet.
74
  return;
75
  }
76
 
77
+ self::validate_password( $user, $_POST['pass1'], array(
78
  'error' => $errors,
79
  'context' => 'reset-password',
80
  ) );
99
  }
100
 
101
  /**
102
+ * Register the password change interstitial.
 
103
  *
104
+ * @param ITSEC_Lib_Login_Interstitial $lib
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  */
106
+ public function register_interstitial( $lib ) {
107
+ $lib->register( 'update-password', array( $this, 'render_interstitial' ), array(
108
+ 'show_to_user' => array( __CLASS__, 'password_change_required' ),
109
+ 'info_message' => array( __CLASS__, 'get_message_for_password_change_reason' ),
110
+ 'submit' => array( $this, 'submit' ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
114
  /**
115
+ * Render the interstitial.
116
  *
117
  * @param WP_User $user
 
 
 
118
  */
119
+ public function render_interstitial( $user ) {
120
+ ?>
 
 
 
 
121
 
122
+ <div class="user-pass1-wrap">
123
+ <p><label for="pass1"><?php _e( 'New Password', 'better-wp-security' ); ?></label></p>
124
+ </div>
125
 
126
+ <div class="wp-pwd">
127
+ <span class="password-input-wrapper">
128
+ <input type="password" data-reveal="1"
129
+ data-pw="<?php echo esc_attr( wp_generate_password( 16 ) ); ?>" name="pass1" id="pass1"
130
+ class="input" size="20" value="" autocomplete="off" aria-describedby="pass-strength-result"/>
131
+ </span>
132
+ <div id="pass-strength-result" class="hide-if-no-js" aria-live="polite"><?php _e( 'Strength indicator', 'better-wp-security' ); ?></div>
133
+ </div>
134
 
135
+ <p class="user-pass2-wrap">
136
+ <label for="pass2"><?php _e( 'Confirm new password' ) ?></label><br/>
137
+ <input type="password" name="pass2" id="pass2" class="input" size="20" value="" autocomplete="off"/>
138
+ </p>
 
 
 
 
 
139
 
140
+ <p class="description indicator-hint"><?php echo wp_get_password_hint(); ?></p>
141
+ <br class="clear"/>
 
 
142
 
143
+ <p class="submit">
144
+ <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large"
145
+ value="<?php esc_attr_e( 'Update Password', 'better-wp-security' ); ?>"/>
146
+ </p>
147
 
148
+ <?php
 
 
 
 
 
 
 
 
149
  }
150
 
151
  /**
152
+ * Handle the request to update the user's password.
153
  *
154
  * @param WP_User $user
155
+ * @param array $data POSTed data.
 
 
 
 
 
 
156
  *
157
+ * @return WP_Error|null
 
158
  */
159
+ public function submit( $user, $data ) {
160
 
161
+ if ( empty( $data['pass1'] ) ) {
162
+ return new WP_Error(
163
+ 'itsec-password-requirements-empty-password',
164
+ __( 'Please enter your new password.', 'better-wp-security' )
165
+ );
166
  }
167
 
168
+ $error = self::validate_password( $user, $data['pass1'] );
 
 
 
 
 
169
 
170
+ if ( $error->get_error_message() ) {
171
+ return $error;
 
 
 
172
  }
173
 
174
+ $error = wp_update_user( array(
175
+ 'ID' => $user->ID,
176
+ 'user_pass' => $data['pass1']
177
+ ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
+ if ( is_wp_error( $error ) ) {
180
+ return $error;
181
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
+ return null;
 
 
 
 
 
184
  }
185
 
186
  /**
187
  * Get a message indicating to the user why a password change is required.
188
  *
189
+ * @param WP_User $user
190
  *
191
  * @return string
192
  */
193
+ public static function get_message_for_password_change_reason( $user ) {
194
+
195
+ if ( ! $reason = self::password_change_required( $user ) ) {
196
+ return '';
197
+ }
198
 
199
  /**
200
  * Retrieve a human readable description as to why a password change has been required for the current user.
core/lib/class-itsec-scheduler-cron.php CHANGED
@@ -77,8 +77,57 @@ class ITSEC_Scheduler_Cron extends ITSEC_Scheduler {
77
 
78
  $job = $this->make_job( $id, $data, $opts );
79
 
80
- $this->call_action( $job );
81
  $this->unschedule_single( $id, $data );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
 
84
  public function is_recurring_scheduled( $id ) {
@@ -205,16 +254,15 @@ class ITSEC_Scheduler_Cron extends ITSEC_Scheduler {
205
  $data_hash = $this->hash_data( $data );
206
  $hash = $this->make_cron_hash( $id, $data );
207
 
208
- if ( $this->unschedule_by_hash( $hash ) ) {
209
 
210
- $options = $this->get_options();
211
  unset( $options['single'][ $id ][ $data_hash ] );
212
  $this->set_options( $options );
213
-
214
- return true;
215
  }
216
 
217
- return false;
218
  }
219
 
220
  private function unschedule_by_hash( $hash ) {
@@ -330,9 +378,15 @@ class ITSEC_Scheduler_Cron extends ITSEC_Scheduler {
330
 
331
  unset( $maybe_data['retry_count'] );
332
 
333
- if ( $this->hash_data( $maybe_data ) === $this->hash_data( $data ) ) {
334
- return true;
 
 
 
 
335
  }
 
 
336
  }
337
 
338
  return false;
77
 
78
  $job = $this->make_job( $id, $data, $opts );
79
 
 
80
  $this->unschedule_single( $id, $data );
81
+ $this->call_action( $job );
82
+ }
83
+
84
+ public function run_due_now( $now = 0 ) {
85
+
86
+ if ( ! ITSEC_Lib::get_lock( 'scheduler', 120 ) ) {
87
+ return;
88
+ }
89
+
90
+ if ( ! $crons = _get_cron_array() ) {
91
+ return;
92
+ }
93
+
94
+ if ( ! is_main_site() ) {
95
+ // This is currently never run from a non main site context, but just in case.
96
+ switch_to_blog( get_network()->site_id );
97
+ }
98
+
99
+ if ( get_transient( 'doing_cron' ) ) {
100
+ is_multisite() && restore_current_blog();
101
+
102
+ return;
103
+ }
104
+
105
+ if ( ITSEC_Lib::get_uncached_option( '_transient_doing_cron' ) ) {
106
+ is_multisite() && restore_current_blog();
107
+
108
+ return;
109
+ }
110
+
111
+ $now = $now ? $now : ITSEC_Core::get_current_time_gmt();
112
+
113
+ foreach ( $crons as $timestamp => $hooks ) {
114
+ if ( $timestamp > $now || ! isset( $hooks[ self::HOOK ] ) ) {
115
+ continue;
116
+ }
117
+
118
+ foreach ( $hooks[ self::HOOK ] as $event ) {
119
+
120
+ if ( $schedule = $event['schedule'] ) {
121
+ wp_reschedule_event( $timestamp, $schedule, self::HOOK, $event['args'] );
122
+ }
123
+
124
+ wp_unschedule_event( $timestamp, self::HOOK, $event['args'] );
125
+ call_user_func_array( array( $this, 'process' ), $event['args'] );
126
+ }
127
+ }
128
+
129
+ ITSEC_Lib::release_lock( 'scheduler' );
130
+ is_multisite() && restore_current_blog();
131
  }
132
 
133
  public function is_recurring_scheduled( $id ) {
254
  $data_hash = $this->hash_data( $data );
255
  $hash = $this->make_cron_hash( $id, $data );
256
 
257
+ $unscheduled = $this->unschedule_by_hash( $hash );
258
 
259
+ if ( isset( $options['single'][ $id ][ $data_hash ] ) ) {
260
  unset( $options['single'][ $id ][ $data_hash ] );
261
  $this->set_options( $options );
262
+ $unscheduled = true;
 
263
  }
264
 
265
+ return $unscheduled;
266
  }
267
 
268
  private function unschedule_by_hash( $hash ) {
378
 
379
  unset( $maybe_data['retry_count'] );
380
 
381
+ if ( $this->hash_data( $maybe_data ) !== $this->hash_data( $data ) ) {
382
+ continue;
383
+ }
384
+
385
+ if ( ! wp_next_scheduled( self::HOOK, array( $id, $hash ) ) ) {
386
+ continue;
387
  }
388
+
389
+ return true;
390
  }
391
 
392
  return false;
core/lib/class-itsec-scheduler-page-load.php CHANGED
@@ -176,7 +176,15 @@ class ITSEC_Scheduler_Page_Load extends ITSEC_Scheduler {
176
  return;
177
  }
178
 
179
- $now = ITSEC_Core::get_current_time_gmt();
 
 
 
 
 
 
 
 
180
  $options = $this->get_options();
181
 
182
  $to_process = array();
@@ -271,9 +279,8 @@ class ITSEC_Scheduler_Page_Load extends ITSEC_Scheduler {
271
 
272
  $job = $this->make_job( $id, $event['data'], array( 'single' => true ) );
273
 
274
- $this->call_action( $job );
275
-
276
  $this->unschedule_single( $id, $data );
 
277
 
278
  if ( $clear_operating_data ) {
279
  $this->operating_data = null;
176
  return;
177
  }
178
 
179
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
180
+ return;
181
+ }
182
+
183
+ $this->run_due_now();
184
+ }
185
+
186
+ public function run_due_now( $now = 0 ) {
187
+ $now = $now ? $now : ITSEC_Core::get_current_time_gmt();
188
  $options = $this->get_options();
189
 
190
  $to_process = array();
279
 
280
  $job = $this->make_job( $id, $event['data'], array( 'single' => true ) );
281
 
 
 
282
  $this->unschedule_single( $id, $data );
283
+ $this->call_action( $job );
284
 
285
  if ( $clear_operating_data ) {
286
  $this->operating_data = null;
core/lib/class-itsec-scheduler.php CHANGED
@@ -17,6 +17,9 @@ abstract class ITSEC_Scheduler {
17
  /** @var array */
18
  protected $loops = array();
19
 
 
 
 
20
  /**
21
  * Schedule a recurring event.
22
  *
@@ -165,6 +168,15 @@ abstract class ITSEC_Scheduler {
165
  */
166
  abstract public function run_single_event( $id, $data = array() );
167
 
 
 
 
 
 
 
 
 
 
168
  /**
169
  * Code executed on every page load to setup the scheduler.
170
  *
@@ -172,6 +184,15 @@ abstract class ITSEC_Scheduler {
172
  */
173
  abstract public function run();
174
 
 
 
 
 
 
 
 
 
 
175
  /**
176
  * Manually trigger modules to register their scheduled events.
177
  *
@@ -261,12 +282,28 @@ abstract class ITSEC_Scheduler {
261
  * @param ITSEC_Job $job
262
  */
263
  protected final function call_action( ITSEC_Job $job ) {
264
- /**
265
- * Fires when a scheduled job should be executed.
266
- *
267
- * @param ITSEC_Job $job
268
- */
269
- do_action( "itsec_scheduled_{$job->get_id()}", $job );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
 
272
  /**
17
  /** @var array */
18
  protected $loops = array();
19
 
20
+ /** @var bool */
21
+ private $is_running = false;
22
+
23
  /**
24
  * Schedule a recurring event.
25
  *
168
  */
169
  abstract public function run_single_event( $id, $data = array() );
170
 
171
+ /**
172
+ * Run any events that are due now.
173
+ *
174
+ * @param int $now
175
+ *
176
+ * @return void
177
+ */
178
+ abstract public function run_due_now( $now = 0 );
179
+
180
  /**
181
  * Code executed on every page load to setup the scheduler.
182
  *
184
  */
185
  abstract public function run();
186
 
187
+ /**
188
+ * Check whether the scheduler is currently executing an event.
189
+ *
190
+ * @return bool
191
+ */
192
+ final public function is_running() {
193
+ return $this->is_running;
194
+ }
195
+
196
  /**
197
  * Manually trigger modules to register their scheduled events.
198
  *
282
  * @param ITSEC_Job $job
283
  */
284
  protected final function call_action( ITSEC_Job $job ) {
285
+ $interactive = ITSEC_Core::is_interactive();
286
+ ITSEC_Core::set_interactive( false );
287
+ $this->is_running = true;
288
+
289
+ try {
290
+ /**
291
+ * Fires when a scheduled job should be executed.
292
+ *
293
+ * @param ITSEC_Job $job
294
+ */
295
+ do_action( "itsec_scheduled_{$job->get_id()}", $job );
296
+ } catch ( Exception $e ) {
297
+ ITSEC_Log::add_fatal_error( 'scheduler', 'unhandled-exception', array(
298
+ 'exception' => (string) $e,
299
+ 'job' => $job->get_id(),
300
+ 'data' => $job->get_data(),
301
+ ) );
302
+ $job->reschedule_in( 500 );
303
+ }
304
+
305
+ $this->is_running = false;
306
+ ITSEC_Core::set_interactive( $interactive );
307
  }
308
 
309
  /**
core/lib/debug.php CHANGED
@@ -240,6 +240,7 @@ final class ITSEC_Debug {
240
  $flags = ENT_COMPAT;
241
 
242
  if ( defined( 'ENT_HTML401' ) ) {
 
243
  $flags |= ENT_HTML401;
244
  }
245
 
@@ -255,15 +256,36 @@ final class ITSEC_Debug {
255
  return '<strong>null</strong>';
256
  }
257
 
258
- if ( is_object( $data ) ) {
259
- $class_name = get_class( $data );
 
 
 
 
 
 
 
 
260
  $retval = "<strong>Object</strong> $class_name";
261
 
262
  if ( ! $expand_objects || ( $depth == $max_depth ) ) {
263
  return $retval;
264
  }
265
 
266
- $vars = get_object_vars( $data );
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  if ( empty( $vars ) ) {
269
  $vars = '';
240
  $flags = ENT_COMPAT;
241
 
242
  if ( defined( 'ENT_HTML401' ) ) {
243
+ // phpcs:ignore PHPCompatibility.PHP.NewConstants.ent_html401Found -- Ensure that Tide doesn't reduce compatibility because of this code which is meant to improve compatibility.
244
  $flags |= ENT_HTML401;
245
  }
246
 
256
  return '<strong>null</strong>';
257
  }
258
 
259
+ if ( is_object( $data ) || 'object' === gettype( $data ) ) {
260
+ if ( ! is_object( $data ) || '__PHP_Incomplete_Class' === get_class( $data ) ) {
261
+ // Special handling for objects for classes that are not loaded.
262
+ $vars = get_object_vars( $data );
263
+
264
+ $class_name = $vars['__PHP_Incomplete_Class_Name'];
265
+ } else {
266
+ $class_name = get_class( $data );
267
+ }
268
+
269
  $retval = "<strong>Object</strong> $class_name";
270
 
271
  if ( ! $expand_objects || ( $depth == $max_depth ) ) {
272
  return $retval;
273
  }
274
 
275
+ if ( isset( $vars ) ) {
276
+ // Special handling for objects for classes that are not loaded.
277
+ unset( $vars['__PHP_Incomplete_Class_Name'] );
278
+ $new_vars = array();
279
+
280
+ foreach ( $vars as $key => $val ) {
281
+ $key = substr( $key, strlen( $class_name ) + 2 );
282
+ $new_vars[$key] = $val;
283
+ }
284
+
285
+ $vars = $new_vars;
286
+ } else {
287
+ $vars = get_object_vars( $data );
288
+ }
289
 
290
  if ( empty( $vars ) ) {
291
  $vars = '';
core/lib/log.php CHANGED
@@ -239,7 +239,7 @@ final class ITSEC_Log {
239
  return array(
240
  'critical-issue' => esc_html__( 'Critical Issue', 'better-wp-security' ),
241
  'action' => esc_html__( 'Action', 'better-wp-security' ),
242
- 'fatal-error' => esc_html__( 'Fatal Error', 'better-wp-security' ),
243
  'error' => esc_html__( 'Error', 'better-wp-security' ),
244
  'warning' => esc_html__( 'Warning', 'better-wp-security' ),
245
  'notice' => esc_html__( 'Notice', 'better-wp-security' ),
@@ -268,6 +268,11 @@ final class ITSEC_Log {
268
  }
269
 
270
  public static function rotate_log_files() {
 
 
 
 
 
271
  $log = self::get_log_file_path();
272
  $max_file_size = 10 * 1024 * 1024; // 10MiB
273
 
@@ -314,6 +319,146 @@ final class ITSEC_Log {
314
  unlink( $file );
315
  }
316
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  }
318
 
319
  add_action( 'itsec_scheduler_register_events', array( 'ITSEC_Log', 'register_events' ) );
239
  return array(
240
  'critical-issue' => esc_html__( 'Critical Issue', 'better-wp-security' ),
241
  'action' => esc_html__( 'Action', 'better-wp-security' ),
242
+ 'fatal' => esc_html__( 'Fatal Error', 'better-wp-security' ),
243
  'error' => esc_html__( 'Error', 'better-wp-security' ),
244
  'warning' => esc_html__( 'Warning', 'better-wp-security' ),
245
  'notice' => esc_html__( 'Notice', 'better-wp-security' ),
268
  }
269
 
270
  public static function rotate_log_files() {
271
+
272
+ if ( $days_to_keep = ITSEC_Modules::get_setting( 'global', 'file_log_rotation' ) ) {
273
+ self::delete_old_logs( $days_to_keep );
274
+ }
275
+
276
  $log = self::get_log_file_path();
277
  $max_file_size = 10 * 1024 * 1024; // 10MiB
278
 
319
  unlink( $file );
320
  }
321
  }
322
+
323
+ private static function delete_old_logs( $days_to_keep ) {
324
+
325
+ $log = self::get_log_file_path();
326
+
327
+ if ( ! file_exists( $log ) ) {
328
+ return;
329
+ }
330
+
331
+ $seconds = $days_to_keep * DAY_IN_SECONDS;
332
+
333
+ $files = glob( "$log.*" );
334
+
335
+ foreach ( $files as $file ) {
336
+ if ( ! $time = self::get_latest_write_for_file( $file ) ) {
337
+ continue;
338
+ }
339
+
340
+ if ( $time + $seconds > ITSEC_Core::get_current_time_gmt() ) {
341
+ continue;
342
+ }
343
+
344
+ unlink( $file );
345
+ }
346
+ }
347
+
348
+ private static function get_latest_write_for_file( $file ) {
349
+
350
+ $line = self::tail( $file );
351
+
352
+ if ( ! $line ) {
353
+ return false;
354
+ }
355
+
356
+ return self::get_date( $line );
357
+ }
358
+
359
+ private static function get_date( $line ) {
360
+ if ( ! $parsed = self::parse_csv_line( $line ) ) {
361
+ return false;
362
+ }
363
+
364
+ if ( ! isset( $parsed[5] ) ) {
365
+ return false;
366
+ }
367
+
368
+ return strtotime( $parsed[5] );
369
+ }
370
+
371
+ private static function parse_csv_line( $line ) {
372
+
373
+ if ( function_exists( 'str_getcsv' ) ) {
374
+ return str_getcsv( $line );
375
+ }
376
+
377
+ require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-file.php' );
378
+
379
+ if ( ! function_exists( 'wp_tempnam' ) ) {
380
+ require_once( ABSPATH . 'wp-admin/includes/file.php' );
381
+ }
382
+
383
+ $temp = wp_tempnam();
384
+
385
+ $success = ITSEC_Lib_File::write( $temp, $line );
386
+
387
+ if ( true !== $success ) {
388
+ return false;
389
+ }
390
+
391
+ if ( ! $fh = fopen( $temp, 'rb' ) ) {
392
+ return false;
393
+ }
394
+
395
+ $parsed = fgetcsv( $fh );
396
+
397
+ if ( ! is_array( $parsed ) ) {
398
+ return false;
399
+ }
400
+
401
+ return $parsed;
402
+ }
403
+
404
+ /**
405
+ * Get the last n lines of a file.
406
+ *
407
+ * @link https://www.geekality.net/2011/05/28/php-tail-tackling-large-files/
408
+ *
409
+ * @param string $filename
410
+ * @param int $lines
411
+ * @param int $buffer
412
+ *
413
+ * @return bool|string
414
+ */
415
+ private static function tail( $filename, $lines = 1, $buffer = 4096 ) {
416
+ // Open the file
417
+ $f = fopen( $filename, "rb" );
418
+
419
+ // Jump to last character
420
+ fseek( $f, - 1, SEEK_END );
421
+
422
+ // Read it and adjust line number if necessary
423
+ // (Otherwise the result would be wrong if file doesn't end with a blank line)
424
+ if ( fread( $f, 1 ) !== "\n" ) {
425
+ -- $lines;
426
+ }
427
+
428
+ // Start reading
429
+ $output = '';
430
+ $chunk = '';
431
+
432
+ // While we would like more
433
+ while ( ftell( $f ) > 0 && $lines >= 0 ) {
434
+ // Figure out how far back we should jump
435
+ $seek = min( ftell( $f ), $buffer );
436
+
437
+ // Do the jump (backwards, relative to where we are)
438
+ fseek( $f, - $seek, SEEK_CUR );
439
+
440
+ // Read a chunk and prepend it to our output
441
+ $output = ( $chunk = fread( $f, $seek ) ) . $output;
442
+
443
+ // Jump back to where we started reading
444
+ fseek( $f, - mb_strlen( $chunk, '8bit' ), SEEK_CUR );
445
+
446
+ // Decrease our line counter
447
+ $lines -= substr_count( $chunk, "\n" );
448
+ }
449
+
450
+ // While we have too many lines
451
+ // (Because of buffer size we might have read too many)
452
+ while ( $lines ++ < 0 ) {
453
+ // Find first newline and remove all text before that
454
+ $output = substr( $output, strpos( $output, "\n" ) + 1 );
455
+ }
456
+
457
+ // Close file and return
458
+ fclose( $f );
459
+
460
+ return $output;
461
+ }
462
  }
463
 
464
  add_action( 'itsec_scheduler_register_events', array( 'ITSEC_Log', 'register_events' ) );
core/lib/schema.php CHANGED
@@ -72,6 +72,17 @@ CREATE TABLE {$wpdb->base_prefix}itsec_temp (
72
  KEY temp_host (temp_host),
73
  KEY temp_user (temp_user),
74
  KEY temp_username (temp_username)
 
 
 
 
 
 
 
 
 
 
 
75
  ) $charset_collate;";
76
 
77
  require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
72
  KEY temp_host (temp_host),
73
  KEY temp_user (temp_user),
74
  KEY temp_username (temp_username)
75
+ ) $charset_collate;
76
+
77
+ CREATE TABLE {$wpdb->base_prefix}itsec_distributed_storage (
78
+ storage_id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
79
+ storage_group varchar(40) NOT NULL,
80
+ storage_key varchar(40) NOT NULL default '',
81
+ storage_chunk int NOT NULL default 0,
82
+ storage_data longtext NOT NULL,
83
+ storage_updated datetime NOT NULL,
84
+ PRIMARY KEY (storage_id),
85
+ UNIQUE KEY storage_group__key__chunk (storage_group,storage_key,storage_chunk)
86
  ) $charset_collate;";
87
 
88
  require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
core/modules/away-mode/class-itsec-away-mode.php CHANGED
@@ -8,9 +8,10 @@ final class ITSEC_Away_Mode {
8
  add_action( 'itsec_admin_init', array( $this, 'run_active_check' ) );
9
  add_action( 'login_init', array( $this, 'run_active_check' ) );
10
 
 
 
11
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
12
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
13
-
14
  }
15
 
16
  /**
@@ -88,6 +89,22 @@ final class ITSEC_Away_Mode {
88
  }
89
  }
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  /**
92
  * Register verbs for Sync.
93
  *
8
  add_action( 'itsec_admin_init', array( $this, 'run_active_check' ) );
9
  add_action( 'login_init', array( $this, 'run_active_check' ) );
10
 
11
+ add_filter( 'itsec_managed_files', array( $this, 'register_managed_file' ) );
12
+
13
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
14
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
 
15
  }
16
 
17
  /**
89
  }
90
  }
91
 
92
+ /**
93
+ * Register the away mode file as a managed file.
94
+ *
95
+ * @param array $files
96
+ *
97
+ * @return array
98
+ */
99
+ public function register_managed_file( $files ) {
100
+
101
+ require_once( dirname( __FILE__ ) . '/utilities.php' );
102
+
103
+ $files[] = ITSEC_Away_Mode_Utilities::get_active_file_name();
104
+
105
+ return $files;
106
+ }
107
+
108
  /**
109
  * Register verbs for Sync.
110
  *
core/modules/backup/class-itsec-backup.php CHANGED
@@ -222,6 +222,8 @@ class ITSEC_Backup {
222
 
223
  if ( 2 !== $this->settings['method'] || true === $one_time ) {
224
  $mail_success = $this->send_mail( $file );
 
 
225
  }
226
 
227
  if ( 1 === $this->settings['method'] ) {
222
 
223
  if ( 2 !== $this->settings['method'] || true === $one_time ) {
224
  $mail_success = $this->send_mail( $file );
225
+ } else {
226
+ $mail_success = null;
227
  }
228
 
229
  if ( 1 === $this->settings['method'] ) {
core/modules/backup/privacy.php ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Backup_Privacy {
4
+ private $settings;
5
+
6
+ public function __construct() {
7
+ $this->settings = ITSEC_Modules::get_settings( 'backup' );
8
+
9
+ add_filter( 'itsec_get_privacy_policy_for_retention', array( $this, 'get_privacy_policy_for_retention' ) );
10
+ add_filter( 'itsec_get_privacy_policy_for_sending', array( $this, 'get_privacy_policy_for_sending' ) );
11
+ }
12
+
13
+ public function get_privacy_policy_for_retention( $policy ) {
14
+ $suggested_text = '<strong class="privacy-policy-tutorial">' . __( 'Suggested text:' ) . ' </strong>';
15
+
16
+ if ( $this->settings['enabled'] ) {
17
+ if ( 1 !== $this->settings['method'] ) {
18
+ $retention_days = $this->settings['interval'] * $this->settings['retain'];
19
+
20
+ if ( $retention_days > 0 ) {
21
+ /* Translators: 1: Number of days that backups are retained for */
22
+ $policy .= "<p>$suggested_text " . sprintf( esc_html__( 'Backups of security log details are retained for %1$d days.', 'better-wp-security' ), $retention_days ) . "</p>\n";
23
+ } else {
24
+ $policy .= "<p class=\"privacy-policy-tutorial\">" . esc_html__( 'Due to current settings, backups of security log details are retained indefinitely. If this is an issue for your site\'s compliance, you should change the settings in the Database Backups section of Security > Settings.', 'better-wp-security' ) . "</p>\n";
25
+ }
26
+ }
27
+
28
+ if ( 2 !== $this->settings['method'] ) {
29
+ $policy .= "<p class=\"privacy-policy-tutorial\">" . esc_html__( 'Database backups are sent via email. You may need to note what the retention policy is of those emails.', 'better-wp-security' ) . "</p>\n";
30
+ }
31
+
32
+ $policy .= "<p class=\"privacy-policy-tutorial\">" . esc_html__( 'Note that you may be required by some regulations to ensure that past personal data erasure requests are respected even in the event of restoring a backup of the site. You may need to set up an internal policy to ensure that previous personal data erasure requests are respected after restoring a database backup.', 'better-wp-security' ) . "</p>\n";
33
+ }
34
+
35
+ return $policy;
36
+ }
37
+
38
+ public function get_privacy_policy_for_sending( $policy ) {
39
+ if ( $this->settings['enabled'] && 2 !== $this->settings['method'] ) {
40
+ $policy .= "<p class=\"privacy-policy-tutorial\">" . esc_html__( 'Database backups are sent via email. Depending on who hosts your email and your site\'s compliance needs, you may need to note that this information is sent to that host and link to their privacy policy.', 'better-wp-security' ) . "</p>\n";
41
+ }
42
+
43
+ return $policy;
44
+ }
45
+ }
46
+ new ITSEC_Backup_Privacy();
core/modules/file-change/activate.php CHANGED
@@ -1,6 +1,4 @@
1
  <?php
2
 
3
- $split = ITSEC_Modules::get_setting( 'file-change', 'split', false );
4
- $interval = $split ? ITSEC_Scheduler::S_FOUR_DAILY : ITSEC_Scheduler::S_DAILY;
5
-
6
- ITSEC_Core::get_scheduler()->schedule( $interval, 'file-change' );
1
  <?php
2
 
3
+ require_once( dirname( __FILE__ ) . '/scanner.php' );
4
+ ITSEC_File_Change_Scanner::schedule_start( false );
 
 
core/modules/file-change/admin.php CHANGED
@@ -1,34 +1,49 @@
1
  <?php
2
 
3
  final class ITSEC_File_Change_Admin {
4
- private $script_version = 1;
 
 
 
5
  private $dismiss_nonce;
6
-
7
-
8
  public function __construct() {
9
- if ( ! ITSEC_Modules::get_setting( 'file-change', 'show_warning' ) ) {
10
- return;
 
11
  }
12
-
13
- add_action( 'init', array( $this, 'init' ) );
14
  }
15
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  public function init() {
17
- global $blog_id;
18
-
19
- if ( ( is_multisite() && ( 1 != $blog_id || ! current_user_can( 'manage_network_options' ) ) ) || ! current_user_can( 'activate_plugins' ) ) {
20
  return;
21
  }
22
-
23
-
24
- add_action( 'wp_ajax_itsec_file_change_dismiss_warning', array( $this, 'dismiss_file_change_warning' ) );
25
-
26
  if ( ! empty( $_GET['file_change_dismiss_warning'] ) ) {
27
  $this->dismiss_file_change_warning();
28
  } else {
29
  add_action( 'admin_print_scripts', array( $this, 'add_scripts' ) );
30
  $this->dismiss_nonce = wp_create_nonce( 'itsec-file-change-dismiss-warning' );
31
-
32
  if ( is_multisite() ) {
33
  add_action( 'network_admin_notices', array( $this, 'show_file_change_warning' ) );
34
  } else {
@@ -36,44 +51,67 @@ final class ITSEC_File_Change_Admin {
36
  }
37
  }
38
  }
39
-
40
  public function add_scripts() {
41
  $vars = array(
42
- 'ajax_action' => 'itsec_file_change_dismiss_warning',
43
  'ajax_nonce' => $this->dismiss_nonce
44
  );
45
-
46
- wp_enqueue_script( 'itsec-file-change-script', plugins_url( 'js/script.js', __FILE__ ), array(), $this->script_version, true );
47
  wp_localize_script( 'itsec-file-change-script', 'itsec_file_change', $vars );
48
  }
49
-
50
- public function dismiss_file_change_warning() {
51
- ini_set( 'display_errors', 1 );
52
-
53
  if ( ! wp_verify_nonce( $_REQUEST['nonce'], 'itsec-file-change-dismiss-warning' ) ) {
54
- die( 'Security check' );
 
 
55
  }
56
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  ITSEC_Modules::set_setting( 'file-change', 'show_warning', false );
58
  }
59
-
60
  public function show_file_change_warning() {
 
61
  $args = array(
62
  'file_change_dismiss_warning' => '1',
63
  'nonce' => $this->dismiss_nonce,
64
  );
65
-
66
- $dismiss_url = add_query_arg( $args, ITSEC_Core::get_settings_page_url() );
67
- $logs_url = ITSEC_Core::get_logs_page_url();
68
- $message = __( 'iThemes Security noticed file changes in your WordPress site. Please review the logs to make sure your system has not been compromised.', 'better-wp-security' );
69
-
70
- echo "<div id='itsec-file-change-warning-dialog' class='error'>\n";
71
- echo "<p>$message</p>\n";
72
- echo "<p>";
73
- echo "<a class='button-primary' href='" . esc_url( $logs_url ) . "'>" . __( 'View Logs', 'better-wp-security' ) . "</a> ";
74
- echo "<a id='itsec-file-change-dismiss-warning' class='button-secondary' href='" . esc_url( $dismiss_url ) . "'>" . __( 'Dismiss Warning', 'better-wp-security' ) . "</a>";
75
- echo "</p>\n";
76
- echo "</div>\n";
 
 
 
 
77
  }
78
  }
79
 
1
  <?php
2
 
3
  final class ITSEC_File_Change_Admin {
4
+
5
+ const AJAX = 'itsec_file_change_dismiss_warning';
6
+
7
+ private $script_version = 2;
8
  private $dismiss_nonce;
9
+
10
+
11
  public function __construct() {
12
+
13
+ if ( ITSEC_Modules::get_setting( 'file-change', 'show_warning' ) ) {
14
+ add_action( 'init', array( $this, 'init' ) );
15
  }
 
 
16
  }
17
+
18
+ public static function enqueue_scanner() {
19
+ $logs_page_url = ITSEC_Core::get_logs_page_url( 'file_change' );
20
+
21
+ ITSEC_Lib::enqueue_util();
22
+ wp_enqueue_script( 'itsec-file-change-scanner', plugins_url( 'js/file-scanner.js', __FILE__ ), array( 'jquery', 'heartbeat', 'itsec-util' ), ITSEC_Core::get_plugin_build(), true );
23
+ wp_localize_script( 'itsec-file-change-scanner', 'ITSECFileChangeScannerl10n', array(
24
+ 'button_text' => __( 'Scan Files Now', 'better-wp-security' ),
25
+ 'scanning_button_text' => __( 'Scanning...', 'better-wp-security' ),
26
+ 'no_changes' => __( 'No changes were detected.', 'better-wp-security' ),
27
+ 'found_changes' => sprintf( __( 'Changes were detected. Please check the <a href="%s" target="_blank" rel="noopener noreferrer">logs</a> for details.', 'better-wp-security' ), esc_url( add_query_arg( 'id', '#REPLACE_ID#', $logs_page_url ) ) ),
28
+ 'unknown_error' => __( 'An unknown error occured. Please try again later', 'better-wp-security' ),
29
+ 'already_running' => sprintf( __( 'A scan is already in progress. Please check the <a href="%s" target="_blank" rel="noopener noreferrer">logs page</a> at a later time for the results of the scan.', 'better-wp-security' ), esc_url( $logs_page_url ) ),
30
+ ) );
31
+ }
32
+
33
  public function init() {
34
+
35
+ if ( ! ITSEC_Core::current_user_can_manage() ) {
 
36
  return;
37
  }
38
+
39
+ add_action( 'wp_ajax_' . self::AJAX, array( $this, 'dismiss_file_change_warning_ajax' ) );
40
+
 
41
  if ( ! empty( $_GET['file_change_dismiss_warning'] ) ) {
42
  $this->dismiss_file_change_warning();
43
  } else {
44
  add_action( 'admin_print_scripts', array( $this, 'add_scripts' ) );
45
  $this->dismiss_nonce = wp_create_nonce( 'itsec-file-change-dismiss-warning' );
46
+
47
  if ( is_multisite() ) {
48
  add_action( 'network_admin_notices', array( $this, 'show_file_change_warning' ) );
49
  } else {
51
  }
52
  }
53
  }
54
+
55
  public function add_scripts() {
56
  $vars = array(
57
+ 'ajax_action' => self::AJAX,
58
  'ajax_nonce' => $this->dismiss_nonce
59
  );
60
+
61
+ wp_enqueue_script( 'itsec-file-change-script', plugins_url( 'js/script.js', __FILE__ ), array( 'jquery', 'common' ), $this->script_version, true );
62
  wp_localize_script( 'itsec-file-change-script', 'itsec_file_change', $vars );
63
  }
64
+
65
+ public function dismiss_file_change_warning_ajax() {
 
 
66
  if ( ! wp_verify_nonce( $_REQUEST['nonce'], 'itsec-file-change-dismiss-warning' ) ) {
67
+ wp_send_json_error( array(
68
+ 'message' => __( 'Request expired. Please refresh and try again.', 'better-wp-security' ),
69
+ ) );
70
  }
71
+
72
+ $status = ITSEC_Modules::set_setting( 'file-change', 'show_warning', false );
73
+
74
+ if ( ! $status || empty( $status['saved'] ) ) {
75
+ wp_send_json_error( array(
76
+ 'message' => __( 'Failed to dismiss warning.', 'better-wp-security' ),
77
+ ) );
78
+ }
79
+
80
+ wp_send_json_success( array(
81
+ 'message' => __( 'Warning dismissed.', 'better-wp-security' ),
82
+ ) );
83
+ }
84
+
85
+ public function dismiss_file_change_warning() {
86
+ if ( empty( $_REQUEST['nonce'] ) || ! wp_verify_nonce( $_REQUEST['nonce'], 'itsec-file-change-dismiss-warning' ) ) {
87
+ return;
88
+ }
89
+
90
  ITSEC_Modules::set_setting( 'file-change', 'show_warning', false );
91
  }
92
+
93
  public function show_file_change_warning() {
94
+
95
  $args = array(
96
  'file_change_dismiss_warning' => '1',
97
  'nonce' => $this->dismiss_nonce,
98
  );
99
+
100
+ if ( $log_id = ITSEC_Modules::get_setting( 'file-change', 'last_scan' ) ) {
101
+ $args['id'] = $log_id;
102
+ }
103
+
104
+ $logs_url = add_query_arg( $args, ITSEC_Core::get_logs_page_url() );
105
+ $message = sprintf(
106
+ esc_html__( 'iThemes Security noticed file changes in your WordPress site. Please %1$s review the logs %2$s to make sure your system has not been compromised.', 'better-wp-security' ),
107
+ '<a href="' . esc_url( $logs_url ) . '">',
108
+ '</a>'
109
+ );
110
+ ?>
111
+ <div id="itsec-file-change-warning-dialog" class="notice notice-error is-dismissible">
112
+ <p><?php echo $message; ?></p>
113
+ </div>
114
+ <?php
115
  }
116
  }
117
 
core/modules/file-change/class-itsec-file-change.php CHANGED
@@ -23,21 +23,79 @@ class ITSEC_File_Change {
23
  * @return void
24
  */
25
  function run() {
26
-
27
- add_action( 'itsec_execute_file_check_cron', array( $this, 'run_scan' ) ); //Action to execute during a cron run.
28
-
29
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
30
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
31
  add_filter( 'itsec_file-change_notification_strings', array( $this, 'register_notification_strings' ) );
32
 
 
 
 
 
 
33
  add_action( 'itsec_scheduler_register_events', array( $this, 'register_event' ) );
34
  add_action( 'itsec_scheduled_file-change', array( $this, 'run_scan' ) );
 
 
 
35
  }
36
 
37
- public function run_scan() {
38
  require_once( dirname( __FILE__ ) . '/scanner.php' );
39
 
40
- return ITSEC_File_Change_Scanner::run_scan();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
 
43
  /**
@@ -46,12 +104,8 @@ class ITSEC_File_Change {
46
  * @param ITSEC_Scheduler $scheduler
47
  */
48
  public function register_event( $scheduler ) {
49
-
50
- // If we're splitting the file check run it every 6 hours.
51
- $split = ITSEC_Modules::get_setting( 'file-change', 'split', false );
52
- $interval = $split ? ITSEC_Scheduler::S_FOUR_DAILY : ITSEC_Scheduler::S_DAILY;
53
-
54
- $scheduler->schedule( $interval, 'file-change' );
55
  }
56
 
57
  /**
@@ -63,6 +117,7 @@ class ITSEC_File_Change {
63
  */
64
  public function register_sync_verbs( $api ) {
65
  $api->register( 'itsec-perform-file-scan', 'Ithemes_Sync_Verb_ITSEC_Perform_File_Scan', dirname( __FILE__ ) . '/sync-verbs/itsec-perform-file-scan.php' );
 
66
  }
67
 
68
  /**
@@ -96,4 +151,87 @@ class ITSEC_File_Change {
96
  'subject' => esc_html__( 'File Change Warning', 'better-wp-security' ),
97
  );
98
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
23
  * @return void
24
  */
25
  function run() {
26
+ add_action( 'init', array( $this, 'health_check' ) );
 
 
27
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
28
  add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
29
  add_filter( 'itsec_file-change_notification_strings', array( $this, 'register_notification_strings' ) );
30
 
31
+ add_action( 'itsec_lib_write_to_file', array( $this, 'write_to_file' ) );
32
+ add_action( 'itsec_lib_delete_file', array( $this, 'delete_file' ) );
33
+
34
+ add_filter( 'heartbeat_received', array( $this, 'heartbeat' ), 10, 2 );
35
+
36
  add_action( 'itsec_scheduler_register_events', array( $this, 'register_event' ) );
37
  add_action( 'itsec_scheduled_file-change', array( $this, 'run_scan' ) );
38
+ add_action( 'itsec_scheduled_file-change-fast', array( $this, 'run_scan' ) );
39
+ ITSEC_Core::get_scheduler()->register_loop( 'file-change', ITSEC_Scheduler::S_DAILY, 60 );
40
+ ITSEC_Core::get_scheduler()->register_loop( 'file-change-fast', ITSEC_Scheduler::S_DAILY, 0 );
41
  }
42
 
43
+ public function run_scan( $job ) {
44
  require_once( dirname( __FILE__ ) . '/scanner.php' );
45
 
46
+ $scanner = new ITSEC_File_Change_Scanner();
47
+ $scanner->run( $job );
48
+ }
49
+
50
+ public function health_check() {
51
+
52
+ $storage = self::make_progress_storage();
53
+
54
+ if ( ! $health_check = $storage->health_check() ) {
55
+ return;
56
+ }
57
+
58
+ // No need to worry yet.
59
+ if ( $health_check + 300 > ITSEC_Core::get_current_time_gmt() ) {
60
+ return;
61
+ }
62
+
63
+ if ( ITSEC_Core::get_scheduler()->is_single_scheduled( $storage->get( 'id' ), null ) ) {
64
+ return;
65
+ }
66
+
67
+ require_once( dirname( __FILE__ ) . '/scanner.php' );
68
+ ITSEC_File_Change_Scanner::recover();
69
+ }
70
+
71
+ /**
72
+ * When iThemes Security writes to a file, store the file's hash so the change is not seen as unexpected.
73
+ *
74
+ * @param string $file
75
+ */
76
+ public function write_to_file( $file ) {
77
+ $hashes = ITSEC_Modules::get_setting( 'file-change', 'expected_hashes', array() );
78
+ $hash = @md5_file( $file );
79
+
80
+ if ( $hash ) {
81
+ $hashes[ $file ] = $hash;
82
+ ITSEC_Modules::set_setting( 'file-change', 'expected_hashes', $hashes );
83
+ }
84
+ }
85
+
86
+ /**
87
+ * When a file is deleted, remove its stored hash.
88
+ *
89
+ * @param string $file
90
+ */
91
+ public function delete_file( $file ) {
92
+ $hashes = ITSEC_Modules::get_setting( 'file-change', 'expected_hashes', array() );
93
+
94
+ if ( isset( $hashes[ $file ] ) ) {
95
+ unset( $hashes[ $file ] );
96
+
97
+ ITSEC_Modules::set_setting( 'file-change', 'expected_hashes', $hashes );
98
+ }
99
  }
100
 
101
  /**
104
  * @param ITSEC_Scheduler $scheduler
105
  */
106
  public function register_event( $scheduler ) {
107
+ require_once( dirname( __FILE__ ) . '/scanner.php' );
108
+ ITSEC_File_Change_Scanner::schedule_start( false, $scheduler );
 
 
 
 
109
  }
110
 
111
  /**
117
  */
118
  public function register_sync_verbs( $api ) {
119
  $api->register( 'itsec-perform-file-scan', 'Ithemes_Sync_Verb_ITSEC_Perform_File_Scan', dirname( __FILE__ ) . '/sync-verbs/itsec-perform-file-scan.php' );
120
+ $api->register( 'itsec-ping-file-scan', 'Ithemes_Sync_Verb_ITSEC_Ping_File_Scan', dirname( __FILE__ ) . '/sync-verbs/itsec-ping-file-scan.php' );
121
  }
122
 
123
  /**
151
  'subject' => esc_html__( 'File Change Warning', 'better-wp-security' ),
152
  );
153
  }
154
+
155
+ /**
156
+ * Add status about the currently running file scan.
157
+ *
158
+ * @param array $response
159
+ * @param array $data
160
+ *
161
+ * @return array
162
+ */
163
+ public function heartbeat( $response, $data ) {
164
+
165
+ if ( ! empty( $data['itsec_file_change_scan_status'] ) && ITSEC_Core::current_user_can_manage() ) {
166
+ require_once( dirname( __FILE__ ) . '/scanner.php' );
167
+
168
+ if ( ITSEC_Core::get_scheduler()->is_single_scheduled( 'file-change-fast', null ) ) {
169
+ ITSEC_Core::get_scheduler()->run_due_now();
170
+ }
171
+
172
+ $response['itsec_file_change_scan_status'] = ITSEC_File_Change_Scanner::get_status();
173
+ }
174
+
175
+ return $response;
176
+ }
177
+
178
+ /**
179
+ * Get the latest change list.
180
+ *
181
+ * @return array
182
+ */
183
+ public static function get_latest_changes() {
184
+ $changes = get_site_option( 'itsec_file_change_latest', array() );
185
+
186
+ if ( ! is_array( $changes ) ) {
187
+ $changes = array();
188
+ }
189
+
190
+ return $changes;
191
+ }
192
+
193
+ /**
194
+ * Make the progress torage container.
195
+ *
196
+ * @return ITSEC_Lib_Distributed_Storage
197
+ */
198
+ public static function make_progress_storage() {
199
+ return new ITSEC_Lib_Distributed_Storage( 'file-change-progress', array(
200
+ 'step' => array( 'default' => '' ),
201
+ 'chunk' => array( 'default' => '' ),
202
+ 'id' => array( 'default' => '' ),
203
+ 'data' => array( 'default' => array() ),
204
+ 'memory' => array( 'default' => 0 ),
205
+ 'memory_peak' => array( 'default' => 0 ),
206
+ 'process' => array( 'default' => array() ),
207
+ 'done_plugins' => array( 'default' => array() ),
208
+ 'max_severity' => array( 'default' => 0 ),
209
+ 'file_list' => array(
210
+ 'default' => array(),
211
+ 'split' => true,
212
+ 'chunk' => 1000,
213
+ 'serialize' => 'wp_json_encode',
214
+ 'unserialize' => 'ITSEC_File_Change::_json_decode_associative'
215
+ ),
216
+ 'files' => array(
217
+ 'default' => array(),
218
+ 'split' => true,
219
+ 'chunk' => 1000,
220
+ 'serialize' => 'wp_json_encode',
221
+ 'unserialize' => 'ITSEC_File_Change::_json_decode_associative'
222
+ ),
223
+ 'change_list' => array(
224
+ 'default' => array(
225
+ 'added' => array(),
226
+ 'changed' => array(),
227
+ 'removed' => array(),
228
+ ),
229
+ 'split' => true
230
+ ),
231
+ ) );
232
+ }
233
+
234
+ public static function _json_decode_associative( $value ) {
235
+ return json_decode( $value, true );
236
+ }
237
  }
core/modules/file-change/deactivate.php CHANGED
@@ -1,2 +1,3 @@
1
  <?php
2
- ITSEC_Core::get_scheduler()->unschedule( 'file-change' );
 
1
  <?php
2
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change', null );
3
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change-fast', null );
core/modules/file-change/js/file-scanner.js ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function ITSECFileChangeScanner( $el, options ) {
2
+
3
+ this.$el = $el;
4
+ this.options = jQuery.extend( {}, {
5
+ messageContainer: jQuery(),
6
+ scanningClass : 'itsec-is-scanning',
7
+ classList : null,
8
+ onStart : null,
9
+ onCancel : null,
10
+ onFinish : null,
11
+ onAbort : null,
12
+ l10n : window['ITSECFileChangeScannerl10n'],
13
+ }, options );
14
+
15
+ this.isRunning = false;
16
+ this.results = null;
17
+ this.deferred = null;
18
+ this.originalClass = $el.prop( 'class' );
19
+ this.originalHeartbeat = wp.heartbeat.interval();
20
+
21
+ jQuery( document ).on( 'heartbeat-send', (function ( e, d ) {
22
+ this.heartbeatSend( e, d );
23
+ }).bind( this ) );
24
+ jQuery( document ).on( 'heartbeat-tick', (function ( e, d ) {
25
+ this.heartbeatTick( e, d );
26
+ }).bind( this ) );
27
+ }
28
+
29
+ ITSECFileChangeScanner.prototype.start = function () {
30
+
31
+ var deferred = jQuery.Deferred();
32
+
33
+ if ( this.isRunning ) {
34
+ deferred.reject( { alreadyInProgress: true } );
35
+
36
+ return deferred.promise();
37
+ }
38
+
39
+ this.deferred = deferred;
40
+ this.$el.prop( 'disabled', true );
41
+
42
+ itsecUtil.sendModuleAJAXRequest( 'file-change', { method: 'one-time-scan' }, (function ( results ) {
43
+ this.options.messageContainer.html( '' );
44
+
45
+ if ( results.errors && results.errors.length > 0 ) {
46
+ $.each( results.errors, (function ( index, error ) {
47
+ this.message( error );
48
+ }).bind( this ) );
49
+ } else if ( !results.success ) {
50
+ this.message( this.options.l10n.unknown_error );
51
+ } else {
52
+ this.onStart();
53
+
54
+ return;
55
+ }
56
+
57
+ this.onStop();
58
+ deferred.reject( { cancelled: true } );
59
+ this.options.onCancel && this.options.onCancel( this );
60
+ }).bind( this ) );
61
+
62
+ return deferred.promise();
63
+ };
64
+
65
+ ITSECFileChangeScanner.prototype.heartbeatSend = function ( e, data ) {
66
+ if ( !data.itsec_file_change_scan_status ) {
67
+ data.itsec_file_change_scan_status = this.isRunning ? 1 : 0;
68
+ }
69
+ };
70
+
71
+ ITSECFileChangeScanner.prototype.heartbeatTick = function ( e, data ) {
72
+
73
+ if ( !data.itsec_file_change_scan_status || !this.isRunning ) {
74
+ return;
75
+ }
76
+
77
+ if ( data.itsec_file_change_scan_status.running ) {
78
+ this.status( data.itsec_file_change_scan_status.message );
79
+ } else if ( data.itsec_file_change_scan_status.complete ) {
80
+ this.status( data.itsec_file_change_scan_status.message );
81
+ this.onStop();
82
+
83
+ if ( data.itsec_file_change_scan_status.found_changes ) {
84
+ this.message( this.options.l10n.found_changes.replace( '#REPLACE_ID#', data.itsec_file_change_scan_status.found_changes ) );
85
+ } else {
86
+ this.message( this.options.l10n.no_changes, 'success' );
87
+ }
88
+ } else if ( data.itsec_file_change_scan_status.aborted ) {
89
+ this.message( data.itsec_file_change_scan_status.message );
90
+ this.onStop();
91
+ this.options.onAbort && this.options.onAbort( this );
92
+ this.deferred.reject( { aborted: true } );
93
+ } else {
94
+ this.onStop();
95
+ this.options.onFinish && this.options.onFinish( this );
96
+ this.deferred.resolve( data.itsec_file_change_scan_status );
97
+ }
98
+ };
99
+
100
+ ITSECFileChangeScanner.prototype.onStart = function () {
101
+
102
+ if ( this.options.classList ) {
103
+ this.$el.prop( 'class', this.options.classList );
104
+ } else {
105
+ this.$el.addClass( this.options.scanningClass );
106
+ }
107
+
108
+ this.$el.prop( 'disabled', true );
109
+
110
+ this.isRunning = true;
111
+ this.options.onStart && this.options.onStart( this );
112
+ this.status( this.options.l10n.scanning_button_text );
113
+ wp.heartbeat.interval( 'fast' );
114
+ };
115
+
116
+ ITSECFileChangeScanner.prototype.onStop = function () {
117
+
118
+ if ( this.options.classList ) {
119
+ this.$el.prop( 'class', this.originalClass );
120
+ } else {
121
+ this.$el.removeClass( this.options.scanningClass );
122
+ }
123
+
124
+ this.$el.prop( 'disabled', false );
125
+ this.status( this.options.l10n.button_text );
126
+ this.isRunning = false;
127
+ wp.heartbeat.interval( this.originalHeartbeat );
128
+ };
129
+
130
+ ITSECFileChangeScanner.prototype.status = function ( message ) {
131
+ if ( this.$el.is( 'input' ) ) {
132
+ this.$el.val( message );
133
+ } else {
134
+ this.$el.text( message );
135
+ }
136
+ };
137
+
138
+ ITSECFileChangeScanner.prototype.message = function ( message, type ) {
139
+ type = type || 'error';
140
+
141
+ var $notice = jQuery( '<div class="notice notice-alt inline"><p></p></div>' );
142
+ $notice.addClass( 'notice-' + type );
143
+
144
+ if ( type === 'success' ) {
145
+ $notice.addClass( 'fade' );
146
+ }
147
+
148
+ jQuery( 'p', $notice ).html( message );
149
+
150
+ this.options.messageContainer.append( $notice );
151
+ };
152
+
153
+ window.ITSECFileChangeScanner = ITSECFileChangeScanner;
core/modules/file-change/js/script.js CHANGED
@@ -1,14 +1,36 @@
1
- jQuery( document ).ready(function( $ ) {
2
- $( '#itsec-file-change-dismiss-warning' ).click(function( e ) {
 
 
 
 
 
 
3
  e.preventDefault();
4
-
5
- $( '#itsec-file-change-warning-dialog' ).hide();
6
-
 
 
 
7
  var data = {
8
- 'action': itsec_file_change.ajax_action,
9
- 'nonce': itsec_file_change.ajax_nonce,
10
  };
11
-
12
- jQuery.post( ajaxurl, data );
13
- });
14
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ jQuery( document ).ready( function ( $ ) {
2
+
3
+ var $notice = $( '#itsec-file-change-warning-dialog' ),
4
+ $button = $('.notice-dismiss', $notice);
5
+
6
+ $button.off( 'click.wp-dismiss-notice' );
7
+
8
+ $button.click( function ( e ) {
9
  e.preventDefault();
10
+
11
+ var $button = $( this );
12
+
13
+ $button.prop( 'disabled', true );
14
+ $button.css( 'opacity', .5 );
15
+
16
  var data = {
17
+ action: itsec_file_change.ajax_action,
18
+ nonce : itsec_file_change.ajax_nonce,
19
  };
20
+
21
+ $.post( ajaxurl, data, function ( response ) {
22
+ $button.prop( 'disabled', false );
23
+ $button.css( 'opacity', 1 );
24
+
25
+ if ( response.success ) {
26
+ $notice.fadeTo( 100, 0, function () {
27
+ $notice.slideUp( 100, function () {
28
+ $notice.remove();
29
+ } );
30
+ } );
31
+ } else {
32
+ alert( response.data.message )
33
+ }
34
+ } );
35
+ } );
36
+ } );
core/modules/file-change/js/settings-page.js CHANGED
@@ -29,54 +29,6 @@ jQuery( document ).ready( function ( $ ) {
29
 
30
  initializeFileTrees();
31
 
32
- /**
33
- * Performs a one-time file scan
34
- */
35
- $( document ).on( 'click', '#itsec-file-change-one_time_check', function( e ) {
36
- e.preventDefault();
37
-
38
- //let user know we're working
39
- $( '#itsec-file-change-one_time_check' )
40
- .removeClass( 'button-primary' )
41
- .addClass( 'button-secondary' )
42
- .attr( 'value', itsec_file_change_settings.scanning_button_text )
43
- .prop( 'disabled', true );
44
-
45
- var data = {
46
- 'method': 'one-time-scan'
47
- };
48
-
49
- $( '#itsec_file_change_status' ).html( '' );
50
-
51
- itsecUtil.sendModuleAJAXRequest( 'file-change', data, function( results ) {
52
- $( '#itsec_file_change_status' ).html( '' );
53
-
54
- if ( false === results.response ) {
55
- $( '#itsec_file_change_status' ).append( '<div class="updated fade inline"><p><strong>' + itsec_file_change_settings.no_changes + '</strong></p></div>' );
56
- } else if ( true === results.response ) {
57
- $( '#itsec_file_change_status' ).append( '<div class="error inline"><p><strong>' + itsec_file_change_settings.found_changes + '</strong></p></div>' );
58
- } else if ( -1 === results.response ) {
59
- $( '#itsec_file_change_status' ).append( '<div class="error inline"><p><strong>' + itsec_file_change_settings.already_running + '</strong></p></div>' );
60
- } else if ( results.errors && results.errors.length > 0 ) {
61
- $.each( results.errors, function( index, error ) {
62
- $( '#itsec_file_change_status' ).append( '<div class="error inline"><p><strong>' + error + '</strong></p></div>' );
63
- } );
64
- } else {
65
- $( '#itsec_file_change_status' ).append( '<div class="error inline"><p><strong>' + itsec_file_change_settings.unknown_error + '</strong></p></div>' );
66
- }
67
-
68
- $( '#itsec-file-change-one_time_check' )
69
- .removeClass( 'button-secondary' )
70
- .addClass( 'button-primary' )
71
- .attr( 'value', itsec_file_change_settings.button_text )
72
- .prop( 'disabled', false );
73
- } );
74
- });
75
-
76
- } );
77
-
78
- jQuery( window ).load( function () {
79
-
80
  /**
81
  * Shows and hides the red selector icon on the file tree allowing users to select an
82
  * individual element.
@@ -95,4 +47,19 @@ jQuery( window ).load( function () {
95
 
96
  } );
97
 
98
- } );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  initializeFileTrees();
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  /**
33
  * Shows and hides the red selector icon on the file tree allowing users to select an
34
  * individual element.
47
 
48
  } );
49
 
50
+ itsecSettingsPage.events.on( 'modulesReloaded', initializeScan );
51
+
52
+ function initializeScan() {
53
+ var $button = $( '#itsec-file-change-one_time_check' );
54
+ var scan = window.scan = new window.ITSECFileChangeScanner( $button, {
55
+ classList : 'button-secondary',
56
+ messageContainer: $( '#itsec_file_change_status' ),
57
+ } );
58
+
59
+ $button.on( 'click', function () {
60
+ scan.start();
61
+ } );
62
+ }
63
+
64
+ initializeScan();
65
+ } );
core/modules/file-change/lib/chunk-scanner.php ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ require_once( ABSPATH . 'wp-admin/includes/file.php' );
4
+
5
+ class ITSEC_File_Change_Chunk_Scanner {
6
+
7
+ /** @var array */
8
+ private $chunks;
9
+
10
+ /** @var string */
11
+ private $directory;
12
+
13
+ /** @var array */
14
+ private $excludes;
15
+
16
+ /** @var array */
17
+ private $settings;
18
+
19
+ /** @var array */
20
+ private $file_list;
21
+
22
+ /**
23
+ * ITSEC_File_Change_Chunk_Scanner constructor.
24
+ *
25
+ * @param array $settings
26
+ * @param array $chunks
27
+ */
28
+ public function __construct( $settings, $chunks = array() ) {
29
+
30
+ $home = get_home_path();
31
+
32
+ if ( ! $chunks ) {
33
+ $upload = ITSEC_Core::get_wp_upload_dir();
34
+
35
+ $chunks = array(
36
+ ITSEC_File_Change_Scanner::C_ADMIN => ABSPATH . 'wp-admin',
37
+ ITSEC_File_Change_Scanner::C_INCLUDES => ABSPATH . WPINC,
38
+ ITSEC_File_Change_Scanner::C_CONTENT => WP_CONTENT_DIR,
39
+ ITSEC_File_Change_Scanner::C_UPLOADS => $upload['basedir'],
40
+ ITSEC_File_Change_Scanner::C_THEMES => WP_CONTENT_DIR . '/themes',
41
+ ITSEC_File_Change_Scanner::C_PLUGINS => WP_PLUGIN_DIR,
42
+ ITSEC_File_Change_Scanner::C_OTHERS => untrailingslashit( $home ),
43
+ );
44
+ }
45
+
46
+ $this->chunks = $chunks;
47
+ $this->settings = $settings;
48
+
49
+ $this->excludes[] = ITSEC_Modules::get_setting( 'backup', 'location' );
50
+ $this->excludes[] = ITSEC_Modules::get_setting( 'global', 'log_location' );
51
+
52
+ foreach ( $settings['file_list'] as $file ) {
53
+ $cleaned = untrailingslashit( $home . ltrim( $file, '/' ) );
54
+ $this->file_list[ $cleaned ] = 1;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Scan and get a list of all files in the given directory.
60
+ *
61
+ * @param string $chunk Chunk to scan.
62
+ * @param int $limit Top level directory limit.
63
+ * @param array $additional_excludes Additional exclusion rules for this scan. Must already be a full path.
64
+ *
65
+ * @return array
66
+ */
67
+ public function scan( $chunk, $limit = - 1, $additional_excludes = array() ) {
68
+
69
+ if ( ! isset( $this->chunks[ $chunk ] ) ) {
70
+ return array();
71
+ }
72
+
73
+ $excludes = $this->excludes;
74
+ $chunks = $this->chunks;
75
+ $file_list = $this->file_list;
76
+
77
+ $this->directory = $chunks[ $chunk ];
78
+ unset( $chunks[ $chunk ] );
79
+ $this->excludes = array_merge( $this->excludes, array_values( $chunks ) );
80
+
81
+ foreach ( $additional_excludes as $exclude ) {
82
+ $this->file_list[ untrailingslashit( $exclude ) ] = 1;
83
+ }
84
+
85
+ do_action( 'itsec-file-change-start-scan' );
86
+ $current_files = $this->get_files( $this->directory, $limit );
87
+ do_action( 'itsec-file-change-end-scan' );
88
+
89
+ $this->excludes = $excludes;
90
+ $this->directory = null;
91
+ $this->file_list = $file_list;
92
+
93
+ return $current_files;
94
+ }
95
+
96
+ /**
97
+ * Recursively find files in a given directory and calculate their checksums.
98
+ *
99
+ * @param string $path Path to search in.
100
+ * @param int $limit
101
+ *
102
+ * @return array
103
+ */
104
+ private function get_files( $path, $limit = - 1 ) {
105
+
106
+ if ( in_array( $path, $this->excludes, true ) ) {
107
+ return array();
108
+ }
109
+
110
+ if ( false === ( $dh = @opendir( $path ) ) ) {
111
+ return array();
112
+ }
113
+
114
+ $data = array();
115
+ $dirs = array();
116
+
117
+ while ( false !== ( $item = @readdir( $dh ) ) ) {
118
+
119
+ if ( '.' === $item || '..' === $item ) {
120
+ continue;
121
+ }
122
+
123
+ $filename = "{$path}/{$item}";
124
+
125
+ if ( isset( $this->file_list[ $filename ] ) ) {
126
+ continue;
127
+ }
128
+
129
+ if ( is_dir( $filename ) && 'dir' === filetype( $filename ) ) {
130
+ if ( $nested = $this->get_files( $filename ) ) {
131
+ $dirs[] = $nested;
132
+ }
133
+ } elseif ( ! in_array( '.' . pathinfo( $item, PATHINFO_EXTENSION ), $this->settings['types'], true ) ) {
134
+ $data[ $filename ] = array(
135
+ 'd' => @filemtime( $filename ),
136
+ 'h' => @md5_file( $filename ),
137
+ );
138
+ }
139
+
140
+ if ( $limit === count( $dirs ) ) {
141
+ break;
142
+ }
143
+ }
144
+
145
+ if ( $dirs ) {
146
+ $dirs[] = $data;
147
+ $data = call_user_func_array( 'array_merge', $dirs );
148
+ }
149
+
150
+ @closedir( $dh );
151
+
152
+ return $data;
153
+ }
154
+ }
core/modules/file-change/lib/hash-comparator-chain.php ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Class ITSEC_File_Change_Hash_Comparator_Chain
4
+ */
5
+ class ITSEC_File_Change_Hash_Comparator_Chain implements ITSEC_File_Change_Hash_Comparator_Loadable {
6
+
7
+ /** @var ITSEC_File_Change_Hash_Comparator[] */
8
+ private $chain;
9
+
10
+ /** @var ITSEC_File_Change_Package */
11
+ private $package;
12
+
13
+ /**
14
+ * ITSEC_File_Change_Hash_Comparator_Chain constructor.
15
+ *
16
+ * @param ITSEC_File_Change_Hash_Comparator[] $chain
17
+ */
18
+ public function __construct( array $chain ) {
19
+ $this->chain = $chain;
20
+ }
21
+
22
+ /**
23
+ * Get all the comparators that support a package.
24
+ *
25
+ * @param ITSEC_File_Change_Package $package
26
+ *
27
+ * @return ITSEC_File_Change_Hash_Comparator[]
28
+ */
29
+ private function for_package( ITSEC_File_Change_Package $package ) {
30
+ $this->package = $package;
31
+
32
+ $supported = array();
33
+
34
+ foreach ( $this->chain as $comparator ) {
35
+ if ( $comparator->supports_package( $package ) ) {
36
+ $supported[] = $comparator;
37
+ }
38
+ }
39
+
40
+ usort( $supported, array( $this, '_sort' ) );
41
+ $this->package = null;
42
+
43
+ return $supported;
44
+ }
45
+
46
+ private function _sort( $a, $b ) {
47
+
48
+ $a_loadable = $a instanceof ITSEC_File_Change_Hash_Comparator_Loadable;
49
+ $b_loadable = $b instanceof ITSEC_File_Change_Hash_Comparator_Loadable;
50
+
51
+ if ( $a_loadable && ! $b_loadable ) {
52
+ return 1;
53
+ } elseif ( ! $a_loadable && $b_loadable ) {
54
+ return - 1;
55
+ } elseif ( $a_loadable && $b_loadable ) {
56
+ return ( $a->get_load_cost( $this->package ) - $b->get_load_cost( $this->package ) );
57
+ }
58
+
59
+ return 0;
60
+ }
61
+
62
+ /**
63
+ * @inheritdoc
64
+ */
65
+ public function supports_package( ITSEC_File_Change_Package $package ) {
66
+
67
+ foreach ( $this->chain as $comparator ) {
68
+ if ( $comparator->supports_package( $package ) ) {
69
+ return true;
70
+ }
71
+ }
72
+
73
+ return false;
74
+ }
75
+
76
+ /**
77
+ * @inheritdoc
78
+ */
79
+ public function has_hash( $relative_path, ITSEC_File_Change_Package $package ) {
80
+
81
+ foreach ( $this->chain as $comparator ) {
82
+ if ( $comparator->supports_package( $package ) && $comparator->has_hash( $relative_path, $package ) ) {
83
+ return true;
84
+ }
85
+ }
86
+
87
+ return false;
88
+ }
89
+
90
+ /**
91
+ * @inheritdoc
92
+ */
93
+ public function hash_matches( $actual_hash, $relative_path, ITSEC_File_Change_Package $package ) {
94
+
95
+ foreach ( $this->for_package( $package ) as $comparator ) {
96
+ if ( $comparator->has_hash( $relative_path, $package ) ) {
97
+ return $comparator->hash_matches( $actual_hash, $relative_path, $package );
98
+ }
99
+ }
100
+
101
+ return false;
102
+ }
103
+
104
+ /**
105
+ * @inheritdoc
106
+ */
107
+ public function load( ITSEC_File_Change_Package $package ) {
108
+ $e = null;
109
+
110
+ foreach ( $this->for_package( $package ) as $comparator ) {
111
+ if ( $comparator instanceof ITSEC_File_Change_Hash_Comparator_Loadable ) {
112
+ try {
113
+ $comparator->load( $package );
114
+ } catch ( ITSEC_File_Change_Hash_Loading_Failed_Exception $e ) {
115
+ continue;
116
+ }
117
+ }
118
+
119
+ return;
120
+ }
121
+
122
+ if ( $e ) {
123
+ throw $e;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * @inheritdoc
129
+ */
130
+ public function get_load_cost( ITSEC_File_Change_Package $package ) {
131
+ foreach ( $this->for_package( $package ) as $comparator ) {
132
+ if ( $comparator instanceof ITSEC_File_Change_Hash_Comparator_Loadable ) {
133
+ return $comparator->get_load_cost( $package );
134
+ }
135
+
136
+ return 0;
137
+ }
138
+
139
+ return 0;
140
+ }
141
+ }
core/modules/file-change/lib/hash-comparator-loadable.php ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ interface ITSEC_File_Change_Hash_Comparator_Loadable extends ITSEC_File_Change_Hash_Comparator {
4
+
5
+ const CACHED = 1;
6
+ const LOCAL = 2;
7
+ const EXTERNAL = 3;
8
+
9
+ /**
10
+ * Load the hashes for a package.
11
+ *
12
+ * @param ITSEC_File_Change_Package $package
13
+ *
14
+ * @return void
15
+ *
16
+ * @throws ITSEC_File_Change_Hash_Loading_Failed_Exception
17
+ */
18
+ public function load( ITSEC_File_Change_Package $package );
19
+
20
+ /**
21
+ * Get the cost to load the hashes.
22
+ *
23
+ * A higher number is slower.
24
+ *
25
+ * @param ITSEC_File_Change_Package $package
26
+ *
27
+ * @return int
28
+ */
29
+ public function get_load_cost( ITSEC_File_Change_Package $package );
30
+ }
core/modules/file-change/lib/hash-comparator-managed-files.php ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_File_Change_Hash_Comparator_Managed_Files
5
+ */
6
+ class ITSEC_File_Change_Hash_Comparator_Managed_Files implements ITSEC_File_Change_Hash_Comparator {
7
+
8
+ /** @var array */
9
+ private $hashes;
10
+
11
+ /**
12
+ * ITSEC_File_Change_Hash_Comparator_Managed_Files constructor.
13
+ */
14
+ public function __construct() {
15
+ $this->hashes = ITSEC_Modules::get_setting( 'file-change', 'expected_hashes', array() );
16
+ }
17
+
18
+ /**
19
+ * @inheritDoc
20
+ */
21
+ public function supports_package( ITSEC_File_Change_Package $package ) {
22
+ return $package instanceof ITSEC_File_Change_Package_System;
23
+ }
24
+
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ public function has_hash( $relative_path, ITSEC_File_Change_Package $package ) {
29
+ return isset( $this->hashes[ $package->get_root_path() . $relative_path ] );
30
+ }
31
+
32
+ /**
33
+ * @inheritDoc
34
+ */
35
+ public function hash_matches( $actual_hash, $relative_path, ITSEC_File_Change_Package $package ) {
36
+ return $this->hashes[ $package->get_root_path() . $relative_path ] === $actual_hash;
37
+ }
38
+ }
core/modules/file-change/lib/hash-comparator.php ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Interface ITSEC_File_Change_Hash_Comparator
5
+ */
6
+ interface ITSEC_File_Change_Hash_Comparator {
7
+
8
+ /**
9
+ * Does this comparator support hashes for a given package.
10
+ *
11
+ * For example, a comparator might only support iThemes Packages.
12
+ *
13
+ * @param ITSEC_File_Change_Package $package
14
+ *
15
+ * @return bool
16
+ */
17
+ public function supports_package( ITSEC_File_Change_Package $package );
18
+
19
+ /**
20
+ * Check if this comparator has an expected hash for the given file.
21
+ *
22
+ * @param string $relative_path Path relative to the root of the package.
23
+ * @param ITSEC_File_Change_Package $package
24
+ *
25
+ * @return bool
26
+ */
27
+ public function has_hash( $relative_path, ITSEC_File_Change_Package $package );
28
+
29
+ /**
30
+ * Check if the file's actual hash matches the expected hash.
31
+ *
32
+ * @param string $actual_hash The hash to compare against.
33
+ * @param string $relative_path Path relative to the root of the package.
34
+ * @param ITSEC_File_Change_Package $package
35
+ *
36
+ * @return bool
37
+ */
38
+ public function hash_matches( $actual_hash, $relative_path, ITSEC_File_Change_Package $package );
39
+ }
core/modules/file-change/lib/hash-loading-failed-exception.php ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_File_Change_Hash_Loading_Failed_Exception extends Exception {
4
+
5
+ /** @var ITSEC_File_Change_Package */
6
+ private $package;
7
+
8
+ /** @var ITSEC_File_Change_Hash_Comparator_Loadable */
9
+ private $comparator;
10
+
11
+ /**
12
+ * Create for a given package and loader.
13
+ *
14
+ * @param ITSEC_File_Change_Package $package
15
+ * @param ITSEC_File_Change_Hash_Comparator_Loadable $comparator
16
+ *
17
+ * @return ITSEC_File_Change_Hash_Loading_Failed_Exception
18
+ */
19
+ public static function create_for( ITSEC_File_Change_Package $package, ITSEC_File_Change_Hash_Comparator_Loadable $comparator ) {
20
+ $e = new self( sprintf(
21
+ /* translators: 1. The name of the comparator. 2. The name of the package, for example "iThemes Security Pro v4.5.0". */
22
+ __( 'The %1$s comparator failed to load hashes for %2$s.', 'better-wp-security' ),
23
+ get_class( $comparator ),
24
+ $package
25
+ ) );
26
+
27
+ $e->package = $package;
28
+ $e->comparator = $comparator;
29
+
30
+ return $e;
31
+ }
32
+
33
+ /**
34
+ * Get the package whose hashes were loaded.
35
+ *
36
+ * @return ITSEC_File_Change_Package
37
+ */
38
+ public function get_package() {
39
+ return $this->package;
40
+ }
41
+
42
+ /**
43
+ * Get the hash comparator that could not load the hashes.
44
+ *
45
+ * @return ITSEC_File_Change_Hash_Comparator_Loadable
46
+ */
47
+ public function get_comparator() {
48
+ return $this->comparator;
49
+ }
50
+ }
core/modules/file-change/lib/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/file-change/lib/package-core.php ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_File_Change_Package_Core
5
+ */
6
+ class ITSEC_File_Change_Package_Core implements ITSEC_File_Change_Package {
7
+
8
+ /** @var string */
9
+ private $root;
10
+
11
+ /**
12
+ * ITSEC_File_Change_Package_Core constructor.
13
+ *
14
+ * @param string $root
15
+ */
16
+ public function __construct( $root ) { $this->root = $root; }
17
+
18
+ /**
19
+ * @inheritdoc
20
+ */
21
+ public function get_root_path() {
22
+ return $this->root;
23
+ }
24
+
25
+ /**
26
+ * @inheritdoc
27
+ */
28
+ public function get_version() {
29
+ return $GLOBALS['wp_version'];
30
+ }
31
+
32
+ /**
33
+ * @inheritdoc
34
+ */
35
+ public function get_type() {
36
+ return 'core';
37
+ }
38
+
39
+ /**
40
+ * @inheritdoc
41
+ */
42
+ public function get_identifier() {
43
+ return 'core';
44
+ }
45
+
46
+ /**
47
+ * @inheritdoc
48
+ */
49
+ public function __toString() {
50
+ return sprintf( __( 'WordPress Core %s', 'better-wp-security' ), 'v' . $this->get_version() );
51
+ }
52
+ }
core/modules/file-change/lib/package-factory.php ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_File_Change_Package_Factory
5
+ */
6
+ class ITSEC_File_Change_Package_Factory {
7
+
8
+ private $search_paths;
9
+
10
+ /**
11
+ * ITSEC_File_Change_Package_Factory constructor.
12
+ */
13
+ public function __construct() {
14
+
15
+ global $wp_theme_directories;
16
+
17
+ $sp = array(
18
+ WP_PLUGIN_DIR . '/' => 'plugin',
19
+ ABSPATH . WPINC . '/' => 'core',
20
+ ABSPATH . 'wp-admin/' => 'core',
21
+ );
22
+
23
+
24
+ if ( empty( $wp_theme_directories ) ) {
25
+ $sp[ WP_CONTENT_DIR . '/themes/' ] = 'theme';
26
+ } else {
27
+ foreach ( $wp_theme_directories as $theme_directory ) {
28
+ $sp[ trailingslashit( $theme_directory ) ] = 'theme';
29
+ }
30
+ }
31
+
32
+ $core_files = '@' . preg_quote( ABSPATH, '@' ) . '[\w-_]+\.@';
33
+ $sp[ $core_files ] = 'core';
34
+
35
+ uksort( $sp, array( $this, '_sort' ) );
36
+
37
+ foreach ( $this->get_system_files() as $file ) {
38
+ $sp[ $file ] = 'system-files';
39
+ }
40
+
41
+ $this->search_paths = array_reverse( $sp );
42
+ }
43
+
44
+ /**
45
+ * Sort a list of paths to that the most precise paths are first.
46
+ *
47
+ * @param string $a
48
+ * @param string $b
49
+ *
50
+ * @return int
51
+ */
52
+ private function _sort( $a, $b ) {
53
+ return substr_count( $a, '/' ) - substr_count( $b, '/' );
54
+ }
55
+
56
+ /**
57
+ * Get the system files to track.
58
+ *
59
+ * @return array
60
+ */
61
+ private function get_system_files() {
62
+
63
+ $files = array(
64
+ ITSEC_Lib::get_htaccess(),
65
+ ITSEC_Lib::get_config(),
66
+ );
67
+
68
+ /**
69
+ * The list of files that iThemes Security manages.
70
+ *
71
+ * @param string[] $files
72
+ */
73
+ return apply_filters( 'itsec_managed_files', $files );
74
+ }
75
+
76
+ /**
77
+ * Find all packages from the set of files added or changed.
78
+ *
79
+ * @param iterable $files
80
+ *
81
+ * @return array[]
82
+ */
83
+ public function find_packages_for_files( $files ) {
84
+
85
+ $packages = array();
86
+ $append = array();
87
+
88
+ $skipped_files = array();
89
+
90
+ foreach ( $files as $file => $attr ) {
91
+ $found = false;
92
+
93
+ foreach ( $this->search_paths as $search_path => $type ) {
94
+
95
+ if ( '@' === $search_path[0] ) {
96
+ if ( ! preg_match( $search_path, $file ) ) {
97
+ continue;
98
+ }
99
+ } elseif ( 0 !== strpos( $file, $search_path ) ) {
100
+ continue;
101
+ }
102
+
103
+ if ( isset( $packages[ $search_path ] ) ) {
104
+ $package = $packages[ $search_path ]['package'];
105
+ } elseif ( ! $package = $this->make( $file, $search_path, $packages ) ) {
106
+ break;
107
+ }
108
+
109
+ // Ugly specific exemption so that single-file plugins don't end up getting matched
110
+ // for all further plugins because their root path is the plugins directory.
111
+ if ( 'plugin' === $type && $package->get_root_path() === $search_path ) {
112
+ $append[] = array(
113
+ 'package' => $package,
114
+ 'files' => array( $this->make_relative( $file, $package->get_root_path() ) => $attr ),
115
+ );
116
+ $found = true;
117
+ break;
118
+ }
119
+
120
+ if ( isset( $packages[ $package->get_root_path() ] ) ) {
121
+ $packages[ $package->get_root_path() ]['files'][ $this->make_relative( $file, $package->get_root_path() ) ] = $attr;
122
+ } else {
123
+ $packages[ $package->get_root_path() ] = array(
124
+ 'package' => $package,
125
+ 'files' => array( $this->make_relative( $file, $package->get_root_path() ) => $attr ),
126
+ );
127
+ }
128
+
129
+ $found = true;
130
+ break;
131
+ }
132
+
133
+ if ( ! $found ) {
134
+ $skipped_files[ $file ] = $attr;
135
+ }
136
+ }
137
+
138
+ if ( $skipped_files ) {
139
+ $unknown = array();
140
+
141
+ foreach ( $skipped_files as $file => $attr ) {
142
+ $unknown[ $this->make_relative( $file, '/' ) ] = $attr;
143
+ }
144
+
145
+ $packages['/'] = array( 'package' => new ITSEC_File_Change_Package_Unknown(), 'files' => $unknown );
146
+ }
147
+
148
+ return array_merge( array_values( $packages ), $append );
149
+ }
150
+
151
+ /**
152
+ * Make an absolute path relative.
153
+ *
154
+ * @param string $absolute Absolute path.
155
+ * @param string $to Path to make relative to.
156
+ *
157
+ * @return string
158
+ */
159
+ private function make_relative( $absolute, $to ) {
160
+ return ltrim( substr( $absolute, strlen( trailingslashit( $to ) ) ), '/' );
161
+ }
162
+
163
+ /**
164
+ * Get all installed plugins.
165
+ *
166
+ * @return array
167
+ */
168
+ private function get_plugins() {
169
+ if ( ! function_exists( 'get_plugins' ) ) {
170
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
171
+ }
172
+
173
+ if ( ! function_exists( 'get_plugins' ) ) {
174
+ return array();
175
+ }
176
+
177
+ // WordPress caches this internally.
178
+ return get_plugins();
179
+ }
180
+
181
+ /**
182
+ * Find a plugin file by a slug.
183
+ *
184
+ * @param string $slug
185
+ *
186
+ * @return array Tuple of the file path and file headers.
187
+ */
188
+ private function find_plugin_by_slug( $slug ) {
189
+
190
+ $plugins = $this->get_plugins();
191
+
192
+ foreach ( $plugins as $file => $data ) {
193
+ // Comparison is faster, but vast majority of plugins installed are not single file plugins.
194
+ if ( 0 === strpos( $file, $slug . '/' ) || $file === $slug ) {
195
+ return array( $file, $data );
196
+ }
197
+ }
198
+
199
+ return array( '', '' );
200
+ }
201
+
202
+ /**
203
+ * Filter the package.
204
+ *
205
+ * @param ITSEC_File_Change_Package|null $package
206
+ * @param string $file
207
+ * @param string $search_path
208
+ *
209
+ * @return ITSEC_File_Change_Package|null
210
+ */
211
+ private function filter( ITSEC_File_Change_Package $package = null, $file, $search_path ) {
212
+
213
+ /**
214
+ * Filter the corresponding package for a file.
215
+ *
216
+ * @param ITSEC_File_Change_Package|null $package
217
+ * @param string $file The absolute path to the file.
218
+ * @param string $search_path The search path this file was found in.
219
+ */
220
+ $filtered = apply_filters( 'itsec_file_change_package', $package, $file, $search_path );
221
+
222
+ if ( null === $filtered || $filtered instanceof ITSEC_File_Change_Package ) {
223
+ return $filtered;
224
+ }
225
+
226
+ return $package;
227
+ }
228
+
229
+ /**
230
+ * Make a package for a file.
231
+ *
232
+ * @param string $file The absolute path to the file.
233
+ * @param string $search_path The search path this file was found in.
234
+ * @param array $packages Packages that have already been found. Keyed by the theme root.
235
+ *
236
+ * @return ITSEC_File_Change_Package|null
237
+ */
238
+ private function make( $file, $search_path, array $packages ) {
239
+
240
+ $package = null;
241
+
242
+ switch ( $this->search_paths[ $search_path ] ) {
243
+ case 'plugin':
244
+ if ( ! $directory = $this->get_first_directory( $file, $search_path ) ) {
245
+ break;
246
+ }
247
+
248
+ if ( isset( $packages[ $root_path = $search_path . $directory . '/' ] ) ) {
249
+ return $packages[ $root_path ]['package']; // Don't filter multiple times if we already have the correct package.
250
+ }
251
+
252
+ list( $plugin_file, $plugin_data ) = $this->find_plugin_by_slug( $directory );
253
+
254
+ if ( $plugin_file ) {
255
+ $package = new ITSEC_File_Change_Package_Plugin( $plugin_file, $plugin_data );
256
+ }
257
+ break;
258
+ case 'theme':
259
+ if ( ! $directory = $this->get_first_directory( $file, $search_path ) ) {
260
+ break;
261
+ }
262
+
263
+ if ( isset( $packages[ $root_path = $search_path . $directory . '/' ] ) ) {
264
+ return $packages[ $root_path ]['package'];
265
+ }
266
+
267
+ if ( ( ! $theme = wp_get_theme( $directory, untrailingslashit( $search_path ) ) ) || ! $theme->exists() ) {
268
+ break;
269
+ }
270
+
271
+ $package = new ITSEC_File_Change_Package_Theme( $theme );
272
+ break;
273
+ case 'core':
274
+ $package = new ITSEC_File_Change_Package_Core( $search_path[0] === '@' ? ABSPATH : $search_path );
275
+ break;
276
+ case 'system-files':
277
+ $package = new ITSEC_File_Change_Package_System();
278
+ break;
279
+ }
280
+
281
+ return $this->filter( $package, $file, $search_path );
282
+ }
283
+
284
+ /**
285
+ * Get the first directory after a search path from a file path.
286
+ *
287
+ * For example, given the following parameters:
288
+ *
289
+ * /app/public/wp-content/plugins/ithemes-security-pro/core/lib/log.php
290
+ * /app/public/wp-content/plugins/
291
+ *
292
+ * 'ithemes-security-pro' will be returned.
293
+ *
294
+ * @param string $file
295
+ * @param string $search_path
296
+ *
297
+ * @return string
298
+ */
299
+ private function get_first_directory( $file, $search_path ) {
300
+ $relative = wp_normalize_path( substr( $file, strlen( $search_path ) ) );
301
+ $parts = explode( '/', $relative, 2 );
302
+
303
+ if ( empty( $parts ) ) {
304
+ return '';
305
+ }
306
+
307
+ return $parts[0];
308
+ }
309
+ }
core/modules/file-change/lib/package-plugin.php ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_File_Change_Package_Plugin
5
+ */
6
+ class ITSEC_File_Change_Package_Plugin implements ITSEC_File_Change_Package {
7
+
8
+ /** @var string */
9
+ protected $file;
10
+
11
+ /** @var array */
12
+ protected $data;
13
+
14
+ /**
15
+ * ITSEC_File_Change_Package_WPOrg_Plugin constructor.
16
+ *
17
+ * @param string $file The full plugin file. For example, askismet/akismet.php
18
+ * @param array $data
19
+ */
20
+ public function __construct( $file, array $data ) {
21
+ $this->file = $file;
22
+ $this->data = $data;
23
+ }
24
+
25
+ /**
26
+ * @inheritdoc
27
+ */
28
+ public function get_root_path() {
29
+ return trailingslashit( dirname( WP_PLUGIN_DIR . '/' . $this->file ) );
30
+ }
31
+
32
+ /**
33
+ * @inheritdoc
34
+ */
35
+ public function get_version() {
36
+ return $this->get_plugin_header( 'Version' );
37
+ }
38
+
39
+ /**
40
+ * @inheritdoc
41
+ */
42
+ public function get_type() {
43
+ return 'plugin';
44
+ }
45
+
46
+ /**
47
+ * @inheritdoc
48
+ */
49
+ public function get_identifier() {
50
+ return $this->file;
51
+ }
52
+
53
+ /**
54
+ * Get a header value from the main plugin file.
55
+ *
56
+ * Both custom and default headers are supported. The results are internally cached.
57
+ *
58
+ * @param string $header The header as it appears in the file, for example "Plugin Name" or "Author URI".
59
+ *
60
+ * @return string
61
+ */
62
+ public function get_plugin_header( $header ) {
63
+
64
+ switch ( $header ) {
65
+ case 'Plugin Name':
66
+ if ( isset( $this->data['Name'] ) ) {
67
+ return $this->data['Name'];
68
+ }
69
+ break;
70
+ case 'Plugin URI':
71
+ if ( isset( $this->data['PluginURI'] ) ) {
72
+ return $this->data['PluginURI'];
73
+ }
74
+ break;
75
+ case 'Author URI':
76
+ if ( isset( $this->data['AuthorURI'] ) ) {
77
+ return $this->data['AuthorURI'];
78
+ }
79
+ break;
80
+ case 'Text Domain':
81
+ if ( isset( $this->data['TextDomain'] ) ) {
82
+ return $this->data['TextDomain'];
83
+ }
84
+ break;
85
+ case 'Domain Path':
86
+ if ( isset( $this->data['DomainPath'] ) ) {
87
+ return $this->data['DomainPath'];
88
+ }
89
+ break;
90
+ }
91
+
92
+ if ( ! isset( $this->data[ $header ] ) ) {
93
+ $headers = @get_file_data( $this->get_root_path() . basename( $this->file ), array( 'header' => $header ) );
94
+
95
+ $this->data[ $header ] = isset( $headers['header'] ) ? $headers['header'] : '';
96
+ }
97
+
98
+ return $this->data[ $header ];
99
+ }
100
+
101
+ /**
102
+ * @inheritdoc
103
+ */
104
+ public function __toString() {
105
+ /* translators: 1. Plugin name 2. Plugin version */
106
+ return sprintf( __( '%1$s plugin %2$s', 'better-wp-security' ), $this->get_plugin_header( 'Plugin Name' ), 'v' . $this->get_version() );
107
+ }
108
+ }
core/modules/file-change/lib/package-system.php ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_File_Change_Package_System
5
+ */
6
+ class ITSEC_File_Change_Package_System implements ITSEC_File_Change_Package {
7
+
8
+ /**
9
+ * @inheritDoc
10
+ */
11
+ public function get_root_path() {
12
+ return '/'; // System files might not necessarily be within the web root.
13
+ }
14
+
15
+ /**
16
+ * @inheritDoc
17
+ */
18
+ public function get_version() {
19
+ return '';
20
+ }
21
+
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ public function get_type() {
26
+ return 'system-files';
27
+ }
28
+
29
+ /**
30
+ * @inheritDoc
31
+ */
32
+ public function get_identifier() {
33
+ return 'system-files';
34
+ }
35
+
36
+ /**
37
+ * @inheritDoc
38
+ */
39
+ public function __toString() {
40
+ return __( 'System Files', 'better-wp-security' );
41
+ }
42
+ }
core/modules/file-change/lib/package-theme.php ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_File_Change_Package_Theme
5
+ */
6
+ class ITSEC_File_Change_Package_Theme implements ITSEC_File_Change_Package, Serializable {
7
+
8
+ /** @var WP_Theme */
9
+ private $theme;
10
+
11
+ /** @var array */
12
+ private $custom_headers = array();
13
+
14
+ /**
15
+ * ITSEC_File_Change_Package_Theme constructor.
16
+ *
17
+ * @param WP_Theme $theme
18
+ */
19
+ public function __construct( WP_Theme $theme ) { $this->theme = $theme; }
20
+
21
+ /**
22
+ * @inheritDoc
23
+ */
24
+ public function get_root_path() {
25
+ return trailingslashit( $this->theme->get_stylesheet_directory() );
26
+ }
27
+
28
+ /**
29
+ * @inheritDoc
30
+ */
31
+ public function get_version() {
32
+ return $this->theme->get( 'Version' );
33
+ }
34
+
35
+ /**
36
+ * @inheritDoc
37
+ */
38
+ public function get_type() {
39
+ return 'theme';
40
+ }
41
+
42
+ /**
43
+ * @inheritDoc
44
+ */
45
+ public function get_identifier() {
46
+ return $this->theme->get_stylesheet();
47
+ }
48
+
49
+ /**
50
+ * Get a header value from the theme's stylesheet.
51
+ *
52
+ * Both custom and default headers are supported. The results are internally cached.
53
+ *
54
+ * @param string $header The header as it appears in the file, for example "Theme Name" or "Author URI".
55
+ *
56
+ * @return string
57
+ */
58
+ public function get_theme_header( $header ) {
59
+
60
+ switch ( $header ) {
61
+ case 'Theme Name':
62
+ return $this->theme->get( 'Name' );
63
+ case 'Theme URI':
64
+ return $this->theme->get( 'ThemeURI' );
65
+ case 'Author URI':
66
+ return $this->theme->get( 'AuthorURI' );
67
+ case 'Text Domain':
68
+ return $this->theme->get( 'TextDomain' );
69
+ case 'Domain Path':
70
+ return $this->theme->get( 'DomainPath' );
71
+ default:
72
+ if ( $value = $this->theme->get( $header ) ) {
73
+ return $value;
74
+ }
75
+ break;
76
+ }
77
+
78
+ if ( ! isset( $this->custom_headers[ $header ] ) ) {
79
+ $file = "{$this->theme->get_theme_root()}/{$this->theme->get_stylesheet()}/style.css";
80
+ $headers = @get_file_data( $file, array( 'header' => $header ) );
81
+
82
+ $this->custom_headers[ $header ] = isset( $headers['header'] ) ? $headers['header'] : '';
83
+ }
84
+
85
+ return $this->custom_headers[ $header ];
86
+ }
87
+
88
+ /**
89
+ * @inheritDoc
90
+ */
91
+ public function __toString() {
92
+ /* translators: 1. Theme name 2. Theme version */
93
+ return sprintf( __( '%1$s theme %2$s', 'better-wp-security' ), $this->get_theme_header( 'Theme Name' ), 'v' . $this->get_version() );
94
+ }
95
+
96
+ /**
97
+ * @inheritDoc
98
+ */
99
+ public function serialize() {
100
+ return serialize( array(
101
+ 'theme_dir' => $this->theme->get_stylesheet(),
102
+ 'theme_root' => $this->theme->get_theme_root(),
103
+ ) );
104
+ }
105
+
106
+ /**
107
+ * @inheritDoc
108
+ */
109
+ public function unserialize( $serialized ) {
110
+ $data = unserialize( $serialized );
111
+
112
+ $this->theme = wp_get_theme( $data['theme_dir'], $data['theme_root'] );
113
+ }
114
+ }
core/modules/file-change/lib/package-unknown.php ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_File_Change_Package_Unknown
5
+ */
6
+ class ITSEC_File_Change_Package_Unknown implements ITSEC_File_Change_Package {
7
+
8
+ /**
9
+ * @inheritDoc
10
+ */
11
+ public function get_root_path() {
12
+ return '/';
13
+ }
14
+
15
+ /**
16
+ * @inheritDoc
17
+ */
18
+ public function get_version() {
19
+ return '0.0';
20
+ }
21
+
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ public function get_type() {
26
+ return 'unknown';
27
+ }
28
+
29
+ /**
30
+ * @inheritDoc
31
+ */
32
+ public function get_identifier() {
33
+ return 'unknown';
34
+ }
35
+
36
+ /**
37
+ * @inheritDoc
38
+ */
39
+ public function __toString() {
40
+ return __( 'Unknown', 'better-wp-security' );
41
+ }
42
+ }
core/modules/file-change/lib/package.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Interface ITSEC_File_Change_Package
5
+ */
6
+ interface ITSEC_File_Change_Package {
7
+
8
+ /**
9
+ * Get the path to the root directory of the package.
10
+ *
11
+ * This contains a trailingslash.
12
+ *
13
+ * @return string
14
+ */
15
+ public function get_root_path();
16
+
17
+ /**
18
+ * Get the version of the package.
19
+ *
20
+ * @return string
21
+ */
22
+ public function get_version();
23
+
24
+ /**
25
+ * Get the type of the package.
26
+ *
27
+ * For example, 'core' or 'theme' or 'ithemes-plugin'.
28
+ *
29
+ * @return string
30
+ */
31
+ public function get_type();
32
+
33
+ /**
34
+ * Get an identifier for the package.
35
+ *
36
+ * This identifier must be globally unique amongst packages of the same type.
37
+ * For example 'akismet/akismet.php' or 'ithemes-security-pro'.
38
+ *
39
+ * @return string
40
+ */
41
+ public function get_identifier();
42
+
43
+ /**
44
+ * Return a human readable label of the package.
45
+ *
46
+ * @return string
47
+ */
48
+ public function __toString();
49
+ }
core/modules/file-change/logs.php CHANGED
@@ -19,8 +19,38 @@ final class ITSEC_File_Change_Logs {
19
  } else {
20
  $entry['description'] = esc_html__( 'Changes Found', 'better-wp-security' );
21
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
 
 
24
  return $entry;
25
  }
26
 
@@ -30,12 +60,19 @@ final class ITSEC_File_Change_Logs {
30
  $details['module']['content'] = $entry['module_display'];
31
  $details['description']['content'] = $entry['description'];
32
 
33
- if ( 'process-start' !== $entry['type'] ) {
34
  $details['memory'] = array(
35
  'header' => esc_html__( 'Memory Used', 'better-wp-security' ),
36
  'content' => sprintf( esc_html_x( '%s MB', 'Megabytes of memory used', 'better-wp-security' ), $entry['data']['memory'] ),
37
  );
38
 
 
 
 
 
 
 
 
39
  $types = array(
40
  'added' => esc_html__( 'Added', 'better-wp-security' ),
41
  'removed' => esc_html__( 'Removed', 'better-wp-security' ),
@@ -50,6 +87,8 @@ final class ITSEC_File_Change_Logs {
50
  }
51
  }
52
 
 
 
53
  return $details;
54
  }
55
  }
19
  } else {
20
  $entry['description'] = esc_html__( 'Changes Found', 'better-wp-security' );
21
  }
22
+ } elseif ( 'skipping-recovery' === $code ) {
23
+ $code_specific = isset( $code_data[0] ) ? $code_data[0] : '';
24
+
25
+ if ( 'no-lock' === $code_specific ) {
26
+ $entry['description'] = esc_html__( 'Skipping Recovery: No Lock', 'better-wp-security' );
27
+ } elseif ( 'empty-storage' === $code_specific ) {
28
+ $entry['description'] = esc_html__( 'Skipping Recovery: No Lock', 'better-wp-security' );
29
+ } else {
30
+ $entry['description'] = esc_html__( 'Skipping Recovery', 'better-wp-security' );
31
+ }
32
+ } elseif ( 'attempting-recovery' === $code ) {
33
+ if ( array( 'no-job-step' ) === $code_data ) {
34
+ $entry['description'] = esc_html__( 'Attempting Recovery: Invalid Job', 'better-wp-security' );
35
+ } else {
36
+ $entry['description'] = esc_html__( 'Attempting Recovery', 'better-wp-security' );
37
+ }
38
+ } elseif ( 'recovery-failed-no-step' === $code ) {
39
+ $entry['description'] = esc_html__( 'Recovery Failed: No Step', 'better-wp-security' );
40
+ } elseif ( 'recovery-failed-too-many-retries' === $code ) {
41
+ $entry['description'] = esc_html__( 'Recovery Failed: Retry Limit', 'better-wp-security' );
42
+ } elseif ( 'recovery-failed-first-loop' === $code ) {
43
+ $entry['description'] = esc_html__( 'Recovery Failed: First Loop', 'better-wp-security' );
44
+ } elseif ( 'recovery-scheduled' === $code ) {
45
+ $entry['description'] = esc_html__( 'Recovery Scheduled', 'better-wp-security' );
46
+ } elseif ( 'file-scan-aborted' === $code ) {
47
+ $entry['description'] = esc_html__( 'Scan Failed', 'better-wp-security' );
48
+ } elseif ( 'rescheduling' === $code ) {
49
+ $entry['description'] = esc_html__( 'Rescheduling', 'better-wp-security' );
50
  }
51
 
52
+ $entry['remote_ip'] = '';
53
+
54
  return $entry;
55
  }
56
 
60
  $details['module']['content'] = $entry['module_display'];
61
  $details['description']['content'] = $entry['description'];
62
 
63
+ if ( 'changes-found' === $code || 'no-changes-found' === $code ) {
64
  $details['memory'] = array(
65
  'header' => esc_html__( 'Memory Used', 'better-wp-security' ),
66
  'content' => sprintf( esc_html_x( '%s MB', 'Megabytes of memory used', 'better-wp-security' ), $entry['data']['memory'] ),
67
  );
68
 
69
+ if ( ! empty( $entry['data']['memory_peak'] ) ) {
70
+ $details['memory_total'] = array(
71
+ 'header' => esc_html__( 'Total Memory', 'better-wp-security' ),
72
+ 'content' => sprintf( esc_html_x( '%s MB', 'Megabytes of memory used', 'better-wp-security' ), $entry['data']['memory_peak'] ),
73
+ );
74
+ }
75
+
76
  $types = array(
77
  'added' => esc_html__( 'Added', 'better-wp-security' ),
78
  'removed' => esc_html__( 'Removed', 'better-wp-security' ),
87
  }
88
  }
89
 
90
+ unset( $details['host'] );
91
+
92
  return $details;
93
  }
94
  }
core/modules/file-change/scanner.php CHANGED
@@ -1,475 +1,978 @@
1
  <?php
2
 
3
- final class ITSEC_File_Change_Scanner {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  /**
6
- * Files and directories to be excluded from the scan
7
  *
8
- * @since 4.0.0
9
- * @access private
10
- * @var array
 
11
  */
12
- private $excludes;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  /**
15
- * The module's saved options
16
  *
17
- * @since 4.0.0
18
- * @access private
19
- * @var array
 
20
  */
21
- private $settings;
22
 
23
- private $home_path;
 
 
 
 
24
 
25
- private static $instance = false;
 
 
 
 
 
 
26
 
 
 
 
 
27
 
28
- private function __construct() {}
 
29
 
30
  /**
31
- * Executes file checking
32
- *
33
- * Performs the actual execution of a file scan after determining that such an execution is needed.
34
  *
35
- * @since 4.0.0
36
  *
37
- * @static
38
- *
39
- * @param bool $scheduled_call [optional] true if this is an automatic check
40
- * @param bool $return_data [optional] whether to return a data array (true) or not (false)
41
- *
42
- * @return mixed
43
  */
44
- public static function run_scan( $scheduled_call = true, $return_data = false ) {
45
- if ( ! self::$instance ) {
46
- self::$instance = new self;
 
 
 
47
  }
48
 
49
- return self::$instance->execute_file_check( $scheduled_call, $return_data );
50
  }
51
 
52
- private function execute_file_check( $scheduled_call, $return_data ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- if ( ! ITSEC_Lib::get_lock( 'file_change', 300 ) ) {
55
- return -1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
 
 
 
58
 
59
- $process_id = ITSEC_Log::add_process_start( 'file_change', 'scan' );
 
 
 
 
 
60
 
 
 
61
 
62
- $this->home_path = untrailingslashit( ITSEC_Lib::get_home_path() );
 
63
 
64
- $this->settings = ITSEC_Modules::get_settings( 'file-change' );
65
 
66
- if ( ! in_array( '.lock', $this->settings['types'] ) ) {
67
- $this->settings['types'][] = '.lock';
68
- }
 
69
 
70
- foreach ( $this->settings['file_list'] as $index => $path ) {
71
- $path = untrailingslashit( $path );
72
- $path = '/' . ltrim( $path, '/' );
73
- $this->settings['file_list'][$index] = $path;
74
  }
75
 
76
- $this->excludes = array(
77
- ITSEC_Modules::get_setting( 'backup', 'location' ),
78
- ITSEC_Modules::get_setting( 'global', 'log_location' ),
 
 
 
 
 
 
 
79
  );
80
 
81
- foreach ( $this->excludes as $index => $path ) {
82
- $path = untrailingslashit( $path );
83
- $path = preg_replace( '/^' . preg_quote( ABSPATH, '/' ) . '/', '', $path );
84
- $path = ltrim( $path, '/' );
85
- $this->excludes[$index] = $path;
86
- }
87
 
 
 
88
 
89
- $send_email = true;
90
 
91
- ITSEC_Lib::set_minimum_memory_limit( '512M' );
92
 
93
- define( 'ITSEC_DOING_FILE_CHECK', true );
 
94
 
 
 
 
95
 
96
- //figure out what chunk we're on
97
- if ( $this->settings['split'] ) {
98
 
99
- if ( false === $this->settings['last_chunk'] || $this->settings['last_chunk'] > 5 ) {
100
- $chunk = 0;
101
- } else {
102
- $chunk = $this->settings['last_chunk'] + 1;
103
- }
104
 
105
- $db_field = 'itsec_local_file_list_' . $chunk;
106
 
107
- $wp_upload_dir = ITSEC_Core::get_wp_upload_dir();
 
108
 
109
- $dirs = array(
110
- 'wp-admin',
111
- WPINC,
112
- WP_CONTENT_DIR,
113
- $wp_upload_dir['basedir'],
114
- WP_CONTENT_DIR . '/themes',
115
- WP_PLUGIN_DIR,
116
- ''
117
- );
118
 
119
- foreach ( $dirs as $index => $dir ) {
120
- $dir = untrailingslashit( $dir );
121
- $dirs[$index] = preg_replace( '/^' . preg_quote( ABSPATH, '/' ) . '/', '', $dir );
122
- }
123
 
124
- $path = $dirs[ $chunk ];
125
 
126
- unset( $dirs[ $chunk ] );
127
- $this->excludes = array_merge( $this->excludes, $dirs );
128
 
129
- } else {
 
130
 
131
- $chunk = false;
132
- $db_field = 'itsec_local_file_list';
133
- $path = '';
134
 
135
- }
 
136
 
 
 
137
 
138
- $memory_used = @memory_get_peak_usage();
 
 
 
 
139
 
140
- $logged_files = get_site_option( $db_field );
 
 
 
 
 
141
 
142
- if ( false === $logged_files ) {
 
 
143
 
144
- $send_email = false;
 
 
 
 
145
 
146
- $logged_files = array();
 
 
147
 
148
- if ( is_multisite() ) {
 
 
 
 
 
149
 
150
- add_site_option( $db_field, $logged_files );
151
 
152
- } else {
 
 
153
 
154
- add_option( $db_field, $logged_files, '', 'no' );
 
155
 
156
- }
 
 
157
 
 
158
  }
159
 
160
- ITSEC_Log::add_process_update( $process_id, array( 'status' => 'init_complete', 'settings' => $this->settings, 'excludes' => $this->excludes, 'path' => $path, 'scheduled_call' => $scheduled_call, 'chunk' => $chunk ) );
 
161
 
162
- do_action( 'itsec-file-change-start-scan' );
163
- $current_files = $this->scan_files( $path );
164
- do_action( 'itsec-file-change-end-scan' );
165
 
166
- ITSEC_Log::add_process_update( $process_id, array( 'status' => 'file_scan_complete' ) );
 
167
 
 
 
 
 
 
 
 
 
168
 
169
- $files_added = @array_diff_assoc( $current_files, $logged_files ); //files added
170
- $files_removed = @array_diff_assoc( $logged_files, $current_files ); //files deleted
171
- $current_minus_added = @array_diff_key( $current_files, $files_added ); //remove all added files from current filelist
172
- $logged_minus_deleted = @array_diff_key( $logged_files, $files_removed ); //remove all deleted files from old file list
173
- $files_changed = array(); //array of changed files
174
 
175
- do_action( 'itsec-file-change-start-hash-comparisons' );
176
 
177
- //compare file hashes and mod dates
178
- foreach ( $current_minus_added as $current_file => $current_attr ) {
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- if ( array_key_exists( $current_file, $logged_minus_deleted ) ) {
 
 
181
 
182
- //if attributes differ added to changed files array
183
- if (
184
- (
185
- (
186
- isset( $current_attr['mod_date'] ) &&
187
- 0 != strcmp( $current_attr['mod_date'], $logged_minus_deleted[ $current_file ]['mod_date'] )
188
- ) ||
189
- 0 != strcmp( $current_attr['d'], $logged_minus_deleted[ $current_file ]['d'] )
190
- ) ||
191
- (
192
- (
193
- isset( $current_attr['hash'] ) &&
194
- 0 != strcmp( $current_attr['hash'], $logged_minus_deleted[ $current_file ]['hash'] ) ) ||
195
- 0 != strcmp( $current_attr['h'], $logged_minus_deleted[ $current_file ]['h'] )
196
- )
197
- ) {
198
 
199
- $remote_check = apply_filters( 'itsec_process_changed_file', true, $current_file, $current_attr['h'] ); //hook to run actions on a changed file at time of discovery
 
 
200
 
201
- if ( true === $remote_check ) { //don't list the file if it matches the WordPress.org hash
 
 
 
 
202
 
203
- $files_changed[ $current_file ]['h'] = isset( $current_attr['hash'] ) ? $current_attr['hash'] : $current_attr['h'];
204
- $files_changed[ $current_file ]['d'] = isset( $current_attr['mod_date'] ) ? $current_attr['mod_date'] : $current_attr['d'];
 
 
 
 
 
 
 
 
205
 
206
- }
 
 
207
 
208
- }
 
 
209
 
210
- }
211
 
 
 
 
212
  }
213
 
 
 
214
 
215
- //get count of changes
216
- $files_added_count = count( $files_added );
217
- $files_deleted_count = count( $files_removed );
218
- $files_changed_count = count( $files_changed );
 
 
 
 
 
219
 
220
- if ( $files_added_count > 0 ) {
 
221
 
222
- $files_added = apply_filters( 'itsec_process_added_files', $files_added ); //hook to run actions on all files added
223
- $files_added_count = count( $files_added );
 
 
224
 
 
 
 
 
 
225
  }
226
 
227
- if ( $files_deleted_count > 0 ) {
228
- do_action( 'itsec_process_removed_files', $files_removed ); //hook to run actions on all files removed
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  }
 
230
 
231
- do_action( 'itsec-file-change-end-hash-comparisons' );
 
 
 
 
 
232
 
233
- ITSEC_Log::add_process_update( $process_id, array( 'status' => 'hash_comparisons_complete' ) );
 
 
234
 
 
235
 
236
- //create single array of all changes
237
- $full_change_list = array(
238
- 'added' => $files_added,
239
- 'removed' => $files_removed,
240
- 'changed' => $files_changed,
241
- );
242
 
243
- $this->settings['latest_changes'] = array(
244
- 'added' => count( $files_added ),
245
- 'removed' => count( $files_removed ),
246
- 'changed' => count( $files_changed ),
247
- );
248
 
249
- update_site_option( $db_field, $current_files );
250
 
 
251
 
252
- //Cleanup variables when we're done with them
253
- unset( $files_added );
254
- unset( $files_removed );
255
- unset( $files_changed );
256
- unset( $current_files );
257
 
258
- $this->settings['last_run'] = ITSEC_Core::get_current_time();
259
- $this->settings['last_chunk'] = $chunk;
 
 
 
 
 
 
 
260
 
261
- ITSEC_Modules::set_settings( 'file-change', $this->settings );
262
 
263
- //get new max memory
264
- $check_memory = @memory_get_peak_usage();
265
- if ( $check_memory > $memory_used ) {
266
- $memory_used = $check_memory - $memory_used;
267
  }
268
 
269
- $full_change_list['memory'] = round( ( $memory_used / 1000000 ), 2 );
270
 
271
- if ( $files_added_count > 0 || $files_changed_count > 0 || $files_deleted_count > 0 ) {
272
- $found_changes = true;
273
- } else {
274
- $found_changes = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  }
276
 
277
- if (
278
- $found_changes &&
279
- $send_email &&
280
- ! $scheduled_call &&
281
- $this->settings['email']
282
- ) {
283
 
284
- $email_details = array(
285
- $files_added_count,
286
- $files_deleted_count,
287
- $files_changed_count,
288
- $full_change_list
289
- );
290
 
291
- $this->send_notification_email( $email_details );
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
 
294
- if (
295
- $found_changes &&
296
- $this->settings['notify_admin'] &&
297
- function_exists( 'get_current_screen' ) &&
298
- (
299
- ! isset( get_current_screen()->id ) ||
300
- false === strpos( get_current_screen()->id, 'security_page_toplevel_page_itsec_logs' )
301
- )
302
- ) {
303
- ITSEC_Modules::set_setting( 'file-change', 'show_warning', true );
304
  }
305
 
306
- if ( $found_changes ) {
307
- ITSEC_Log::add_warning( 'file_change', "changes-found::$files_added_count,$files_deleted_count,$files_changed_count", $full_change_list );
308
- } else {
309
- ITSEC_Log::add_notice( 'file_change', 'no-changes-found', $full_change_list );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  }
311
 
312
- ITSEC_Lib::release_lock( 'file_change' );
313
 
 
 
 
314
 
315
- ITSEC_Log::add_process_stop( $process_id );
 
316
 
317
- if ( $files_added_count > 0 || $files_changed_count > 0 || $files_deleted_count > 0 ) {
 
 
 
 
 
318
 
319
- //There were changes found
320
- if ( $return_data ) {
321
 
322
- return $full_change_list;
 
323
 
324
- } else {
 
 
 
 
 
 
 
325
 
326
- return true;
 
 
327
 
 
 
 
 
 
 
328
  }
329
 
 
330
  } else {
 
 
331
 
332
- return false; //No changes were found
 
333
 
 
 
334
  }
335
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  }
337
 
338
  /**
339
- * Get Report Details
340
- *
341
- * Creates the HTML markup for the email that is to be built
342
  *
343
- * @since 4.0.0
344
  *
345
- * @param array $email_details array of details to build email
346
- *
347
- * @return string report details
348
  */
349
- public function get_email_report( $email_details ) {
350
- _deprecated_function( __METHOD__, '3.9.0' );
 
 
 
351
 
352
- return $this->generate_notification_email( $email_details )->get_content();
 
 
 
 
 
 
 
 
353
  }
354
 
355
  /**
356
- * Builds table section for file report
357
- *
358
- * Builds the individual table areas for files added, changed and deleted that goes in the file
359
- * change notification emails.
360
  *
361
- * @since 4.6.0
362
- *
363
- * @access private
 
 
 
 
 
 
 
 
 
364
  *
365
- * @param string $title User readable title to display
366
- * @param array $files array of files to build the report on
 
 
 
 
 
 
 
 
 
 
367
  *
368
- * @return string the markup with the given files to be added to the report
369
  */
370
- private function build_table_section( $title, $files ) {
371
 
372
- $section = '<h4>' . __( 'Files', 'better-wp-security' ) . ' ' . $title . '</h4>';
373
- $section .= '<table border="1" style="width: 100%; text-align: center;">' . PHP_EOL;
374
- $section .= '<tr>' . PHP_EOL;
375
- $section .= '<th>' . __( 'File', 'better-wp-security' ) . '</th>' . PHP_EOL;
376
- $section .= '<th>' . __( 'Modified', 'better-wp-security' ) . '</th>' . PHP_EOL;
377
- $section .= '<th>' . __( 'File Hash', 'better-wp-security' ) . '</th>' . PHP_EOL;
378
- $section .= '</tr>' . PHP_EOL;
379
 
380
- if ( empty( $files ) ) {
 
381
 
382
- $section .= '<tr>' . PHP_EOL;
383
- $section .= '<td colspan="3">' . __( 'No files were changed.', 'better-wp-security' ) . '</td>' . PHP_EOL;
384
- $section .= '</tr>' . PHP_EOL;
 
 
 
 
 
 
 
 
385
 
386
- } else {
 
387
 
388
- foreach ( $files as $item => $attr ) {
 
 
 
 
 
 
 
389
 
390
- $section .= '<tr>' . PHP_EOL;
391
- $section .= '<td>' . $item . '</td>' . PHP_EOL;
392
- $section .= '<td>' . date( 'l F jS, Y \a\t g:i a e', ( isset( $attr['mod_date'] ) ? $attr['mod_date'] : $attr['d'] ) ) . '</td>' . PHP_EOL;
393
- $section .= '<td>' . ( isset( $attr['hash'] ) ? $attr['hash'] : $attr['h'] ) . '</td>' . PHP_EOL;
394
- $section .= '</tr>' . PHP_EOL;
395
 
 
 
 
 
 
396
  }
397
-
398
  }
399
 
400
- $section .= '</table>' . PHP_EOL;
401
-
402
- return $section;
403
  }
404
 
405
  /**
406
- * Scans all files in a given path
407
- *
408
- * Scans all items in a given path recursively building an array of items including
409
- * hashes, filenames and modification dates
410
- *
411
- * @since 4.0.0
412
- *
413
- * @access private
414
- *
415
- * @param string $path Path to scan. Defaults to WordPress root
416
  *
417
- * @return array array of files found and their information
418
  *
 
419
  */
420
- private function scan_files( $path ) {
421
- if ( in_array( $path, $this->excludes ) ) {
422
- return array();
423
- }
424
 
 
 
425
 
426
- $abspath = "{$this->home_path}/$path";
427
- $data = array();
 
 
 
428
 
429
- if ( false === ( $dh = @opendir( $abspath ) ) ) {
430
- return $data;
431
- }
432
 
 
 
 
433
 
434
- while ( false !== ( $item = @readdir( $dh ) ) ) {
 
 
435
 
436
- if ( '.' === $item || '..' === $item ) {
437
- continue;
 
 
 
 
 
 
 
 
 
 
 
438
  }
 
439
 
 
 
440
 
441
- $relname = "$path/$item";
442
- $absname = "$abspath/$item";
 
 
 
443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
- // Efficient but difficult to grock way to skip an item if it is in the file_list and the method is
446
- // exclude or if it is not in the file_list and the method is include.
447
- if ( in_array( $relname, $this->settings['file_list'] ) xor 'include' === $this->settings['method'] ) {
448
- continue;
449
- }
 
 
 
 
 
450
 
 
 
451
 
452
- if ( is_dir( $absname ) && 'dir' === filetype( $absname ) ) {
 
 
453
 
454
- $data = array_merge( $data, $this->scan_files( $relname ) );
 
455
 
456
- } else {
457
- if ( in_array( '.' . pathinfo( $item, PATHINFO_EXTENSION ), $this->settings['types'] ) ) {
458
- continue;
459
- }
 
 
 
 
460
 
461
- $data[ substr( $relname, 1 ) ] = array(
462
- 'd' => @filemtime( $absname ),
463
- 'h' => @md5_file( $absname ),
464
- );
465
- }
 
 
 
466
 
 
 
467
  }
468
 
469
- @closedir( $dh );
 
 
 
 
 
470
 
471
- return $data;
 
472
 
 
473
  }
474
 
475
  /**
@@ -490,7 +993,7 @@ final class ITSEC_File_Change_Scanner {
490
 
491
  $changed = $email_details[0] + $email_details[1] + $email_details[2];
492
 
493
- if ( $changed <= 0 ) {
494
  return;
495
  }
496
 
@@ -571,4 +1074,4 @@ final class ITSEC_File_Change_Scanner {
571
 
572
  return $rows;
573
  }
574
- }
1
  <?php
2
 
3
+ require_once( ABSPATH . 'wp-admin/includes/file.php' );
4
+ require_once( dirname( __FILE__ ) . '/class-itsec-file-change.php' );
5
+ require_once( dirname( __FILE__ ) . '/lib/chunk-scanner.php' );
6
+ require_once( dirname( __FILE__ ) . '/lib/hash-comparator.php' );
7
+ require_once( dirname( __FILE__ ) . '/lib/hash-comparator-loadable.php' );
8
+ require_once( dirname( __FILE__ ) . '/lib/hash-comparator-chain.php' );
9
+ require_once( dirname( __FILE__ ) . '/lib/hash-comparator-managed-files.php' );
10
+ require_once( dirname( __FILE__ ) . '/lib/hash-loading-failed-exception.php' );
11
+ require_once( dirname( __FILE__ ) . '/lib/package.php' );
12
+ require_once( dirname( __FILE__ ) . '/lib/package-core.php' );
13
+ require_once( dirname( __FILE__ ) . '/lib/package-factory.php' );
14
+ require_once( dirname( __FILE__ ) . '/lib/package-plugin.php' );
15
+ require_once( dirname( __FILE__ ) . '/lib/package-system.php' );
16
+ require_once( dirname( __FILE__ ) . '/lib/package-theme.php' );
17
+ require_once( dirname( __FILE__ ) . '/lib/package-unknown.php' );
18
+
19
+ do_action( 'itsec_load_file_change_scanner' );
20
+
21
+ class ITSEC_File_Change_Scanner {
22
+
23
+ const DESTROYED = 'itsec_file_change_scan_destroyed';
24
+ const FILE_LIST = 'itsec_file_list';
25
+
26
+ const C_ADMIN = 'admin';
27
+ const C_INCLUDES = 'includes';
28
+ const C_CONTENT = 'content';
29
+ const C_UPLOADS = 'uploads';
30
+ const C_THEMES = 'themes';
31
+ const C_PLUGINS = 'plugins';
32
+ const C_OTHERS = 'others';
33
+
34
+ const S_NONE = 0;
35
+ const S_NORMAL = 1;
36
+ const S_BAD_CHANGE = 2;
37
+ const S_UNKNOWN_FILE = 3;
38
+
39
+ const T_ADDED = 'a';
40
+ const T_CHANGED = 'c';
41
+ const T_REMOVED = 'r';
42
+
43
+ /** @var ITSEC_File_Change_Hash_Comparator */
44
+ private $comparator;
45
+
46
+ /** @var ITSEC_File_Change_Package_Factory */
47
+ private $package_factory;
48
+
49
+ /** @var ITSEC_Lib_Distributed_Storage */
50
+ private $storage;
51
+
52
+ /** @var array */
53
+ private $settings;
54
+
55
+ /** @var array */
56
+ private $chunk_order;
57
+
58
+ /** @var ITSEC_File_Change_Chunk_Scanner */
59
+ private $chunk_scanner;
60
 
61
  /**
62
+ * ITSEC_New_File_Change_Scanner constructor.
63
  *
64
+ * @param ITSEC_File_Change_Chunk_Scanner $chunk_scanner
65
+ * @param ITSEC_File_Change_Hash_Comparator $comparator
66
+ * @param ITSEC_File_Change_Package_Factory $package_factory
67
+ * @param ITSEC_Lib_Distributed_Storage $storage
68
  */
69
+ public function __construct(
70
+ ITSEC_File_Change_Chunk_Scanner $chunk_scanner = null,
71
+ ITSEC_File_Change_Hash_Comparator $comparator = null,
72
+ ITSEC_File_Change_Package_Factory $package_factory = null,
73
+ ITSEC_Lib_Distributed_Storage $storage = null
74
+ ) {
75
+ $this->chunk_scanner = $chunk_scanner;
76
+ $this->comparator = $comparator;
77
+ $this->package_factory = $package_factory;
78
+ $this->storage = $storage;
79
+ $this->settings = ITSEC_Modules::get_settings( 'file-change' );
80
+
81
+ $this->chunk_order = array(
82
+ self::C_ADMIN,
83
+ self::C_INCLUDES,
84
+ self::C_CONTENT,
85
+ self::C_UPLOADS,
86
+ self::C_THEMES,
87
+ self::C_PLUGINS,
88
+ self::C_OTHERS,
89
+ );
90
+ }
91
 
92
  /**
93
+ * Schedule a scan to start.
94
  *
95
+ * @param bool $user_initiated
96
+ * @param ITSEC_Scheduler $scheduler
97
+ *
98
+ * @return bool|WP_Error
99
  */
100
+ public static function schedule_start( $user_initiated = true, $scheduler = null ) {
101
 
102
+ $scheduler = $scheduler ? $scheduler : ITSEC_Core::get_scheduler();
103
+
104
+ if ( self::is_running( $scheduler ) ) {
105
+ return new WP_Error( 'itsec-file-change-scan-already-running', __( 'A File Change scan is currently in progress.', 'better-wp-security' ) );
106
+ }
107
 
108
+ if ( $user_initiated ) {
109
+ $id = 'file-change-fast';
110
+ $opts = array( 'fire_at' => ITSEC_Core::get_current_time_gmt() );
111
+ } else {
112
+ $id = 'file-change';
113
+ $opts = array();
114
+ }
115
 
116
+ $scheduler->schedule_loop( $id, array(
117
+ 'step' => 'get-files',
118
+ 'chunk' => self::C_ADMIN,
119
+ ), $opts );
120
 
121
+ return true;
122
+ }
123
 
124
  /**
125
+ * Check if a scan is running.
 
 
126
  *
127
+ * @param ITSEC_Scheduler
128
  *
129
+ * @return bool
 
 
 
 
 
130
  */
131
+ public static function is_running( $scheduler = null ) {
132
+
133
+ $scheduler = $scheduler ? $scheduler : ITSEC_Core::get_scheduler();
134
+
135
+ if ( $scheduler->is_single_scheduled( 'file-change-fast', null ) ) {
136
+ return true;
137
  }
138
 
139
+ return ! ITSEC_File_Change::make_progress_storage()->is_empty();
140
  }
141
 
142
+ /**
143
+ * Get the scan status.
144
+ *
145
+ * @param bool $is_running
146
+ *
147
+ * @return array
148
+ */
149
+ public static function get_status( $is_running = true ) {
150
+ $scheduler = ITSEC_Core::get_scheduler();
151
+
152
+ $storage = ITSEC_File_Change::make_progress_storage();
153
+
154
+ if ( ! $storage->is_empty() ) {
155
+ switch ( $storage->get( 'step' ) ) {
156
+ case 'get-files':
157
+ switch ( $storage->get( 'chunk' ) ) {
158
+ case self::C_ADMIN:
159
+ $message = esc_html__( 'Scanning admin files...', 'better-wp-security' );
160
+ break;
161
+ case self::C_INCLUDES:
162
+ $message = esc_html__( 'Scanning includes files...', 'better-wp-security' );
163
+ break;
164
+ case self::C_THEMES:
165
+ $message = esc_html__( 'Scanning theme files...', 'better-wp-security' );
166
+ break;
167
+ case self::C_PLUGINS:
168
+ $message = esc_html__( 'Scanning plugin files...', 'better-wp-security' );
169
+ break;
170
+ case self::C_CONTENT:
171
+ $message = esc_html__( 'Scanning content files...', 'better-wp-security' );
172
+ break;
173
+ case self::C_UPLOADS:
174
+ $message = esc_html__( 'Scanning media files...', 'better-wp-security' );
175
+ break;
176
+ case self::C_OTHERS:
177
+ default:
178
+ $message = esc_html__( 'Scanning files...', 'better-wp-security' );
179
+ break;
180
+ }
181
+ break;
182
+ case 'compare-files':
183
+ $message = esc_html__( 'Comparing files...', 'better-wp-security' );
184
+ break;
185
+ case 'check-hashes':
186
+ $message = esc_html__( 'Verifying file changes...', 'better-wp-security' );
187
+ break;
188
+ case 'scan-files':
189
+ $message = esc_html__( 'Checking for malware...', 'better-wp-security' );
190
+ break;
191
+ case 'complete':
192
+ $message = esc_html__( 'Wrapping up...', 'better-wp-security' );
193
+ break;
194
+ default:
195
+ $message = esc_html__( 'Scanning...', 'better-wp-security' );
196
+ break;
197
+ }
198
 
199
+ $status = array(
200
+ 'running' => true,
201
+ 'step' => $storage->get( 'step' ),
202
+ 'chunk' => $storage->get( 'chunk' ),
203
+ 'health' => $storage->health_check(),
204
+ 'message' => $message,
205
+ );
206
+ } elseif ( get_site_option( self::DESTROYED ) ) {
207
+ delete_site_option( self::DESTROYED );
208
+ $status = array(
209
+ 'running' => false,
210
+ 'aborted' => true,
211
+ 'message' => esc_html__( 'Scan could not be completed. Please contact support if this error persists.', 'better-wp-security' ),
212
+ );
213
+ } elseif ( self::is_running( $scheduler ) ) {
214
+ $status = array(
215
+ 'running' => true,
216
+ 'message' => esc_html__( 'Preparing...', 'better-wp-security' ),
217
+ );
218
+ } elseif ( $is_running ) {
219
+ ITSEC_Storage::save();
220
+ ITSEC_Storage::reload();
221
+ ITSEC_Modules::get_settings_obj( 'file-change' )->load();
222
+
223
+ $status = array(
224
+ 'running' => false,
225
+ 'complete' => true,
226
+ 'message' => esc_html__( 'Complete!', 'better-wp-security' ),
227
+ 'found_changes' => ITSEC_Modules::get_setting( 'file-change', 'last_scan' ),
228
+ );
229
+ } else {
230
+ $status = array(
231
+ 'running' => false,
232
+ 'message' => '',
233
+ );
234
  }
235
 
236
+ return $status;
237
+ }
238
 
239
+ /**
240
+ * Recover from a failed health check.
241
+ *
242
+ * @return bool Whether the scan was recovered. Will return false if aborted.
243
+ */
244
+ public static function recover() {
245
 
246
+ if ( ! ITSEC_Lib::get_lock( 'file-change-recover' ) ) {
247
+ ITSEC_Log::add_debug( 'file_change', 'skipping-recovery::no-lock' );
248
 
249
+ return false;
250
+ }
251
 
252
+ $storage = ITSEC_File_Change::make_progress_storage();
253
 
254
+ if ( $storage->is_empty() ) {
255
+ ITSEC_Log::add_debug( 'file_change', 'skipping-recovery::empty-storage', array(
256
+ 'backtrace' => debug_backtrace()
257
+ ) );
258
 
259
+ return false;
 
 
 
260
  }
261
 
262
+ $scheduler = ITSEC_Core::get_scheduler();
263
+
264
+ $store = array(
265
+ 'step' => $storage->get( 'step' ),
266
+ 'chunk' => $storage->get( 'chunk' ),
267
+ 'id' => $storage->get( 'id' ),
268
+ 'data' => $storage->get( 'data' ),
269
+ 'memory' => $storage->get( 'memory' ),
270
+ 'memory_peak' => $storage->get( 'memory_peak' ),
271
+ 'health_check' => $storage->health_check(),
272
  );
273
 
274
+ ITSEC_Log::add_debug( 'file_change', 'attempting-recovery', array( 'storage' => $store ) );
 
 
 
 
 
275
 
276
+ if ( empty( $store['step'] ) ) {
277
+ ITSEC_Log::add_debug( 'file_change', 'recovery-failed-no-step' );
278
 
279
+ self::abort();
280
 
281
+ ITSEC_Lib::release_lock( 'file-change-recover' );
282
 
283
+ return false;
284
+ }
285
 
286
+ $job_data = $store['data'];
287
+ $job_data['step'] = $store['step'];
288
+ $job_data['chunk'] = $store['chunk'];
289
 
290
+ if ( 'get-files' === $job_data['step'] && self::C_ADMIN === $job_data['chunk'] ) {
291
+ ITSEC_Log::add_debug( 'file_change', 'recovery-failed-first-loop' );
292
 
293
+ self::abort();
 
 
 
 
294
 
295
+ ITSEC_Lib::release_lock( 'file-change-recover' );
296
 
297
+ return false;
298
+ }
299
 
300
+ $job = new ITSEC_Job( $scheduler, $store['id'], $job_data, array( 'single' => true ) );
 
 
 
 
 
 
 
 
301
 
302
+ if ( 5 < $job->is_retry() ) {
303
+ ITSEC_Log::add_debug( 'file_change', 'recovery-failed-too-many-retries' );
 
 
304
 
305
+ self::abort();
306
 
307
+ ITSEC_Lib::release_lock( 'file-change-recover' );
 
308
 
309
+ return false;
310
+ }
311
 
312
+ $job->reschedule_in( 30 );
 
 
313
 
314
+ ITSEC_Log::add_debug( 'file_change', 'recovery-scheduled', compact( 'job' ) );
315
+ ITSEC_Lib::release_lock( 'file-change-recover' );
316
 
317
+ return true;
318
+ }
319
 
320
+ /**
321
+ * Abort an in-progress scan.
322
+ */
323
+ public static function abort() {
324
+ $storage = ITSEC_File_Change::make_progress_storage();
325
 
326
+ if ( 'file-change-fast' === $storage->get( 'id' ) ) {
327
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change-fast', null );
328
+ } else {
329
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change', null );
330
+ self::schedule_start( false );
331
+ }
332
 
333
+ if ( $process = $storage->get( 'process' ) ) {
334
+ ITSEC_Log::add_process_stop( $process, array( 'aborted' => true ) );
335
+ }
336
 
337
+ ITSEC_Log::add_fatal_error( 'file_change', 'file-scan-aborted', array(
338
+ 'id' => $storage->get( 'id' ),
339
+ 'step' => $storage->get( 'step' ),
340
+ 'chunk' => $storage->get( 'chunk' ),
341
+ ) );
342
 
343
+ $storage->clear();
344
+ update_site_option( self::DESTROYED, ITSEC_Core::get_current_time_gmt() );
345
+ }
346
 
347
+ /**
348
+ * Handle a Job.
349
+ *
350
+ * @param ITSEC_Job $job
351
+ */
352
+ public function run( ITSEC_Job $job ) {
353
 
354
+ $data = $job->get_data();
355
 
356
+ if ( empty( $data['step'] ) ) {
357
+ ITSEC_Log::add_debug( 'file_change', 'attempting-recovery::no-job-step', array( 'job' => $data ) );
358
+ self::recover();
359
 
360
+ return;
361
+ }
362
 
363
+ if ( ! $this->allow_to_run( $job ) ) {
364
+ ITSEC_Log::add_debug( 'file_change', 'rescheduling', array( 'job' => $data, 'id' => $job->get_id() ) );
365
+ $job->reschedule_in( 10 * MINUTE_IN_SECONDS );
366
 
367
+ return;
368
  }
369
 
370
+ ITSEC_Lib::set_minimum_memory_limit( '512M' );
371
+ @set_time_limit( 0 );
372
 
373
+ if ( ! defined( 'ITSEC_DOING_FILE_CHECK' ) ) {
374
+ define( 'ITSEC_DOING_FILE_CHECK', true );
375
+ }
376
 
377
+ if ( 1 === $data['loop_item'] ) {
378
+ $settings = $this->settings;
379
 
380
+ $process = ITSEC_Log::add_process_start( 'file_change', 'scan', array(
381
+ 'settings' => $settings,
382
+ 'scheduled_call' => 'file-change' === $job->get_id(),
383
+ ) );
384
+ $this->get_storage()->set( 'process', $process );
385
+ $this->get_storage()->set( 'id', $job->get_id() );
386
+ delete_site_option( self::DESTROYED );
387
+ }
388
 
389
+ $this->get_storage()->set( 'data', $data );
390
+ $this->get_storage()->set( 'step', $data['step'] );
 
 
 
391
 
392
+ $memory_used = @memory_get_peak_usage();
393
 
394
+ switch ( $data['step'] ) {
395
+ case 'get-files':
396
+ $this->get_files( $job );
397
+ break;
398
+ case 'compare-files':
399
+ $this->compare_files( $job );
400
+ break;
401
+ case 'check-hashes':
402
+ $this->check_hashes( $job );
403
+ break;
404
+ case 'complete':
405
+ $this->complete( $job );
406
+ break;
407
+ }
408
 
409
+ if ( $this->get_storage()->is_empty() ) {
410
+ return;
411
+ }
412
 
413
+ $check_memory = @memory_get_peak_usage();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
+ if ( $check_memory > $memory_used ) {
416
+ $memory_used = $check_memory - $memory_used;
417
+ }
418
 
419
+ if ( $memory_used > $this->get_storage()->get( 'memory' ) ) {
420
+ $this->get_storage()->set( 'memory', $memory_used );
421
+ $this->get_storage()->set( 'memory_peak', $check_memory );
422
+ }
423
+ }
424
 
425
+ /**
426
+ * Should we allow a scan to be run now.
427
+ *
428
+ * This is used to block a scheduled scan from running while a user initiated scan is currently processing.
429
+ *
430
+ * @param ITSEC_Job $job
431
+ *
432
+ * @return bool
433
+ */
434
+ private function allow_to_run( ITSEC_Job $job ) {
435
 
436
+ if ( 'file-change' !== $job->get_id() ) {
437
+ return true;
438
+ }
439
 
440
+ if ( ITSEC_Core::get_scheduler()->is_single_scheduled( 'file-change-fast', null ) ) {
441
+ return false;
442
+ }
443
 
444
+ $data = $job->get_data();
445
 
446
+ // Don't allow starting a slow file change scan if one is already in progress and running.
447
+ if ( 1 === $data['loop_item'] && ! $this->get_storage()->is_empty() ) {
448
+ return false;
449
  }
450
 
451
+ return true;
452
+ }
453
 
454
+ /**
455
+ * Get the hashes and date modify times for all files in the requested chunk.
456
+ *
457
+ * This will write the file list to step storage and schedule the next chunk.
458
+ * If last chunk, will schedule the compare-files step.
459
+ *
460
+ * @param ITSEC_Job $job
461
+ */
462
+ private function get_files( ITSEC_Job $job ) {
463
 
464
+ $data = $job->get_data();
465
+ $this->get_storage()->set( 'chunk', $data['chunk'] );
466
 
467
+ $this->add_process_update( array(
468
+ 'status' => 'get_chunk_files',
469
+ 'chunk' => $data['chunk'],
470
+ ) );
471
 
472
+ if ( self::C_PLUGINS === $data['chunk'] ) {
473
+ list( $file_list, $do_same_chunk ) = $this->get_files_plugins();
474
+ } else {
475
+ $file_list = $this->get_chunk_scanner()->scan( $data['chunk'] );
476
+ $do_same_chunk = false;
477
  }
478
 
479
+ $this->get_storage()->append( 'file_list', $file_list );
480
+ $pos = array_search( $data['chunk'], $this->chunk_order, true );
481
+
482
+ if ( $do_same_chunk ) {
483
+ $job->schedule_next_in_loop( array( 'chunk' => $data['chunk'] ) );
484
+ } elseif ( isset( $this->chunk_order[ $pos + 1 ] ) ) {
485
+ $this->get_storage()->set( 'chunk', $this->chunk_order[ $pos + 1 ] );
486
+ $job->schedule_next_in_loop( array(
487
+ 'chunk' => $this->chunk_order[ $pos + 1 ],
488
+ ) );
489
+ } else {
490
+ $this->add_process_update( array( 'status' => 'file_scan_complete' ) );
491
+ $job->schedule_next_in_loop( array(
492
+ 'step' => 'compare-files'
493
+ ) );
494
  }
495
+ }
496
 
497
+ /**
498
+ * Handler for plugins so we don't try to scan more than 10 plugins in a process.
499
+ *
500
+ * @return array
501
+ */
502
+ private function get_files_plugins() {
503
 
504
+ $excludes = $this->get_storage()->get( 'done_plugins' );
505
+ $this->add_process_update( array( 'status' => 'get_chunk_files_plugins', 'excludes' => $excludes ) );
506
+ $file_list = $this->get_chunk_scanner()->scan( self::C_PLUGINS, 10, $excludes );
507
 
508
+ $scanned = array();
509
 
510
+ foreach ( $file_list as $file => $attr ) {
511
+ $trimmed = ITSEC_Lib::replace_prefix( $file, WP_PLUGIN_DIR . '/', '' );
512
+ list( $top_dir ) = explode( '/', $trimmed );
 
 
 
513
 
514
+ $scanned[ WP_PLUGIN_DIR . '/' . $top_dir ] = 1;
515
+ }
 
 
 
516
 
517
+ $this->add_process_update( array( 'status' => 'get_chunk_files_plugins_scanned', 'scanned' => $scanned ) );
518
 
519
+ $this->get_storage()->set( 'done_plugins', array_merge( $this->get_storage()->get( 'done_plugins' ), array_keys( $scanned ) ) );
520
 
521
+ return array( $file_list, count( $scanned ) >= 10 );
522
+ }
 
 
 
523
 
524
+ /**
525
+ * Compare the list of file hashes to determine what files have been added/changed/removed.
526
+ *
527
+ * If there are no file changes, the scan will be completed. Otherwise it will schedule a job
528
+ * to check the hashes.
529
+ *
530
+ * @param ITSEC_Job $job
531
+ */
532
+ private function compare_files( ITSEC_Job $job ) {
533
 
534
+ $excludes = array();
535
 
536
+ foreach ( $this->settings['file_list'] as $file ) {
537
+ $cleaned = untrailingslashit( get_home_path() . ltrim( $file, '/' ) );
538
+ $excludes[ $cleaned ] = 1;
 
539
  }
540
 
541
+ $types = array_flip( $this->settings['types'] );
542
 
543
+ $this->add_process_update( array( 'status' => 'file_comparisons_start', 'excludes' => $excludes, 'types' => $types ) );
544
+
545
+ $current_files = $this->get_storage()->get_cursor( 'file_list' );
546
+ $prev_files = self::get_file_list_to_compare();
547
+
548
+ $report = array();
549
+
550
+ foreach ( $current_files as $file => $attr ) {
551
+ if ( ! isset( $prev_files[ $file ] ) ) {
552
+ $attr['t'] = self::T_ADDED;
553
+ $report[ $file ] = $attr;
554
+ } elseif ( $prev_files[ $file ]['h'] !== $attr['h'] ) {
555
+ $attr['t'] = self::T_CHANGED;
556
+ $report[ $file ] = $attr;
557
+ }
558
+
559
+ unset( $prev_files[ $file ] );
560
  }
561
 
562
+ foreach ( $prev_files as $file => $attr ) {
 
 
 
 
 
563
 
564
+ if ( isset( $excludes[ $file ] ) ) {
565
+ continue;
566
+ }
 
 
 
567
 
568
+ foreach ( $excludes as $exclude => $_ ) {
569
+ if ( 0 === strpos( $file, trailingslashit( $exclude ) ) ) {
570
+ continue 2;
571
+ }
572
+ }
573
+
574
+ $extension = '.' . pathinfo( $file, PATHINFO_EXTENSION );
575
+
576
+ if ( isset( $types[ $extension ] ) ) {
577
+ continue;
578
+ }
579
+
580
+ $attr['t'] = self::T_REMOVED;
581
+ $report[ $file ] = $attr;
582
  }
583
 
584
+ $this->add_process_update( array( 'status' => 'file_comparisons_complete' ) );
585
+
586
+ if ( ! $report ) {
587
+ $this->add_process_update( array( 'status' => 'file_comparisons_complete_no_changes' ) );
588
+ $this->complete( $job );
589
+
590
+ return;
 
 
 
591
  }
592
 
593
+ $this->get_storage()->set( 'files', $report );
594
+ $job->schedule_next_in_loop( array( 'step' => 'check-hashes' ) );
595
+ }
596
+
597
+ /**
598
+ * Check the file changes with each package's hashes to determine whether the change was expected or not.
599
+ *
600
+ * @param ITSEC_Job $job
601
+ */
602
+ private function check_hashes( ITSEC_Job $job ) {
603
+
604
+ $this->add_process_update( array( 'status' => 'hash_comparisons_start' ) );
605
+
606
+ do_action( 'itsec-file-change-start-hash-comparisons' );
607
+
608
+ $factory = $this->get_package_factory();
609
+ $comparator = $this->get_comparator();
610
+ $packages = $factory->find_packages_for_files( $this->get_storage()->get_cursor( 'files' ) );
611
+
612
+ foreach ( $packages as $root => $group ) {
613
+ /** @var ITSEC_File_Change_Package $package */
614
+ $package = $group['package'];
615
+ $files = $group['files'];
616
+
617
+ if ( ! $comparator->supports_package( $package ) ) {
618
+ $packages[ $root ]['files'] = $this->set_default_severity( $files );
619
+ continue;
620
+ }
621
+
622
+ if ( $comparator instanceof ITSEC_File_Change_Hash_Comparator_Loadable ) {
623
+ try {
624
+ $comparator->load( $package );
625
+ } catch ( ITSEC_File_Change_Hash_Loading_Failed_Exception $e ) {
626
+ $packages[ $root ]['files'] = $this->set_default_severity( $files );
627
+ $this->add_process_update( array( 'status' => 'hash_load_failed', 'e' => (string) $e ) );
628
+ continue;
629
+ }
630
+ }
631
+
632
+ // $file is a relative path to the package.
633
+ // $attr contains 'h' for the hash, and 'd' for the date modified.
634
+ foreach ( $files as $file => $attr ) {
635
+ switch ( $attr['t'] ) {
636
+ case self::T_ADDED:
637
+ if ( ! $comparator->has_hash( $file, $package ) ) {
638
+ $attr['s'] = self::S_UNKNOWN_FILE;
639
+ break;
640
+ }
641
+
642
+ if ( ! $comparator->hash_matches( $attr['h'], $file, $package ) ) {
643
+ // This isn't exactly an unknown file, or a bad change, but it fits more with bad change,
644
+ // and is unlikely to occur so not worth a separate report type.
645
+ $attr['s'] = self::S_BAD_CHANGE;
646
+ break;
647
+ }
648
+
649
+ $attr['s'] = self::S_NONE;
650
+ break;
651
+ case self::T_CHANGED:
652
+ if ( ! $comparator->has_hash( $file, $package ) ) {
653
+ break;
654
+ }
655
+
656
+ if ( ! $comparator->hash_matches( $attr['h'], $file, $package ) ) {
657
+ $attr['s'] = self::S_BAD_CHANGE;
658
+ break;
659
+ }
660
+ $attr['s'] = self::S_NONE;
661
+ break;
662
+ case self::T_REMOVED:
663
+ if ( ! $comparator->has_hash( $file, $package ) ) {
664
+ $attr['s'] = self::S_NONE;
665
+ }
666
+ break;
667
+ }
668
+
669
+ if ( ! isset( $attr['s'] ) ) {
670
+ $attr['s'] = self::S_NORMAL;
671
+ }
672
+
673
+ $files[ $file ] = $attr;
674
+ }
675
+
676
+ $packages[ $root ]['files'] = $files;
677
  }
678
 
679
+ do_action( 'itsec-file-change-end-hash-comparisons' );
680
 
681
+ $this->add_process_update( array( 'status' => 'hash_comparisons_complete' ) );
682
+ $this->storage->set( 'max_severity', $this->get_max_severity( $packages ) );
683
+ $this->storage->set( 'change_list', $this->build_change_list( $packages ) );
684
 
685
+ $job->schedule_next_in_loop( array( 'step' => 'complete' ) );
686
+ }
687
 
688
+ /**
689
+ * Run the completion routine.
690
+ *
691
+ * @param ITSEC_Job $job
692
+ */
693
+ private function complete( ITSEC_Job $job ) {
694
 
695
+ $this->add_process_update( array( 'status' => 'start_complete' ) );
 
696
 
697
+ $storage = $this->get_storage();
698
+ self::record_file_list( $storage->get_cursor( 'file_list' ) );
699
 
700
+ $list = $storage->get( 'change_list' );
701
+
702
+ $list['memory'] = round( ( $storage->get( 'memory' ) / 1000000 ), 2 );
703
+ $list['memory_peak'] = round( ( $storage->get( 'memory_peak' ) / 1000000 ), 2 );
704
+
705
+ $c_added = count( $list['added'] );
706
+ $c_changed = count( $list['changed'] );
707
+ $c_removed = count( $list['removed'] );
708
 
709
+ $found_changes = $c_added || $c_changed || $c_removed;
710
+
711
+ if ( $found_changes ) {
712
 
713
+ $severity = $storage->get( 'max_severity' );
714
+
715
+ if ( $severity > self::S_UNKNOWN_FILE ) {
716
+ $method = 'add_critical_issue';
717
+ } else {
718
+ $method = 'add_warning';
719
  }
720
 
721
+ $id = ITSEC_Log::$method( 'file_change', "changes-found::{$c_added},{$c_removed},{$c_changed}", $list );
722
  } else {
723
+ $id = ITSEC_Log::add_notice( 'file_change', 'no-changes-found', $list );
724
+ }
725
 
726
+ ITSEC_Modules::set_setting( 'file-change', 'last_scan', $found_changes ? $id : 0 );
727
+ update_site_option( 'itsec_file_change_latest', $list );
728
 
729
+ if ( $found_changes && $this->settings['notify_admin'] ) {
730
+ ITSEC_Modules::set_setting( 'file-change', 'show_warning', true );
731
  }
732
 
733
+ if ( $process = $storage->get( 'process' ) ) {
734
+ ITSEC_Log::add_process_stop( $process );
735
+ }
736
+
737
+ $storage->clear();
738
+
739
+ if ( 'file-change' === $job->get_id() ) {
740
+ $job->schedule_new_loop( array(
741
+ 'step' => 'get-files',
742
+ 'chunk' => self::C_ADMIN,
743
+ ) );
744
+ }
745
+
746
+ $this->send_notification_email( array( $c_added, $c_removed, $c_changed, $list ) );
747
  }
748
 
749
  /**
750
+ * Get the comparator to use to check if changes are expected.
 
 
751
  *
752
+ * Handles lazily setting the comparator since it is not needed for all stages of the file change scan.
753
  *
754
+ * @return ITSEC_File_Change_Hash_Comparator
 
 
755
  */
756
+ private function get_comparator() {
757
+ if ( ! $this->comparator ) {
758
+ $comparators = array(
759
+ new ITSEC_File_Change_Hash_Comparator_Managed_Files(),
760
+ );
761
 
762
+ /**
763
+ * Filter the list of comparators to use.
764
+ */
765
+ $comparators = apply_filters( 'itsec_file_change_comparators', $comparators );
766
+
767
+ $this->comparator = new ITSEC_File_Change_Hash_Comparator_Chain( $comparators );
768
+ }
769
+
770
+ return $this->comparator;
771
  }
772
 
773
  /**
774
+ * Get the Package factory.
 
 
 
775
  *
776
+ * @return ITSEC_File_Change_Package_Factory
777
+ */
778
+ private function get_package_factory() {
779
+ if ( ! $this->package_factory ) {
780
+ $this->package_factory = new ITSEC_File_Change_Package_Factory();
781
+ }
782
+
783
+ return $this->package_factory;
784
+ }
785
+
786
+ /**
787
+ * Get the Chunk Scanner.
788
  *
789
+ * @return ITSEC_File_Change_Chunk_Scanner
790
+ */
791
+ private function get_chunk_scanner() {
792
+ if ( ! $this->chunk_scanner ) {
793
+ $this->chunk_scanner = new ITSEC_File_Change_Chunk_Scanner( $this->settings );
794
+ }
795
+
796
+ return $this->chunk_scanner;
797
+ }
798
+
799
+ /**
800
+ * Get the main storage mechanism.
801
  *
802
+ * @return ITSEC_Lib_Distributed_Storage
803
  */
804
+ private function get_storage() {
805
 
806
+ if ( null === $this->storage ) {
807
+ $this->storage = ITSEC_File_Change::make_progress_storage();
808
+ }
 
 
 
 
809
 
810
+ return $this->storage;
811
+ }
812
 
813
+ /**
814
+ * Set the default severity for a list of files.
815
+ *
816
+ * @param array $files
817
+ *
818
+ * @return array
819
+ */
820
+ private function set_default_severity( $files ) {
821
+ foreach ( $files as $file => $attr ) {
822
+ $files[ $file ]['s'] = self::S_NORMAL;
823
+ }
824
 
825
+ return $files;
826
+ }
827
 
828
+ /**
829
+ * Get the maximum severity level of a file change.
830
+ *
831
+ * @param array $packaged
832
+ *
833
+ * @return int
834
+ */
835
+ private function get_max_severity( $packaged ) {
836
 
837
+ $severity = self::S_NONE;
 
 
 
 
838
 
839
+ foreach ( $packaged as $root => $group ) {
840
+ foreach ( $group['files'] as $attr ) {
841
+ if ( $attr['s'] > $severity ) {
842
+ $severity = $attr['s'];
843
+ }
844
  }
 
845
  }
846
 
847
+ return $severity;
 
 
848
  }
849
 
850
  /**
851
+ * Convert a list of packages and their files to a list of the file change types.
 
 
 
 
 
 
 
 
 
852
  *
853
+ * @param array $packaged
854
  *
855
+ * @return array
856
  */
857
+ private function build_change_list( $packaged ) {
 
 
 
858
 
859
+ require_once( ABSPATH . 'wp-admin/includes/file.php' );
860
+ $home = get_home_path();
861
 
862
+ $list = array(
863
+ 'added' => array(),
864
+ 'removed' => array(),
865
+ 'changed' => array(),
866
+ );
867
 
868
+ foreach ( $packaged as $root => $group ) {
869
+ /** @var ITSEC_File_Change_Package $package */
870
+ $package = $group['package'];
871
 
872
+ foreach ( $group['files'] as $file => $attr ) {
873
+ if ( $attr['s'] > self::S_NONE && ! empty( $attr['t'] ) ) {
874
+ $path = $package->get_root_path() . $file;
875
 
876
+ if ( 0 === strpos( $path, $home ) ) {
877
+ $path = substr( $path, strlen( $home ) );
878
+ }
879
 
880
+ $attr['p'] = (string) $package;
881
+
882
+ switch ( $attr['t'] ) {
883
+ case self::T_ADDED:
884
+ $list['added'][ $path ] = $attr;
885
+ break;
886
+ case self::T_CHANGED:
887
+ $list['changed'][ $path ] = $attr;
888
+ break;
889
+ case self::T_REMOVED:
890
+ $list['removed'][ $path ] = $attr;
891
+ }
892
+ }
893
  }
894
+ }
895
 
896
+ return $list;
897
+ }
898
 
899
+ private function add_process_update( $data = false ) {
900
+ if ( $process = $this->get_storage()->get( 'process' ) ) {
901
+ ITSEC_Log::add_process_update( $process, $data );
902
+ }
903
+ }
904
 
905
+ /**
906
+ * Make the storage for recording the static list of files and their hashes.
907
+ *
908
+ * @return ITSEC_Lib_Distributed_Storage
909
+ */
910
+ public static function make_file_list_storage() {
911
+ return new ITSEC_Lib_Distributed_Storage( 'file-list', array(
912
+ 'home' => array(),
913
+ 'files' => array(
914
+ 'split' => true,
915
+ 'chunk' => 2500,
916
+ 'serialize' => 'wp_json_encode',
917
+ 'unserialize' => 'ITSEC_File_Change::_json_decode_associative',
918
+ ),
919
+ ) );
920
+ }
921
 
922
+ /**
923
+ * Record a list of file hashes and change times.
924
+ *
925
+ * This should not be done until the whole scan process is complete.
926
+ *
927
+ * @param iterable $file_list
928
+ *
929
+ * @return bool
930
+ */
931
+ public static function record_file_list( $file_list ) {
932
 
933
+ $storage = self::make_file_list_storage();
934
+ $storage->set( 'home', get_home_path() );
935
 
936
+ if ( is_array( $file_list ) ) {
937
+ return $storage->set( 'files', $file_list );
938
+ }
939
 
940
+ return $storage->set_from_iterator( 'files', $file_list );
941
+ }
942
 
943
+ /**
944
+ * Get the file list we want to compare our newly compared files to.
945
+ *
946
+ * This is in effect the last change list recorded.
947
+ *
948
+ * @return array
949
+ */
950
+ public static function get_file_list_to_compare() {
951
 
952
+ $storage = self::make_file_list_storage();
953
+ $files = $storage->get( 'files' );
954
+
955
+ if ( ! $files ) {
956
+ return array();
957
+ }
958
+
959
+ $home = $storage->get( 'home' );
960
 
961
+ if ( $home === get_home_path() ) {
962
+ return $files;
963
  }
964
 
965
+ $new_home = get_home_path();
966
+ $updated = array();
967
+
968
+ foreach ( $files as $file => $attr ) {
969
+ $updated[ ITSEC_Lib::replace_prefix( $file, $home, $new_home ) ] = $attr;
970
+ }
971
 
972
+ $storage->set( 'files', $updated );
973
+ $storage->set( 'home', $new_home );
974
 
975
+ return $updated;
976
  }
977
 
978
  /**
993
 
994
  $changed = $email_details[0] + $email_details[1] + $email_details[2];
995
 
996
+ if ( ! $changed ) {
997
  return;
998
  }
999
 
1074
 
1075
  return $rows;
1076
  }
1077
+ }
core/modules/file-change/settings-page.php CHANGED
@@ -1,7 +1,7 @@
1
  <?php
2
 
3
  final class ITSEC_File_Change_Settings_Page extends ITSEC_Module_Settings_Page {
4
- private $script_version = 2;
5
 
6
 
7
  public function __construct() {
@@ -14,22 +14,20 @@ final class ITSEC_File_Change_Settings_Page extends ITSEC_Module_Settings_Page {
14
  }
15
 
16
  public function enqueue_scripts_and_styles() {
17
- $settings = ITSEC_Modules::get_settings( $this->id );
18
 
19
- $logs_page_url = ITSEC_Core::get_logs_page_url( 'file_change' );
20
 
21
  $vars = array(
22
- 'button_text' => isset( $settings['split'] ) && true === $settings['split'] ? __( 'Scan Next File Chunk', 'better-wp-security' ) : __( 'Scan Files Now', 'better-wp-security' ),
23
- 'scanning_button_text' => __( 'Scanning...', 'better-wp-security' ),
24
- 'no_changes' => __( 'No changes were detected.', 'better-wp-security' ),
25
- 'found_changes' => sprintf( __( 'Changes were detected. Please check the <a href="%s" target="_blank" rel="noopener noreferrer">logs page</a> for details.', 'better-wp-security' ), esc_url( $logs_page_url ) ),
26
- 'unknown_error' => __( 'An unknown error occured. Please try again later', 'better-wp-security' ),
27
- 'already_running' => sprintf( __( 'A scan is already in progress. Please check the <a href="%s" target="_blank" rel="noopener noreferrer">logs page</a> at a later time for the results of the scan.', 'better-wp-security' ), esc_url( $logs_page_url ) ),
28
- 'ABSPATH' => ITSEC_Lib::get_home_path(),
29
- 'nonce' => wp_create_nonce( 'itsec_do_file_check' ),
30
  );
31
 
32
- wp_enqueue_script( 'itsec-file-change-settings-script', plugins_url( 'js/settings-page.js', __FILE__ ), array( 'jquery' ), $this->script_version, true );
 
 
 
 
 
33
  wp_localize_script( 'itsec-file-change-settings-script', 'itsec_file_change_settings', $vars );
34
 
35
 
@@ -49,7 +47,13 @@ final class ITSEC_File_Change_Settings_Page extends ITSEC_Module_Settings_Page {
49
  if ( 'one-time-scan' === $data['method'] ) {
50
  require_once( dirname( __FILE__ ) . '/scanner.php' );
51
 
52
- ITSEC_Response::set_response( ITSEC_File_Change_Scanner::run_scan( false ) );
 
 
 
 
 
 
53
  } else if ( 'get-filetree-data' === $data['method'] ) {
54
  ITSEC_Response::set_response( $this->get_filetree_data( $data ) );
55
  }
@@ -64,11 +68,6 @@ final class ITSEC_File_Change_Settings_Page extends ITSEC_Module_Settings_Page {
64
  }
65
 
66
  protected function render_settings( $form ) {
67
- $methods = array(
68
- 'exclude' => __( 'Exclude Selected', 'better-wp-security' ),
69
- 'include' => __( 'Include Selected', 'better-wp-security' ),
70
- );
71
-
72
 
73
  $file_list = $form->get_option( 'file_list' );
74
 
@@ -80,33 +79,30 @@ final class ITSEC_File_Change_Settings_Page extends ITSEC_Module_Settings_Page {
80
 
81
  $form->set_option( 'file_list', $file_list );
82
 
83
- $split = $form->get_option( 'split' );
84
- $one_time_button_label = ( true === $split ) ? __( 'Scan Next File Chunk', 'better-wp-security' ) : __( 'Scan Files Now', 'better-wp-security' )
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  ?>
87
  <div class="hide-if-no-js">
88
  <p><?php _e( "Press the button below to scan your site's files for changes. Note that if changes are found this will take you to the logs page for details.", 'better-wp-security' ); ?></p>
89
- <p><?php $form->add_button( 'one_time_check', array( 'value' => $one_time_button_label, 'class' => 'button-primary' ) ); ?></p>
90
  <div id="itsec_file_change_status"></div>
91
  </div>
92
 
93
  <table class="form-table itsec-settings-section">
94
- <tr>
95
- <th scope="row"><label for="itsec-file-change-split"><?php _e( 'Split File Scanning', 'better-wp-security' ); ?></label></th>
96
- <td>
97
- <?php $form->add_checkbox( 'split' ); ?>
98
- <label for="itsec-file-change-split"><?php _e( 'Split file checking into chunks.', 'better-wp-security' ); ?></label>
99
- <p class="description"><?php _e( 'Splits file checking into 7 chunks (plugins, themes, wp-admin, wp-includes, uploads, the rest of wp-content and everything that is left over) and divides the checks evenly over the course of a day. This feature may result in more notifications but will allow for the scanning of bigger sites to continue even on a lower-end web host.', 'better-wp-security' ); ?></p>
100
- </td>
101
- </tr>
102
- <tr>
103
- <th scope="row"><label for="itsec-file-change-method"><?php _e( 'Include/Exclude Files and Folders', 'better-wp-security' ); ?></label></th>
104
- <td>
105
- <?php $form->add_select( 'method', $methods ); ?>
106
- <label for="itsec-file-change-method"><?php _e( 'Include/Exclude Files', 'better-wp-security' ); ?></label>
107
- <p class="description"><?php _e( 'Select whether we should exclude files and folders selected or whether the scan should only include files and folders selected.', 'better-wp-security' ); ?></p>
108
- </td>
109
- </tr>
110
  <tr>
111
  <th scope="row"><?php _e( 'Files and Folders List', 'better-wp-security' ); ?></th>
112
  <td>
@@ -155,7 +151,8 @@ final class ITSEC_File_Change_Settings_Page extends ITSEC_Module_Settings_Page {
155
  $directory = urldecode( $directory );
156
  $directory = realpath( $directory );
157
 
158
- $base_directory = realpath( ITSEC_Lib::get_home_path() );
 
159
 
160
  // Ensure that requests cannot traverse arbitrary directories.
161
  if ( 0 !== strpos( $directory, $base_directory ) ) {
@@ -181,13 +178,20 @@ final class ITSEC_File_Change_Settings_Page extends ITSEC_Module_Settings_Page {
181
  // All files and directories (alphabetical sorting)
182
  foreach ( $files as $file ) {
183
 
184
- if ( '.' != $file && '..' != $file && file_exists( $directory . $file ) && is_dir( $directory . $file ) ) {
 
 
185
 
186
- echo '<li class="directory collapsed"><a href="#" rel="' . htmlentities( $directory . $file ) . '/">' . htmlentities( $file ) . '<div class="itsec_treeselect_control"><img src="' . plugins_url( 'images/redminus.png', __FILE__ ) . '" style="vertical-align: -3px;" title="Add to exclusions..." class="itsec_filetree_exclude"></div></a></li>';
 
 
187
 
188
- } elseif ( '.' != $file && '..' != $file && file_exists( $directory . $file ) && ! is_dir( $directory . $file ) ) {
189
 
190
- $ext = preg_replace( '/^.*\./', '', $file );
 
 
 
191
  echo '<li class="file ext_' . $ext . '"><a href="#" rel="' . htmlentities( $directory . $file ) . '">' . htmlentities( $file ) . '<div class="itsec_treeselect_control"><img src="' . plugins_url( 'images/redminus.png', __FILE__ ) . '" style="vertical-align: -3px;" title="Add to exclusions..." class="itsec_filetree_exclude"></div></a></li>';
192
 
193
  }
@@ -203,7 +207,6 @@ final class ITSEC_File_Change_Settings_Page extends ITSEC_Module_Settings_Page {
203
  return ob_get_clean();
204
 
205
  }
206
-
207
  }
208
 
209
  new ITSEC_File_Change_Settings_Page();
1
  <?php
2
 
3
  final class ITSEC_File_Change_Settings_Page extends ITSEC_Module_Settings_Page {
4
+ private $script_version = 3;
5
 
6
 
7
  public function __construct() {
14
  }
15
 
16
  public function enqueue_scripts_and_styles() {
 
17
 
18
+ require_once( ABSPATH . 'wp-admin/includes/file.php' );
19
 
20
  $vars = array(
21
+ 'ABSPATH' => get_home_path(),
22
+ 'nonce' => wp_create_nonce( 'itsec_do_file_check' ),
 
 
 
 
 
 
23
  );
24
 
25
+ if ( ! class_exists( 'ITSEC_File_Change_Admin' ) ) {
26
+ require_once( dirname( __FILE__ ) . '/admin.php' );
27
+ }
28
+
29
+ ITSEC_File_Change_Admin::enqueue_scanner();
30
+ wp_enqueue_script( 'itsec-file-change-settings-script', plugins_url( 'js/settings-page.js', __FILE__ ), array( 'jquery', 'itsec-file-change-scanner' ), $this->script_version, true );
31
  wp_localize_script( 'itsec-file-change-settings-script', 'itsec_file_change_settings', $vars );
32
 
33
 
47
  if ( 'one-time-scan' === $data['method'] ) {
48
  require_once( dirname( __FILE__ ) . '/scanner.php' );
49
 
50
+ $results = ITSEC_File_Change_Scanner::schedule_start();
51
+
52
+ if ( is_wp_error( $results ) ) {
53
+ ITSEC_Response::add_error( $results );
54
+ } else {
55
+ ITSEC_Response::set_success( true );
56
+ }
57
  } else if ( 'get-filetree-data' === $data['method'] ) {
58
  ITSEC_Response::set_response( $this->get_filetree_data( $data ) );
59
  }
68
  }
69
 
70
  protected function render_settings( $form ) {
 
 
 
 
 
71
 
72
  $file_list = $form->get_option( 'file_list' );
73
 
79
 
80
  $form->set_option( 'file_list', $file_list );
81
 
82
+ require_once( dirname( __FILE__ ) . '/scanner.php' );
 
83
 
84
+ if ( ITSEC_File_Change_Scanner::is_running() ) {
85
+ $status = ITSEC_File_Change_Scanner::get_status();
86
+
87
+ $button = array(
88
+ 'value' => empty( $status['message'] ) ? __( 'Scan in Progress', 'better-wp-security' ) : $status['message'],
89
+ 'disabled' => 'disabled',
90
+ 'class' => 'button-secondary',
91
+ );
92
+ } else {
93
+ $button = array(
94
+ 'value' => __( 'Scan Files Now', 'better-wp-security' ),
95
+ 'class' => 'button-primary',
96
+ );
97
+ }
98
  ?>
99
  <div class="hide-if-no-js">
100
  <p><?php _e( "Press the button below to scan your site's files for changes. Note that if changes are found this will take you to the logs page for details.", 'better-wp-security' ); ?></p>
101
+ <p><?php $form->add_button( 'one_time_check', $button ); ?></p>
102
  <div id="itsec_file_change_status"></div>
103
  </div>
104
 
105
  <table class="form-table itsec-settings-section">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  <tr>
107
  <th scope="row"><?php _e( 'Files and Folders List', 'better-wp-security' ); ?></th>
108
  <td>
151
  $directory = urldecode( $directory );
152
  $directory = realpath( $directory );
153
 
154
+ require_once( ABSPATH . 'wp-admin/includes/file.php' );
155
+ $base_directory = get_home_path();
156
 
157
  // Ensure that requests cannot traverse arbitrary directories.
158
  if ( 0 !== strpos( $directory, $base_directory ) ) {
178
  // All files and directories (alphabetical sorting)
179
  foreach ( $files as $file ) {
180
 
181
+ if ( '.' === $file || '..' === $file ) {
182
+ continue;
183
+ }
184
 
185
+ if ( ! file_exists( $directory . $file ) ) {
186
+ continue;
187
+ }
188
 
189
+ if ( is_dir( $directory . $file ) ) {
190
 
191
+ echo '<li class="directory collapsed"><a href="#" rel="' . htmlentities( $directory . $file ) . '/">' . htmlentities( $file ) . '<div class="itsec_treeselect_control"><img src="' . plugins_url( 'images/redminus.png', __FILE__ ) . '" style="vertical-align: -3px;" title="Add to exclusions..." class="itsec_filetree_exclude"></div></a></li>';
192
+
193
+ } else {
194
+ $ext = pathinfo( $file, PATHINFO_EXTENSION );
195
  echo '<li class="file ext_' . $ext . '"><a href="#" rel="' . htmlentities( $directory . $file ) . '">' . htmlentities( $file ) . '<div class="itsec_treeselect_control"><img src="' . plugins_url( 'images/redminus.png', __FILE__ ) . '" style="vertical-align: -3px;" title="Add to exclusions..." class="itsec_filetree_exclude"></div></a></li>';
196
 
197
  }
207
  return ob_get_clean();
208
 
209
  }
 
210
  }
211
 
212
  new ITSEC_File_Change_Settings_Page();
core/modules/file-change/settings.php CHANGED
@@ -7,33 +7,23 @@ final class ITSEC_File_Change_Settings extends ITSEC_Settings {
7
 
8
  public function get_defaults() {
9
  return array(
10
- 'split' => false,
11
- 'method' => 'exclude',
12
- 'file_list' => array(),
13
- 'types' => array(
14
- '.jpg',
15
- '.jpeg',
16
- '.png',
17
- '.log',
18
- '.mo',
19
- '.po'
20
- ),
21
- 'notify_admin' => true,
22
- 'last_run' => 0,
23
- 'last_chunk' => false,
24
- 'show_warning' => false,
25
- 'latest_changes' => array(),
26
- );
27
- }
28
 
29
- protected function handle_settings_changes( $old_settings ) {
30
- $split = isset( $old_settings['split'] ) ? $old_settings['split'] : false;
31
 
32
- if ( $split !== $this->settings['split'] ) {
33
- ITSEC_Core::get_scheduler()->unschedule( 'file-change' );
34
- $interval = $this->settings['split'] ? ITSEC_Scheduler::S_FOUR_DAILY : ITSEC_Scheduler::S_DAILY;
35
- ITSEC_Core::get_scheduler()->schedule( $interval, 'file-change' );
36
- }
 
 
 
37
  }
38
  }
39
 
7
 
8
  public function get_defaults() {
9
  return array(
10
+ 'file_list' => array(),
11
+ 'types' => array(
12
+ '.log', '.mo', '.po',
13
+ // Images
14
+ '.bmp', '.gif', '.ico', '.jpe', '.jpeg', '.jpg', '.png', '.psd', '.raw', '.svg', '.tif', '.tiff',
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ // Audio
17
+ '.aif', '.flac', '.m4a', '.mp3', '.oga', '.ogg', '.ogg', '.ra', '.wav', '.wma',
18
 
19
+ // Video
20
+ '.asf', '.avi', '.mkv', '.mov', '.mp4', '.mpe', '.mpeg', '.mpg', '.ogv', '.qt', '.rm', '.vob', '.webm', '.wm', '.wmv',
21
+ ),
22
+ 'notify_admin' => true,
23
+ 'show_warning' => false,
24
+ 'expected_hashes' => array(),
25
+ 'last_scan' => 0,
26
+ );
27
  }
28
  }
29
 
core/modules/file-change/setup.php CHANGED
@@ -9,10 +9,10 @@ if ( ! class_exists( 'ITSEC_File_Change_Setup' ) ) {
9
 
10
  public function __construct() {
11
 
12
- add_action( 'itsec_modules_do_plugin_activation', array( $this, 'execute_activate' ) );
13
- add_action( 'itsec_modules_do_plugin_deactivation', array( $this, 'execute_deactivate' ) );
14
- add_action( 'itsec_modules_do_plugin_uninstall', array( $this, 'execute_uninstall' ) );
15
- add_action( 'itsec_modules_do_plugin_upgrade', array( $this, 'execute_upgrade' ), null, 2 );
16
 
17
  }
18
 
@@ -35,6 +35,8 @@ if ( ! class_exists( 'ITSEC_File_Change_Setup' ) ) {
35
 
36
  wp_clear_scheduled_hook( 'itsec_file_check' );
37
 
 
 
38
  }
39
 
40
  /**
@@ -57,6 +59,11 @@ if ( ! class_exists( 'ITSEC_File_Change_Setup' ) ) {
57
  delete_site_option( 'itsec_local_file_list_6' );
58
  delete_site_option( 'itsec_file_change_warning' );
59
 
 
 
 
 
 
60
  }
61
 
62
  /**
@@ -140,7 +147,7 @@ if ( ! class_exists( 'ITSEC_File_Change_Setup' ) ) {
140
 
141
  // This used to be boolean. Attempt to migrate to new string, falling back to default
142
  if ( ! is_array( $current_options['method'] ) ) {
143
- $current_options['method'] = ( $current_options['method'] )? 'exclude' : 'include';
144
  } elseif ( ! in_array( $current_options['method'], array( 'include', 'exclude' ) ) ) {
145
  $current_options['method'] = 'exclude';
146
  }
@@ -153,10 +160,193 @@ if ( ! class_exists( 'ITSEC_File_Change_Setup' ) ) {
153
  wp_clear_scheduled_hook( 'itsec_execute_file_check_cron' );
154
  }
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  }
157
 
158
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
 
162
  new ITSEC_File_Change_Setup();
9
 
10
  public function __construct() {
11
 
12
+ add_action( 'itsec_modules_do_plugin_activation', array( $this, 'execute_activate' ) );
13
+ add_action( 'itsec_modules_do_plugin_deactivation', array( $this, 'execute_deactivate' ) );
14
+ add_action( 'itsec_modules_do_plugin_uninstall', array( $this, 'execute_uninstall' ) );
15
+ add_action( 'itsec_modules_do_plugin_upgrade', array( $this, 'execute_upgrade' ), null, 2 );
16
 
17
  }
18
 
35
 
36
  wp_clear_scheduled_hook( 'itsec_file_check' );
37
 
38
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change', null );
39
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change-fast', null );
40
  }
41
 
42
  /**
59
  delete_site_option( 'itsec_local_file_list_6' );
60
  delete_site_option( 'itsec_file_change_warning' );
61
 
62
+ require_once( dirname( __FILE__ ) . '/scanner.php' );
63
+
64
+ ITSEC_Lib_Distributed_Storage::clear_group( 'file-change-progress' );
65
+ ITSEC_Lib_Distributed_Storage::clear_group( 'file-list' );
66
+ delete_site_option( ITSEC_File_Change_Scanner::DESTROYED );
67
  }
68
 
69
  /**
147
 
148
  // This used to be boolean. Attempt to migrate to new string, falling back to default
149
  if ( ! is_array( $current_options['method'] ) ) {
150
+ $current_options['method'] = ( $current_options['method'] ) ? 'exclude' : 'include';
151
  } elseif ( ! in_array( $current_options['method'], array( 'include', 'exclude' ) ) ) {
152
  $current_options['method'] = 'exclude';
153
  }
160
  wp_clear_scheduled_hook( 'itsec_execute_file_check_cron' );
161
  }
162
 
163
+ if ( $itsec_old_version < 4088 ) {
164
+ $types = ITSEC_Modules::get_setting( 'file-change', 'types' );
165
+ $defaults = array( '.jpg', '.jpeg', '.png', '.log', '.mo', '.po' );
166
+
167
+ sort( $types );
168
+ sort( $defaults );
169
+
170
+ $update = false;
171
+
172
+ if ( $types === $defaults ) {
173
+ $update = true;
174
+ } else {
175
+ $defaults[] = '.lock';
176
+
177
+ sort( $defaults );
178
+
179
+ if ( $types === $defaults ) {
180
+ $update = true;
181
+ }
182
+ }
183
+
184
+ if ( $update ) {
185
+ ITSEC_Modules::set_setting( 'file-change', 'types', ITSEC_Modules::get_default( 'file-change', 'types' ) );
186
+ }
187
+
188
+ require_once( dirname( __FILE__ ) . '/scanner.php' );
189
+
190
+ $options = array(
191
+ 'itsec_local_file_list',
192
+ 'itsec_local_file_list_0',
193
+ 'itsec_local_file_list_1',
194
+ 'itsec_local_file_list_2',
195
+ 'itsec_local_file_list_3',
196
+ 'itsec_local_file_list_4',
197
+ 'itsec_local_file_list_5',
198
+ 'itsec_local_file_list_6',
199
+ );
200
+ $file_list = array();
201
+
202
+ $home = get_home_path();
203
+
204
+ foreach ( $options as $option ) {
205
+ $opt_list = get_site_option( $option );
206
+
207
+ if ( $opt_list && is_array( $opt_list ) ) {
208
+ foreach ( $opt_list as $file => $attr ) {
209
+ $file_list[ $home . $file ] = $attr;
210
+ }
211
+ }
212
+ }
213
+
214
+ if ( $file_list ) {
215
+ ITSEC_File_Change_Scanner::record_file_list( $file_list );
216
+ }
217
+
218
+ ITSEC_Core::get_scheduler()->unschedule( 'file-change' );
219
+ ITSEC_File_Change_Scanner::schedule_start( false );
220
+ }
221
+
222
+ if ( $itsec_old_version < 4090 ) {
223
+ require_once( dirname( __FILE__ ) . '/scanner.php' );
224
+
225
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change', null );
226
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change-fast', null );
227
+ ITSEC_Lib_Distributed_Storage::clear_group( 'file-change-progress' );
228
+
229
+ $file_list_option = get_site_option( 'itsec_file_list' );
230
+
231
+ if ( $file_list_option && ! empty( $file_list_option['files'] ) ) {
232
+ $files = end( $file_list_option['files'] );
233
+ $home = $file_list_option['home'];
234
+
235
+ if ( $home !== get_home_path() ) {
236
+ $new_home = get_home_path();
237
+
238
+ foreach ( $files as $file => $attr ) {
239
+ $files[ ITSEC_Lib::replace_prefix( $file, $home, $new_home ) ] = $attr;
240
+ }
241
+ }
242
+
243
+ ITSEC_File_Change_Scanner::record_file_list( $this->migrate_file_attr( $files ) );
244
+ }
245
+
246
+ delete_site_option( 'itsec_file_list' );
247
+
248
+ if ( $latest_changes = ITSEC_Modules::get_setting( 'file-change', 'latest_changes' ) ) {
249
+
250
+ if ( ! empty( $latest_changes['added'] ) && is_array( $latest_changes['added'] ) ) {
251
+ $latest_changes['added'] = $this->migrate_file_attr( $latest_changes['added'] );
252
+ } else {
253
+ $latest_changes['added'] = array();
254
+ }
255
+
256
+ if ( ! empty( $latest_changes['changed'] ) && is_array( $latest_changes['changed'] ) ) {
257
+ $latest_changes['changed'] = $this->migrate_file_attr( $latest_changes['changed'] );
258
+ } else {
259
+ $latest_changes['changed'] = array();
260
+ }
261
+
262
+ if ( ! empty( $latest_changes['removed'] ) && is_array( $latest_changes['removed'] ) ) {
263
+ $latest_changes['removed'] = $this->migrate_file_attr( $latest_changes['removed'] );
264
+ } else {
265
+ $latest_changes['removed'] = array();
266
+ }
267
+
268
+ update_site_option( 'itsec_file_change_latest', $latest_changes );
269
+ }
270
+
271
+ ITSEC_File_Change_Scanner::schedule_start( false );
272
+ } elseif ( $itsec_old_version < 4091 ) {
273
+ $settings = ITSEC_Modules::get_settings( 'file-change' );
274
+
275
+ if ( array_key_exists( 'latest_changes', $settings ) ) {
276
+
277
+ if ( $latest_changes = $settings['latest_changes'] ) {
278
+ update_site_option( 'itsec_file_change_latest', $latest_changes );
279
+ }
280
+
281
+ unset( $settings['latest_changes'] );
282
+ ITSEC_Modules::set_settings( 'file-change', $settings );
283
+ }
284
+ }
285
+
286
+ if ( $itsec_old_version < 4093 ) {
287
+ require_once( dirname( __FILE__ ) . '/scanner.php' );
288
+
289
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change', null );
290
+ ITSEC_Core::get_scheduler()->unschedule_single( 'file-change-fast', null );
291
+ ITSEC_File_Change_Scanner::schedule_start( false );
292
+ delete_site_option( 'itsec_file_change_scan_progress' );
293
+ }
294
  }
295
 
296
+ /**
297
+ * Migrate file attributes to the shorter format.
298
+ *
299
+ * @param array $files
300
+ *
301
+ * @return array
302
+ */
303
+ private function migrate_file_attr( $files ) {
304
+
305
+ $changed = array();
306
+
307
+ foreach ( $files as $file => $attr ) {
308
+ $migrated = array(
309
+ 'h' => $attr['h'],
310
+ 'd' => $attr['d'],
311
+ );
312
+
313
+ if ( isset( $attr['s'] ) ) {
314
+ $migrated['s'] = $attr['s'];
315
+ } elseif ( isset( $attr['severity'] ) ) {
316
+ $migrated['s'] = $attr['severity'];
317
+ }
318
 
319
+ if ( isset( $attr['t'] ) ) {
320
+ $migrated['t'] = $attr['t'];
321
+ } elseif ( isset( $attr['type'] ) ) {
322
+ switch ( $attr['type'] ) {
323
+ case 'added':
324
+ $migrated['t'] = ITSEC_File_Change_Scanner::T_ADDED;
325
+ break;
326
+ case 'changed':
327
+ $migrated['t'] = ITSEC_File_Change_Scanner::T_CHANGED;
328
+ break;
329
+ case 'removed':
330
+ $migrated['t'] = ITSEC_File_Change_Scanner::T_REMOVED;
331
+ break;
332
+ default:
333
+ $migrated['t'] = $attr['type'];
334
+ break;
335
+ }
336
+ }
337
+
338
+ if ( isset( $attr['p'] ) ) {
339
+ $migrated['p'] = $attr['p'];
340
+ } elseif ( isset( $attr['package'] ) ) {
341
+ $migrated['p'] = $attr['package'];
342
+ }
343
+
344
+ $changed[ $file ] = $migrated;
345
+ }
346
+
347
+ return $changed;
348
+ }
349
+ }
350
  }
351
 
352
  new ITSEC_File_Change_Setup();
core/modules/file-change/sync-verbs/itsec-perform-file-scan.php CHANGED
@@ -7,6 +7,6 @@ class Ithemes_Sync_Verb_ITSEC_Perform_File_Scan extends Ithemes_Sync_Verb {
7
  public function run( $arguments ) {
8
  require_once( dirname( dirname( __FILE__ ) ) . '/scanner.php' );
9
 
10
- return ITSEC_File_Change_Scanner::run_scan( false, true );
11
  }
12
  }
7
  public function run( $arguments ) {
8
  require_once( dirname( dirname( __FILE__ ) ) . '/scanner.php' );
9
 
10
+ return ITSEC_File_Change_Scanner::schedule_start();
11
  }
12
  }
core/modules/file-change/sync-verbs/itsec-ping-file-scan.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class Ithemes_Sync_Verb_ITSEC_Ping_File_Scan
5
+ */
6
+ class Ithemes_Sync_Verb_ITSEC_Ping_File_Scan extends Ithemes_Sync_Verb {
7
+
8
+ public static $name = 'itsec-ping-file-scan';
9
+ public static $description = 'Ping the file scan for a status update.';
10
+
11
+ public function run( $arguments ) {
12
+
13
+ require_once( dirname( dirname( __FILE__ ) ) . '/scanner.php' );
14
+
15
+ if ( ITSEC_Core::get_scheduler()->is_single_scheduled( 'file-change-fast', null ) ) {
16
+ ITSEC_Core::get_scheduler()->run_due_now();
17
+ }
18
+
19
+ $status = ITSEC_File_Change_Scanner::get_status();
20
+
21
+ if ( ! empty( $status['complete'] ) ) {
22
+ $status['change_list'] = ITSEC_File_Change::get_latest_changes();
23
+ }
24
+
25
+ return $status;
26
+ }
27
+ }
core/modules/file-change/validator.php CHANGED
@@ -6,50 +6,19 @@ class ITSEC_File_Change_Validator extends ITSEC_Validator {
6
  }
7
 
8
  protected function sanitize_settings() {
9
- $previous_settings = ITSEC_Modules::get_settings( $this->get_id() );
10
 
11
- if ( ! isset( $this->settings['last_run'] ) ) {
12
- $this->settings['last_run'] = $previous_settings['last_run'];
13
- }
14
- if ( ! isset( $this->settings['last_chunk'] ) ) {
15
- $this->settings['last_chunk'] = $previous_settings['last_chunk'];
16
- }
17
- if ( ! isset( $this->settings['show_warning'] ) ) {
18
- $this->settings['show_warning'] = $previous_settings['show_warning'];
19
- }
20
 
21
- $this->set_previous_if_empty( array( 'latest_changes' ) );
22
- $this->preserve_setting_if_exists( array( 'email' ) );
23
- $this->vars_to_skip_validate_matching_types[] = 'last_chunk';
24
- $this->vars_to_skip_validate_matching_fields[] = 'email';
25
 
26
- $this->sanitize_setting( 'bool', 'split', __( 'Split File Scanning', 'better-wp-security' ) );
27
- $this->sanitize_setting( array( 'exclude', 'include' ), 'method', __( 'Include/Exclude Files and Folders', 'better-wp-security' ) );
28
  $this->sanitize_setting( 'newline-separated-array', 'file_list', __( 'Files and Folders List', 'better-wp-security' ) );
29
  $this->sanitize_setting( 'newline-separated-extensions', 'types', __( 'Ignore File Types', 'better-wp-security' ) );
30
  $this->sanitize_setting( 'bool', 'notify_admin', __( 'Display File Change Admin Warning', 'better-wp-security' ) );
31
- $this->sanitize_setting( 'positive-int', 'last_run', __( 'Last Run', 'better-wp-security' ), false );
32
 
33
  $this->settings = apply_filters( 'itsec-file-change-sanitize-settings', $this->settings );
34
  }
35
-
36
- protected function validate_settings() {
37
- $current_time = ITSEC_Core::get_current_time();
38
-
39
- if ( defined( 'ITSEC_DOING_FILE_CHECK' ) && true === ITSEC_DOING_FILE_CHECK ) {
40
- $this->settings['last_run'] = $current_time;
41
- } else {
42
- if ( $this->settings['split'] ) {
43
- $interval = 12282;
44
- } else {
45
- $interval = 86340;
46
- }
47
-
48
- if ( $this->settings['last_run'] <= $current_time - $interval ) {
49
- $this->settings['last_run'] = $current_time - $interval + 120;
50
- }
51
- }
52
- }
53
  }
54
 
55
  ITSEC_Modules::register_validator( new ITSEC_File_Change_Validator() );
6
  }
7
 
8
  protected function sanitize_settings() {
 
9
 
10
+ unset( $this->settings['latest_changes'] );
 
 
 
 
 
 
 
 
11
 
12
+ $this->set_previous_if_empty( array( 'show_warning', 'expected_hashes', 'last_scan' ) );
13
+ $this->preserve_setting_if_exists( array( 'email', 'split', 'last_run', 'last_chunk', 'method' ) );
14
+ $this->vars_to_skip_validate_matching_fields = array( 'email', 'split', 'last_run', 'last_chunk', 'method', 'latest_changes' );
 
15
 
 
 
16
  $this->sanitize_setting( 'newline-separated-array', 'file_list', __( 'Files and Folders List', 'better-wp-security' ) );
17
  $this->sanitize_setting( 'newline-separated-extensions', 'types', __( 'Ignore File Types', 'better-wp-security' ) );
18
  $this->sanitize_setting( 'bool', 'notify_admin', __( 'Display File Change Admin Warning', 'better-wp-security' ) );
 
19
 
20
  $this->settings = apply_filters( 'itsec-file-change-sanitize-settings', $this->settings );
21
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
  ITSEC_Modules::register_validator( new ITSEC_File_Change_Validator() );
core/modules/global/active.php CHANGED
@@ -7,9 +7,6 @@ add_action( 'itsec_white_ips', 'itsec_global_filter_whitelisted_ips', 0 );
7
 
8
 
9
  function itsec_global_add_notice() {
10
- if ( ITSEC_Modules::get_setting( 'global', 'show_new_dashboard_notice' ) && current_user_can( ITSEC_Core::get_required_cap() ) ) {
11
- ITSEC_Core::add_notice( 'itsec_global_show_new_dashboard_notice' );
12
- }
13
 
14
  if ( ! defined( 'ITSEC_USE_CRON' ) && ITSEC_Core::current_user_can_manage() ) {
15
  ITSEC_Core::add_notice( 'itsec_show_disable_cron_constants_notice' );
@@ -22,24 +19,6 @@ function itsec_global_add_notice() {
22
  }
23
  add_action( 'admin_init', 'itsec_global_add_notice', 0 );
24
 
25
- function itsec_global_show_new_dashboard_notice() {
26
- echo '<div class="updated itsec-notice"><span class="it-icon-itsec"></span>'
27
- . __( 'New! The iThemes Security dashboard just got a new look.', 'better-wp-security' )
28
- . '<a class="itsec-notice-button" href="' . esc_url( 'https://ithemes.com/security/new-ithemes-security-dashboard/' ) . '">' . esc_html( __( "See what's new", 'better-wp-security' ) ) . '</a>'
29
- . '<button class="itsec-notice-hide" data-nonce="' . wp_create_nonce( 'dismiss-new-dashboard-notice' ) . '" data-source="new_dashboard">&times;</button>'
30
- . '</div>';
31
- }
32
-
33
- function itsec_global_dismiss_new_dashboard_notice() {
34
- if ( wp_verify_nonce( $_REQUEST['notice_nonce'], 'dismiss-new-dashboard-notice' ) ) {
35
- ITSEC_Modules::set_setting( 'global', 'show_new_dashboard_notice', false );
36
- wp_send_json_success();
37
- }
38
- wp_send_json_error();
39
- }
40
- add_action( 'wp_ajax_itsec-dismiss-notice-new_dashboard', 'itsec_global_dismiss_new_dashboard_notice' );
41
-
42
-
43
  function itsec_network_brute_force_add_notice() {
44
  if ( ITSEC_Modules::get_setting( 'network-brute-force', 'api_nag' ) && current_user_can( ITSEC_Core::get_required_cap() ) ) {
45
  ITSEC_Core::add_notice( 'itsec_network_brute_force_show_notice' );
@@ -163,4 +142,19 @@ function itsec_cron_test_callback( $time ) {
163
  ITSEC_Lib::schedule_cron_test();
164
  }
165
 
166
- add_action( 'itsec_cron_test', 'itsec_cron_test_callback' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
 
9
  function itsec_global_add_notice() {
 
 
 
10
 
11
  if ( ! defined( 'ITSEC_USE_CRON' ) && ITSEC_Core::current_user_can_manage() ) {
12
  ITSEC_Core::add_notice( 'itsec_show_disable_cron_constants_notice' );
19
  }
20
  add_action( 'admin_init', 'itsec_global_add_notice', 0 );
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  function itsec_network_brute_force_add_notice() {
23
  if ( ITSEC_Modules::get_setting( 'network-brute-force', 'api_nag' ) && current_user_can( ITSEC_Core::get_required_cap() ) ) {
24
  ITSEC_Core::add_notice( 'itsec_network_brute_force_show_notice' );
142
  ITSEC_Lib::schedule_cron_test();
143
  }
144
 
145
+ add_action( 'itsec_cron_test', 'itsec_cron_test_callback' );
146
+
147
+ /**
148
+ * Record that a user has logged-in.
149
+ *
150
+ * @param string $username
151
+ * @param WP_User $user
152
+ */
153
+ function itsec_record_first_login( $username, $user ) {
154
+
155
+ if ( ! get_user_meta( $user->ID, '_itsec_has_logged_in', true ) ) {
156
+ update_user_meta( $user->ID, '_itsec_has_logged_in', ITSEC_Core::get_current_time_gmt() );
157
+ }
158
+ }
159
+
160
+ add_action( 'wp_login', 'itsec_record_first_login', 15, 2 );
core/modules/global/js/settings-page.js CHANGED
@@ -23,12 +23,15 @@ var itsec_log_type_changed = function() {
23
 
24
  if ( 'both' === type ) {
25
  jQuery( '#itsec-global-log_rotation' ).parents( 'tr' ).show();
 
26
  jQuery( '#itsec-global-log_location' ).parents( 'tr' ).show();
27
  } else if ( 'file' === type ) {
28
  jQuery( '#itsec-global-log_rotation' ).parents( 'tr' ).hide();
 
29
  jQuery( '#itsec-global-log_location' ).parents( 'tr' ).show();
30
  } else {
31
  jQuery( '#itsec-global-log_rotation' ).parents( 'tr' ).show();
 
32
  jQuery( '#itsec-global-log_location' ).parents( 'tr' ).hide();
33
  }
34
  };
23
 
24
  if ( 'both' === type ) {
25
  jQuery( '#itsec-global-log_rotation' ).parents( 'tr' ).show();
26
+ jQuery( '#itsec-global-file_log_rotation' ).parents( 'tr' ).show();
27
  jQuery( '#itsec-global-log_location' ).parents( 'tr' ).show();
28
  } else if ( 'file' === type ) {
29
  jQuery( '#itsec-global-log_rotation' ).parents( 'tr' ).hide();
30
+ jQuery( '#itsec-global-file_log_rotation' ).parents( 'tr' ).show();
31
  jQuery( '#itsec-global-log_location' ).parents( 'tr' ).show();
32
  } else {
33
  jQuery( '#itsec-global-log_rotation' ).parents( 'tr' ).show();
34
+ jQuery( '#itsec-global-file_log_rotation' ).parents( 'tr' ).hide();
35
  jQuery( '#itsec-global-log_location' ).parents( 'tr' ).hide();
36
  }
37
  };
core/modules/global/privacy.php ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Global_Privacy {
4
+ private $settings;
5
+
6
+ public function __construct() {
7
+ $this->settings = ITSEC_Modules::get_settings( 'global' );
8
+
9
+ add_filter( 'itsec_get_privacy_policy_for_security_logs', array( $this, 'get_privacy_policy_for_security_logs' ) );
10
+ add_filter( 'itsec_get_privacy_policy_for_retention', array( $this, 'get_privacy_policy_for_retention' ) );
11
+ }
12
+
13
+ public function get_privacy_policy_for_security_logs( $policy ) {
14
+ $suggested_text = '<strong class="privacy-policy-tutorial">' . __( 'Suggested text:' ) . ' </strong>';
15
+
16
+ $retention_days = $this->get_retention_days();
17
+
18
+ /* Translators: 1: Number of days that data is retained for */
19
+ $policy .= "<p>$suggested_text " . sprintf( esc_html__( 'The IP address of visitors, user ID of logged in users, and username of login attempts are conditionally logged to check for malicious activity and to protect the site from specific kinds of attacks. Examples of conditions when logging occurs include login attempts, log out requests, requests for suspicious URLs, changes to site content, and password updates. This information is retained for %1$d days.', 'better-wp-security' ), $retention_days ) . "</p>\n";
20
+
21
+ return $policy;
22
+ }
23
+
24
+ public function get_privacy_policy_for_retention( $policy ) {
25
+ $suggested_text = '<strong class="privacy-policy-tutorial">' . __( 'Suggested text:' ) . ' </strong>';
26
+
27
+ $retention_days = $this->get_retention_days();
28
+
29
+ /* Translators: 1: Number of days that data is retained for */
30
+ $policy .= "<p>$suggested_text " . sprintf( esc_html__( 'Security logs are retained for %1$d days.', 'better-wp-security' ), $retention_days ) . "</p>\n";
31
+
32
+ return $policy;
33
+ }
34
+
35
+ private function get_retention_days() {
36
+ if ( 'database' === $this->settings['log_type'] ) {
37
+ return $this->settings['log_rotation'];
38
+ } else if ( 'file' === $this->settings['log_type'] ) {
39
+ return $this->settings['file_log_rotation'];
40
+ } else {
41
+ return max( $this->settings['log_rotation'], $this->settings['file_log_rotation'] );
42
+ }
43
+ }
44
+ }
45
+ new ITSEC_Global_Privacy();
core/modules/global/settings-page.php CHANGED
@@ -1,7 +1,7 @@
1
  <?php
2
 
3
  final class ITSEC_Global_Settings_Page extends ITSEC_Module_Settings_Page {
4
- private $version = 1;
5
 
6
 
7
  public function __construct() {
@@ -173,7 +173,15 @@ final class ITSEC_Global_Settings_Page extends ITSEC_Module_Settings_Page {
173
  <td>
174
  <?php $form->add_text( 'log_rotation', array( 'class' => 'small-text' ) ); ?>
175
  <label for="itsec-global-log_rotation"><?php _e( 'Days', 'better-wp-security' ); ?></label>
176
- <p class="description"><?php _e( 'The number of days database logs should be kept. File logs will be kept indefinitely but will be rotated once the file hits 10MB.', 'better-wp-security' ); ?></p>
 
 
 
 
 
 
 
 
177
  </td>
178
  </tr>
179
  <tr>
1
  <?php
2
 
3
  final class ITSEC_Global_Settings_Page extends ITSEC_Module_Settings_Page {
4
+ private $version = 2;
5
 
6
 
7
  public function __construct() {
173
  <td>
174
  <?php $form->add_text( 'log_rotation', array( 'class' => 'small-text' ) ); ?>
175
  <label for="itsec-global-log_rotation"><?php _e( 'Days', 'better-wp-security' ); ?></label>
176
+ <p class="description"><?php _e( 'The number of days database logs should be kept.', 'better-wp-security' ); ?></p>
177
+ </td>
178
+ </tr>
179
+ <tr>
180
+ <th scope="row"><label for="itsec-global-file_log_rotation"><?php _e( 'Days to Keep File Logs', 'better-wp-security' ); ?></label></th>
181
+ <td>
182
+ <?php $form->add_text( 'file_log_rotation', array( 'class' => 'small-text' ) ); ?>
183
+ <label for="itsec-global-log_rotation"><?php _e( 'Days', 'better-wp-security' ); ?></label>
184
+ <p class="description"><?php _e( 'The number of days file logs should be kept. File logs will additionally be rotated once the file hits 10MB. Set to 0 to only use log rotation.', 'better-wp-security' ); ?></p>
185
  </td>
186
  </tr>
187
  <tr>
core/modules/global/settings.php CHANGED
@@ -16,6 +16,7 @@ final class ITSEC_Global_Settings_New extends ITSEC_Settings {
16
  'lockout_period' => 15,
17
  'lockout_white_list' => array(),
18
  'log_rotation' => 60,
 
19
  'log_type' => 'database',
20
  'log_location' => ITSEC_Core::get_storage_dir( 'logs' ),
21
  'log_info' => '',
@@ -28,7 +29,6 @@ final class ITSEC_Global_Settings_New extends ITSEC_Settings {
28
  'proxy_override' => false,
29
  'hide_admin_bar' => false,
30
  'show_error_codes' => false,
31
- 'show_new_dashboard_notice' => true,
32
  'show_security_check' => true,
33
  'build' => 0,
34
  'activation_timestamp' => 0,
16
  'lockout_period' => 15,
17
  'lockout_white_list' => array(),
18
  'log_rotation' => 60,
19
+ 'file_log_rotation' => 180,
20
  'log_type' => 'database',
21
  'log_location' => ITSEC_Core::get_storage_dir( 'logs' ),
22
  'log_info' => '',
29
  'proxy_override' => false,
30
  'hide_admin_bar' => false,
31
  'show_error_codes' => false,
 
32
  'show_security_check' => true,
33
  'build' => 0,
34
  'activation_timestamp' => 0,
core/modules/global/validator.php CHANGED
@@ -19,8 +19,8 @@ class ITSEC_Global_Validator extends ITSEC_Validator {
19
  }
20
 
21
 
22
- $this->vars_to_skip_validate_matching_fields = array( 'digest_last_sent', 'digest_messages', 'digest_email', 'email_notifications', 'notification_email', 'backup_email' );
23
- $this->set_previous_if_empty( array( 'did_upgrade', 'log_info', 'show_new_dashboard_notice', 'show_security_check', 'build', 'activation_timestamp', 'lock_file', 'cron_status', 'use_cron', 'cron_test_time' ) );
24
  $this->set_default_if_empty( array( 'log_location', 'nginx_file' ) );
25
  $this->preserve_setting_if_exists( array( 'digest_email', 'email_notifications', 'notification_email', 'backup_email' ) );
26
 
@@ -40,6 +40,7 @@ class ITSEC_Global_Validator extends ITSEC_Validator {
40
  $this->sanitize_setting( 'positive-int', 'blacklist_period', __( 'Blacklist Lockout Period', 'better-wp-security' ) );
41
  $this->sanitize_setting( 'positive-int', 'lockout_period', __( 'Lockout Period', 'better-wp-security' ) );
42
  $this->sanitize_setting( 'positive-int', 'log_rotation', __( 'Days to Keep Database Logs', 'better-wp-security' ) );
 
43
 
44
  $this->sanitize_setting( 'newline-separated-ips', 'lockout_white_list', __( 'Lockout White List', 'better-wp-security' ) );
45
 
19
  }
20
 
21
 
22
+ $this->vars_to_skip_validate_matching_fields = array( 'digest_last_sent', 'digest_messages', 'digest_email', 'email_notifications', 'notification_email', 'backup_email', 'show_new_dashboard_notice' );
23
+ $this->set_previous_if_empty( array( 'did_upgrade', 'log_info', 'show_security_check', 'build', 'activation_timestamp', 'lock_file', 'cron_status', 'use_cron', 'cron_test_time' ) );
24
  $this->set_default_if_empty( array( 'log_location', 'nginx_file' ) );
25
  $this->preserve_setting_if_exists( array( 'digest_email', 'email_notifications', 'notification_email', 'backup_email' ) );
26
 
40
  $this->sanitize_setting( 'positive-int', 'blacklist_period', __( 'Blacklist Lockout Period', 'better-wp-security' ) );
41
  $this->sanitize_setting( 'positive-int', 'lockout_period', __( 'Lockout Period', 'better-wp-security' ) );
42
  $this->sanitize_setting( 'positive-int', 'log_rotation', __( 'Days to Keep Database Logs', 'better-wp-security' ) );
43
+ $this->sanitize_setting( 'positive-int', 'file_log_rotation', __( 'Days to Keep File Logs', 'better-wp-security' ) );
44
 
45
  $this->sanitize_setting( 'newline-separated-ips', 'lockout_white_list', __( 'Lockout White List', 'better-wp-security' ) );
46
 
core/modules/hide-backend/privacy.php ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Hide_Backend_Privacy {
4
+ private $settings;
5
+
6
+ public function __construct() {
7
+ $this->settings = ITSEC_Modules::get_settings( 'hide-backend' );
8
+
9
+ if ( ! $this->settings['enabled'] ) {
10
+ return;
11
+ }
12
+
13
+ add_filter( 'itsec_get_privacy_policy_for_cookies', array( $this, 'get_privacy_policy_for_cookies' ) );
14
+ }
15
+
16
+ public function get_privacy_policy_for_cookies( $policy ) {
17
+ $suggested_text = '<strong class="privacy-policy-tutorial">' . __( 'Suggested text:' ) . ' </strong>';
18
+
19
+ $policy .= "<p>$suggested_text " . esc_html__( 'Visiting the login page sets a temporary cookie that aids compatibility with some alternate login methods. This cookie contains no personal data and expires after 1 hour.', 'better-wp-security' ) . "</p>\n";
20
+
21
+ return $policy;
22
+ }
23
+ }
24
+ new ITSEC_Hide_Backend_Privacy();
core/modules/ipcheck/privacy.php ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Network_Bruteforce_Privacy {
4
+ private $settings;
5
+
6
+ public function __construct() {
7
+ $this->settings = ITSEC_Modules::get_settings( 'network-brute-force' );
8
+
9
+ if ( empty( $this->settings['api_key'] ) || empty( $this->settings['api_secret'] ) ) {
10
+ return;
11
+ }
12
+
13
+ add_filter( 'itsec_get_privacy_policy_for_sending', array( $this, 'get_privacy_policy_for_sending' ) );
14
+ }
15
+
16
+ public function get_privacy_policy_for_sending( $policy ) {
17
+ $suggested_text = '<strong class="privacy-policy-tutorial">' . __( 'Suggested text:' ) . ' </strong>';
18
+
19
+ /* Translators: 1: URL to the iThemes privacy policy */
20
+ $policy .= "<p>$suggested_text " . sprintf( wp_kses( __( 'This site is part of a network of sites that protect against distributed brute force attacks. To enable this protection, the IP address of visitors attempting to log into the site is shared with a service provided by ithemes.com. For privacy policy details, please see the <a href="%1$s">iThemes Privacy Policy</a>.', 'better-wp-security' ), array( 'a' => array( 'href' => array() ) ) ), 'https://ithemes.com/privacy-policy' ) . "</p>\n";
21
+
22
+ return $policy;
23
+ }
24
+ }
25
+ new ITSEC_Network_Bruteforce_Privacy();
core/modules/malware/privacy.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Malware_Privacy {
4
+ public function __construct() {
5
+ add_filter( 'itsec_get_privacy_policy_for_sharing', array( $this, 'get_privacy_policy_for_sharing' ) );
6
+ }
7
+
8
+ public function get_privacy_policy_for_sharing( $policy ) {
9
+ $suggested_text = '<strong class="privacy-policy-tutorial">' . __( 'Suggested text:' ) . ' </strong>';
10
+
11
+ /* Translators: 1: Link to Sucuri's privacy policy */
12
+ $policy .= "<p>$suggested_text " . sprintf( wp_kses( __( 'This site is scanned for potential malware and vulnerabilities by Sucuri\'s SiteCheck. We do not send personal information to Sucuri; however, Sucuri could find personal information posted publicly (such as in comments) during their scan. For more details, please see <a href="%1$s">Sucuri\'s privacy policy</a>.', 'better-wp-security' ), array( 'a' => array( 'href' => array() ) ) ), 'https://sucuri.net/privacy' ) . "</p>\n";
13
+
14
+ return $policy;
15
+ }
16
+ }
17
+ new ITSEC_Malware_Privacy();
core/modules/notification-center/class-notification-center.php CHANGED
@@ -386,7 +386,7 @@ final class ITSEC_Notification_Center {
386
  $notification_data[] = $data;
387
 
388
  if ( $enforce_unique ) {
389
- $notification_data = array_unique( $notification_data );
390
  }
391
 
392
  $all_data[ $notification ] = $notification_data;
386
  $notification_data[] = $data;
387
 
388
  if ( $enforce_unique ) {
389
+ $notification_data = array_unique( $notification_data, SORT_REGULAR );
390
  }
391
 
392
  $all_data[ $notification ] = $notification_data;
core/modules/notification-center/validator.php CHANGED
@@ -242,7 +242,7 @@ class ITSEC_Notification_Center_Validator extends ITSEC_Validator {
242
  $available_users = array();
243
 
244
  foreach ( $roles->roles as $role => $details ) {
245
- if ( isset( $details['capabilities']['manage_options'] ) && ( true === $details['capabilities']['manage_options'] ) ) {
246
  $available_roles["role:$role"] = translate_user_role( $details['name'] );
247
 
248
  $users = get_users( array( 'role' => $role ) );
242
  $available_users = array();
243
 
244
  foreach ( $roles->roles as $role => $details ) {
245
+ if ( ! empty( $details['capabilities']['manage_options'] ) ) {
246
  $available_roles["role:$role"] = translate_user_role( $details['name'] );
247
 
248
  $users = get_users( array( 'role' => $role ) );
core/modules/privacy/active.php ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
1
+ <?php
2
+
3
+ require_once( 'class-itsec-privacy.php' );
4
+ $itsec_privacy = new ITSEC_Privacy();
5
+ $itsec_privacy->run();
core/modules/privacy/class-itsec-privacy.php ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Privacy {
4
+ public function run() {
5
+ add_action( 'admin_init', array( $this, 'admin_init' ) );
6
+ add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_exporter' ) );
7
+ add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_eraser' ) );
8
+ }
9
+
10
+ public function admin_init() {
11
+ if ( function_exists( 'wp_add_privacy_policy_content' ) ) {
12
+ wp_add_privacy_policy_content( 'iThemes Security', $this->get_privacy_policy_content() );
13
+ }
14
+ }
15
+
16
+ private function get_privacy_policy_content() {
17
+ require_once( dirname( __FILE__ ) . '/util.php' );
18
+
19
+ return ITSEC_Privacy_Util::get_privacy_policy_content();
20
+ }
21
+
22
+ public function register_exporter( $exporters ) {
23
+ $exporters['ithemes-security'] = array(
24
+ 'exporter_friendly_name' => __( 'iThemes Security Plugin', 'better-wp-security' ),
25
+ 'callback' => array( $this, 'export' ),
26
+ );
27
+
28
+ return $exporters;
29
+ }
30
+
31
+ public function export( $email, $page = 1 ) {
32
+ require_once( dirname( __FILE__ ) . '/util.php' );
33
+
34
+ return ITSEC_Privacy_Util::export( $email, (int) $page );
35
+ }
36
+
37
+ public function register_eraser( $erasers ) {
38
+ $erasers['ithemes-security'] = array(
39
+ 'eraser_friendly_name' => __( 'iThemes Security Plugin', 'better-wp-security' ),
40
+ 'callback' => array( $this, 'erase' ),
41
+ );
42
+
43
+ return $erasers;
44
+ }
45
+
46
+ public function erase( $email, $page = 1 ) {
47
+ require_once( dirname( __FILE__ ) . '/util.php' );
48
+
49
+ return ITSEC_Privacy_Util::erase( $email, (int) $page );
50
+ }
51
+ }
core/modules/privacy/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/privacy/util.php ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ final class ITSEC_Privacy_Util {
4
+ public static function get_privacy_policy_content() {
5
+ ITSEC_Modules::load_module_file( 'privacy.php', ':active' );
6
+
7
+
8
+ $sections = array(
9
+ 'collection' => array(
10
+ 'heading' => __( 'What personal data we collect and why we collect it' ),
11
+ 'subheadings' => array(
12
+ 'comments' => __( 'Comments' ),
13
+ 'media' => __( 'Media' ),
14
+ 'contact_forms' => __( 'Contact Forms' ),
15
+ 'cookies' => __( 'Cookies' ),
16
+ 'embeds' => __( 'Embedded content from other websites' ),
17
+ 'analytics' => __( 'Analytics' ),
18
+ 'security_logs' => __( 'Security Logs' ),
19
+ ),
20
+ ),
21
+ 'sharing' => __( 'Who we share your data with' ),
22
+ 'retention' => __( 'How long we retain your data' ),
23
+ 'rights' => __( 'What rights you have over your data' ),
24
+ 'sending' => __( 'Where we send your data' ),
25
+ 'additional' => __( 'Additional information' ),
26
+ 'protection' => __( 'How we protect your data' ),
27
+ 'breach_procedures' => __( 'What data breach procedures we have in place' ),
28
+ 'third_parties' => __( 'What third parties we receive data from' ),
29
+ 'profiling' => __( 'What automated decision making and/or profiling we do with user data' ),
30
+ );
31
+
32
+ $sections = apply_filters( 'itsec_get_privacy_policy_sections', $sections );
33
+
34
+
35
+ $policy = '';
36
+
37
+ foreach ( $sections as $section => $details ) {
38
+ $section_text = apply_filters( "itsec_get_privacy_policy_for_$section", '' );
39
+
40
+ if ( is_string( $details ) ) {
41
+ $section_heading = $details;
42
+ } else {
43
+ $section_heading = $details['heading'];
44
+
45
+ foreach ( $details['subheadings'] as $id => $heading ) {
46
+ $text = apply_filters( "itsec_get_privacy_policy_for_$id", '' );
47
+
48
+ if ( ! empty( $text ) ) {
49
+ $section_text .= "<h3>$heading</h3>\n$text\n";
50
+ }
51
+ }
52
+ }
53
+
54
+ if ( ! empty( $section_text ) ) {
55
+ $policy .= "<h2>$section_heading</h2>\n$section_text\n";
56
+ }
57
+ }
58
+
59
+ if ( ! empty( $policy ) ) {
60
+ $policy = "<div class=\"wp-suggested-text\">\n$policy\n</div>\n";
61
+ }
62
+
63
+ return $policy;
64
+ }
65
+
66
+ public static function export( $email, $page ) {
67
+ global $wpdb;
68
+
69
+ $limit = 500;
70
+ $offset = ( $page - 1 ) * $limit;
71
+
72
+ $user = get_user_by( 'email', $email );
73
+ $user_id = false === $user ? false : $user->ID;
74
+ $escaped_email = '%%' . $wpdb->esc_like( $email ) . '%%';
75
+
76
+ if ( false === $user ) {
77
+ $query = "SELECT id, module, code, type, timestamp, user_id, url FROM {$wpdb->base_prefix}itsec_logs WHERE data LIKE %s OR url LIKE %s LIMIT $offset,$limit";
78
+ $query = $wpdb->prepare( $query, $escaped_email, $escaped_email );
79
+ } else {
80
+ $query = "SELECT id, module, code, type, timestamp, user_id, url FROM {$wpdb->base_prefix}itsec_logs WHERE data LIKE %s OR url LIKE %s OR user_id=%d LIMIT $offset,$limit";
81
+ $query = $wpdb->prepare( $query, $escaped_email, $escaped_email, $user_id );
82
+ }
83
+
84
+ $logs = $wpdb->get_results( $query, ARRAY_A );
85
+ $export_items = array();
86
+
87
+ foreach ( (array) $logs as $log ) {
88
+ $group_id = 'security-logs';
89
+ $group_label = __( 'Security Logs', 'better-wp-security' );
90
+ $item_id = "security-log-{$log['id']}";
91
+
92
+ $data = self::get_data_from_log_entry( $log, $email, $user_id );
93
+
94
+ $export_items[] = compact( 'group_id', 'group_label', 'item_id', 'data' );
95
+ }
96
+
97
+
98
+ $done = count( $logs ) < $limit;
99
+
100
+ return array(
101
+ 'data' => $export_items,
102
+ 'done' => $done,
103
+ );
104
+ }
105
+
106
+ public static function erase( $email, $page ) {
107
+ global $wpdb;
108
+
109
+ $limit = 500;
110
+ $offset = ( $page - 1 ) * $limit;
111
+
112
+ $user = get_user_by( 'email', $email );
113
+ $user_id = false === $user ? false : $user->ID;
114
+ $escaped_email = '%%' . $wpdb->esc_like( $email ) . '%%';
115
+
116
+ if ( false === $user ) {
117
+ $query = "SELECT COUNT(id) AS count FROM {$wpdb->base_prefix}itsec_logs WHERE data LIKE %s OR url LIKE %s LIMIT $offset,$limit";
118
+ $query = $wpdb->prepare( $query, $escaped_email, $escaped_email );
119
+ } else {
120
+ $query = "SELECT COUNT(id) AS count FROM {$wpdb->base_prefix}itsec_logs WHERE data LIKE %s OR url LIKE %s OR user_id=%d LIMIT $offset,$limit";
121
+ $query = $wpdb->prepare( $query, $escaped_email, $escaped_email, $user_id );
122
+ }
123
+
124
+ $count = (int) $wpdb->get_var( $query );
125
+ $done = $count < $limit;
126
+
127
+ return array(
128
+ 'items_removed' => false,
129
+ 'items_retained' => true,
130
+ 'messages' => array(
131
+ __( 'The security logs are retained since they may be required as part of analysis of a site compromise.', 'better-wp-security' ),
132
+ ),
133
+ 'done' => $done,
134
+ );
135
+ }
136
+
137
+ private static function get_data_from_log_entry( $log, $email, $user_id ) {
138
+ $data = array(
139
+ array(
140
+ 'name' => __( 'Timestamp', 'better-wp-security' ),
141
+ 'value' => $log['timestamp'],
142
+ ),
143
+ );
144
+
145
+ if ( false === strpos( $log['code'], '::' ) ) {
146
+ $code = $log['code'];
147
+ } else {
148
+ list( $code, $junk ) = explode( '::', $log['code'], 2 );
149
+ }
150
+
151
+ if ( 'lockout' === $log['module'] ) {
152
+ $event = __( 'Failed login', 'better-wp-security' );
153
+ } else if ( 'four_oh_four' === $log['module'] ) {
154
+ $event = __( 'Requested suspicious URL', 'better-wp-security' );
155
+ } else if ( 'ipcheck' === $log['module'] ) {
156
+ $event = __( 'Failed check by network brute force protection', 'better-wp-security' );
157
+ } else if ( 'brute_force' === $log['module'] ) {
158
+ if ( 'auto-ban-admin-username' === $code ) {
159
+ $event = __( 'Attempted to log in as admin', 'better-wp-security' );
160
+ } else {
161
+ $event = __( 'Failed login', 'better-wp-security' );
162
+ }
163
+ } else if ( 'away_mode' === $log['module'] ) {
164
+ $event = __( 'Access while site in away mode', 'better-wp-security' );
165
+ } else if ( 'recaptcha' === $log['module'] ) {
166
+ $event = __( 'Failed reCAPTCHA validation', 'better-wp-security' );
167
+ } else if ( 'two_factor' === $log['module'] ) {
168
+ if ( 'failed_authentication' === $code ) {
169
+ $event = __( 'Failed two-factor authentication validation', 'better-wp-security' );
170
+ } else if ( 'successful_authentication' === $code ) {
171
+ $event = __( 'Two-factor authentication validated successfully', 'better-wp-security' );
172
+ } else if ( 'sync_override' === $code ) {
173
+ $event = __( 'Overrode two-factor authentication using iThemes Sync', 'better-wp-security' );
174
+ }
175
+ } else if ( 'user_logging' === $log['module'] ) {
176
+ if ( 'post-status-changed' === $code ) {
177
+ $event = __( 'Changed content', 'better-wp-security' );
178
+ } else if ( 'user-logged-in' === $code ) {
179
+ $event = __( 'Logged in', 'better-wp-security' );
180
+ } else if ( 'user-logged-out' === $code ) {
181
+ $event = __( 'Logged out', 'better-wp-security' );
182
+ }
183
+ }
184
+
185
+ if ( empty( $event ) ) {
186
+ $event = __( 'Unknown event or action', 'better-wp-security' );
187
+ }
188
+
189
+ $data[] = array(
190
+ 'name' => __( 'Event', 'better-wp-security' ),
191
+ 'value' => $event,
192
+ );
193
+
194
+ return $data;
195
+ }
196
+ }
core/modules/security-check/scanner.php CHANGED
@@ -12,6 +12,7 @@ final class ITSEC_Security_Check_Scanner {
12
  'ban-users' => __( 'Banned Users', 'better-wp-security' ),
13
  'backup' => __( 'Database Backups', 'better-wp-security' ),
14
  'brute-force' => __( 'Local Brute Force Protection', 'better-wp-security' ),
 
15
  'magic-links' => __( 'Magic Links', 'better-wp-security' ),
16
  'malware-scheduling' => __( 'Malware Scan Scheduling', 'better-wp-security' ),
17
  'network-brute-force' => __( 'Network Brute Force Protection', 'better-wp-security' ),
@@ -70,6 +71,8 @@ final class ITSEC_Security_Check_Scanner {
70
 
71
  self::enforce_setting( 'global', 'write_files', true, __( 'Enabled the Write to Files setting in Global Settings.', 'better-wp-security' ) );
72
 
 
 
73
  do_action( 'itsec-security-check-after-default-checks', self::$feedback, self::$available_modules );
74
  }
75
 
12
  'ban-users' => __( 'Banned Users', 'better-wp-security' ),
13
  'backup' => __( 'Database Backups', 'better-wp-security' ),
14
  'brute-force' => __( 'Local Brute Force Protection', 'better-wp-security' ),
15
+ 'online-files' => __( 'File Change Detection', 'better-wp-security' ),
16
  'magic-links' => __( 'Magic Links', 'better-wp-security' ),
17
  'malware-scheduling' => __( 'Malware Scan Scheduling', 'better-wp-security' ),
18
  'network-brute-force' => __( 'Network Brute Force Protection', 'better-wp-security' ),
71
 
72
  self::enforce_setting( 'global', 'write_files', true, __( 'Enabled the Write to Files setting in Global Settings.', 'better-wp-security' ) );
73
 
74
+ self::enforce_setting( 'online-files', 'compare_file_hashes', true, __( 'Enabled Online Files Comparison in File Change Detection.', 'better-wp-security' ) );
75
+
76
  do_action( 'itsec-security-check-after-default-checks', self::$feedback, self::$available_modules );
77
  }
78
 
core/modules/ssl/class-itsec-ssl.php CHANGED
@@ -69,7 +69,7 @@ class ITSEC_SSL {
69
  add_filter( 'script_loader_src', array( $this, 'script_loader_src' ) );
70
  add_filter( 'style_loader_src', array( $this, 'style_loader_src' ) );
71
  add_filter( 'upload_dir', array( $this, 'upload_dir' ) );
72
- } else if ( 'enabled' === $settings['require_ssl'] && 'GET' === $_SERVER['REQUEST_METHOD'] && ( ! defined( 'WP_CLI' ) || ! WP_CLI ) ) {
73
  $this->redirect_to_https();
74
  }
75
  }
69
  add_filter( 'script_loader_src', array( $this, 'script_loader_src' ) );
70
  add_filter( 'style_loader_src', array( $this, 'style_loader_src' ) );
71
  add_filter( 'upload_dir', array( $this, 'upload_dir' ) );
72
+ } else if ( 'enabled' === $settings['require_ssl'] && ( ! defined( 'WP_CLI' ) || ! WP_CLI ) && 'GET' === $_SERVER['REQUEST_METHOD'] ) {
73
  $this->redirect_to_https();
74
  }
75
  }
core/modules/strong-passwords/js/script.js CHANGED
@@ -6,4 +6,5 @@ jQuery( document ).ready( function () {
6
 
7
  } );
8
 
 
9
  } );
6
 
7
  } );
8
 
9
+ jQuery( '.pw-weak' ).remove();
10
  } );
core/modules/system-tweaks/config-generators.php CHANGED
@@ -127,7 +127,7 @@ final class ITSEC_System_Tweaks_Config_Generators {
127
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} %24&x [NC,OR]\n";
128
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} 127\.0 [NC,OR]\n";
129
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} (globals|encode|localhost|loopback) [NC,OR]\n";
130
- $rewrites .= "\t\tRewriteCond %{QUERY_STRING} (request|concat|insert|union|declare) [NC,OR]\n";
131
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} %[01][0-9A-F] [NC]\n";
132
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} !^loggedout=true\n";
133
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} !^action=jetpack-sso\n";
@@ -254,7 +254,7 @@ final class ITSEC_System_Tweaks_Config_Generators {
254
  $modification .= "\tif ( \$args ~* \"%24&x\" ) { set \$susquery 1; }\n";
255
  $modification .= "\tif ( \$args ~* \"127\.0\" ) { set \$susquery 1; }\n";
256
  $modification .= "\tif ( \$args ~* \"(globals|encode|localhost|loopback)\" ) { set \$susquery 1; }\n";
257
- $modification .= "\tif ( \$args ~* \"(request|insert|concat|union|declare)\" ) { set \$susquery 1; }\n";
258
  $modification .= "\tif ( \$args ~* \"%[01][0-9A-F]\" ) { set \$susquery 1; }\n";
259
  $modification .= "\tif ( \$args ~ \"^loggedout=true\" ) { set \$susquery 0; }\n";
260
  $modification .= "\tif ( \$args ~ \"^action=jetpack-sso\" ) { set \$susquery 0; }\n";
127
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} %24&x [NC,OR]\n";
128
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} 127\.0 [NC,OR]\n";
129
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} (globals|encode|localhost|loopback) [NC,OR]\n";
130
+ $rewrites .= "\t\tRewriteCond %{QUERY_STRING} (concat|insert|union|declare) [NC,OR]\n";
131
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} %[01][0-9A-F] [NC]\n";
132
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} !^loggedout=true\n";
133
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} !^action=jetpack-sso\n";
254
  $modification .= "\tif ( \$args ~* \"%24&x\" ) { set \$susquery 1; }\n";
255
  $modification .= "\tif ( \$args ~* \"127\.0\" ) { set \$susquery 1; }\n";
256
  $modification .= "\tif ( \$args ~* \"(globals|encode|localhost|loopback)\" ) { set \$susquery 1; }\n";
257
+ $modification .= "\tif ( \$args ~* \"(insert|concat|union|declare)\" ) { set \$susquery 1; }\n";
258
  $modification .= "\tif ( \$args ~* \"%[01][0-9A-F]\" ) { set \$susquery 1; }\n";
259
  $modification .= "\tif ( \$args ~ \"^loggedout=true\" ) { set \$susquery 0; }\n";
260
  $modification .= "\tif ( \$args ~ \"^action=jetpack-sso\" ) { set \$susquery 0; }\n";
core/modules/system-tweaks/setup.php CHANGED
@@ -81,16 +81,11 @@ if ( ! class_exists( 'ITSEC_System_Tweaks_Setup' ) ) {
81
  $current_options['write_permissions'] = isset( $itsec_bwps_options['st_fileperm'] ) && $itsec_bwps_options['st_fileperm'] == 1 ? true : false;
82
 
83
  update_site_option( 'itsec_tweaks', $current_options );
84
- ITSEC_Response::regenerate_server_config();
85
  ITSEC_Response::regenerate_wp_config();
86
  }
87
 
88
  }
89
 
90
- if ( $itsec_old_version < 4035 ) {
91
- ITSEC_Response::regenerate_server_config();
92
- }
93
-
94
  if ( $itsec_old_version < 4041 ) {
95
  $current_options = get_site_option( 'itsec_tweaks' );
96
 
@@ -121,6 +116,10 @@ if ( ! class_exists( 'ITSEC_System_Tweaks_Setup' ) ) {
121
  ITSEC_Modules::set_settings( 'system-tweaks', $current_options );
122
  }
123
  }
 
 
 
 
124
  }
125
 
126
  }
81
  $current_options['write_permissions'] = isset( $itsec_bwps_options['st_fileperm'] ) && $itsec_bwps_options['st_fileperm'] == 1 ? true : false;
82
 
83
  update_site_option( 'itsec_tweaks', $current_options );
 
84
  ITSEC_Response::regenerate_wp_config();
85
  }
86
 
87
  }
88
 
 
 
 
 
89
  if ( $itsec_old_version < 4041 ) {
90
  $current_options = get_site_option( 'itsec_tweaks' );
91
 
116
  ITSEC_Modules::set_settings( 'system-tweaks', $current_options );
117
  }
118
  }
119
+
120
+ if ( $itsec_old_version < 4095 ) {
121
+ ITSEC_Response::regenerate_server_config();
122
+ }
123
  }
124
 
125
  }
core/notify.php CHANGED
@@ -122,15 +122,31 @@ class ITSEC_Notify {
122
  break;
123
  }
124
 
125
- $mail = $nc->mail();
126
 
 
127
  $mail->add_header( $title, $banner_title );
128
  $mail->add_info_box( sprintf( esc_html__( 'The following is a summary of security related activity on your site: %s', 'better-wp-security' ), '<b>' . $mail->get_display_url() . '</b>' ) );
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  $mail->add_section_heading( esc_html__( 'Lockouts', 'better-wp-security' ), 'lock' );
131
 
132
- $user_count = $itsec_lockout->get_lockouts( 'user', array( 'after' => $last_sent, 'return' => 'count' ) );
133
- $host_count = $itsec_lockout->get_lockouts( 'host', array( 'after' => $last_sent, 'return' => 'count' ) );
134
 
135
  if ( $host_count > 0 || $user_count > 0 ) {
136
  $mail->add_lockouts_summary( $user_count, $host_count );
@@ -139,8 +155,6 @@ class ITSEC_Notify {
139
  $mail->add_text( esc_html__( 'No lockouts since the last email check.', 'better-wp-security' ) );
140
  }
141
 
142
- $data_proxy = new ITSEC_Notify_Data_Proxy( $data );
143
-
144
  if ( $data_proxy->has_message( 'file-change' ) ) {
145
  $mail->add_section_heading( esc_html__( 'File Changes', 'better-wp-security' ), 'folder' );
146
  $mail->add_text( esc_html__( 'File changes detected on the site.', 'better-wp-security' ) );
@@ -238,101 +252,6 @@ class ITSEC_Notify {
238
  ITSEC_Core::get_notification_center()->enqueue_data( 'digest', array( 'type' => 'file-change' ) );
239
  }
240
 
241
- /**
242
- * Enqueue or send notification accordingly
243
- *
244
- * @since 4.5
245
- *
246
- * @param null|array $body Custom message information to send
247
- *
248
- * @return bool whether the message was successfully enqueue or sent
249
- */
250
- public function notify( $body = null ) {
251
-
252
- _deprecated_function( __METHOD__, '3.9.0', 'ITSEC_Notification_Center' );
253
-
254
- if ( empty( $body ) || ! is_array( $body ) ) {
255
- return true;
256
- }
257
-
258
- $allowed_tags = array(
259
- 'a' => array(
260
- 'href' => array(),
261
- ),
262
- 'em' => array(),
263
- 'p' => array(),
264
- 'strong' => array(),
265
- 'table' => array(
266
- 'border' => array(),
267
- 'style' => array(),
268
- ),
269
- 'tr' => array(),
270
- 'td' => array(
271
- 'colspan' => array(),
272
- ),
273
- 'th' => array(),
274
- 'br' => array(),
275
- 'h4' => array(),
276
- );
277
-
278
- $subject = trim( sanitize_text_field( $body['subject'] ) );
279
- $message = wp_kses( $body['message'], $allowed_tags );
280
-
281
- if ( isset( $body['headers'] ) ) {
282
-
283
- $headers = $body['headers'];
284
-
285
- } else {
286
-
287
- $headers = '';
288
-
289
- }
290
-
291
- return $this->send_mail( $subject, $message, $headers );
292
- }
293
-
294
- /**
295
- * Sends email to recipient
296
- *
297
- * @since 4.5
298
- *
299
- * @param string $subject Email subject
300
- * @param string $message Message contents
301
- * @param string|array $headers Optional. Additional headers.
302
- *
303
- * @return bool Whether the email contents were sent successfully.
304
- */
305
- private function send_mail( $subject, $message, $headers = '' ) {
306
-
307
- $recipients = ITSEC_Modules::get_setting( 'global', 'notification_email' );
308
- $all_success = true;
309
-
310
- add_filter( 'wp_mail_content_type', array( $this, 'wp_mail_content_type' ) );
311
-
312
- foreach ( $recipients as $recipient ) {
313
-
314
- if ( is_email( trim( $recipient ) ) ) {
315
-
316
- if ( defined( 'ITSEC_DEBUG' ) && ITSEC_DEBUG === true ) {
317
- $message .= '<p>' . __( 'Debug info (source page): ' . esc_url( $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"] ) ) . '</p>';
318
- }
319
-
320
- $success = wp_mail( trim( $recipient ), $subject, '<html>' . $message . '</html>', $headers );
321
-
322
- if ( $all_success === true && $success === false ) {
323
- $all_success = false;
324
- }
325
-
326
- }
327
-
328
- }
329
-
330
- remove_filter( 'wp_mail_content_type', array( $this, 'wp_mail_content_type' ) );
331
-
332
- return $all_success;
333
-
334
- }
335
-
336
  /**
337
  * Set HTML content type for email
338
  *
122
  break;
123
  }
124
 
125
+ $data_proxy = new ITSEC_Notify_Data_Proxy( $data );
126
 
127
+ $mail = $nc->mail();
128
  $mail->add_header( $title, $banner_title );
129
  $mail->add_info_box( sprintf( esc_html__( 'The following is a summary of security related activity on your site: %s', 'better-wp-security' ), '<b>' . $mail->get_display_url() . '</b>' ) );
130
 
131
+ $content = $mail->get_content();
132
+
133
+ /**
134
+ * Fires before the main content of the Security Digest is added.
135
+ *
136
+ * @param ITSEC_Mail $mail
137
+ * @param ITSEC_Notify_Data_Proxy $data_proxy
138
+ * @param int $last_sent
139
+ */
140
+ do_action( 'itsec_security_digest_before', $mail, $data_proxy, $last_sent );
141
+
142
+ if ( $content !== $mail->get_content() ) {
143
+ $send_email = true;
144
+ }
145
+
146
  $mail->add_section_heading( esc_html__( 'Lockouts', 'better-wp-security' ), 'lock' );
147
 
148
+ $user_count = $itsec_lockout->get_lockouts( 'user', array( 'after' => $last_sent, 'current' => false, 'return' => 'count' ) );
149
+ $host_count = $itsec_lockout->get_lockouts( 'host', array( 'after' => $last_sent, 'current' => false, 'return' => 'count' ) );
150
 
151
  if ( $host_count > 0 || $user_count > 0 ) {
152
  $mail->add_lockouts_summary( $user_count, $host_count );
155
  $mail->add_text( esc_html__( 'No lockouts since the last email check.', 'better-wp-security' ) );
156
  }
157
 
 
 
158
  if ( $data_proxy->has_message( 'file-change' ) ) {
159
  $mail->add_section_heading( esc_html__( 'File Changes', 'better-wp-security' ), 'folder' );
160
  $mail->add_text( esc_html__( 'File changes detected on the site.', 'better-wp-security' ) );
252
  ITSEC_Core::get_notification_center()->enqueue_data( 'digest', array( 'type' => 'file-change' ) );
253
  }
254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  /**
256
  * Set HTML content type for email
257
  *
core/response.php CHANGED
@@ -339,6 +339,7 @@ final class ITSEC_Response {
339
  $this->errors = array();
340
  $this->warnings = array();
341
  $this->messages = array();
 
342
  $this->success = true;
343
  $this->js_function_calls = array();
344
  $this->show_default_success_message = true;
339
  $this->errors = array();
340
  $this->warnings = array();
341
  $this->messages = array();
342
+ $this->infos = array();
343
  $this->success = true;
344
  $this->js_function_calls = array();
345
  $this->show_default_success_message = true;
core/setup.php CHANGED
@@ -147,6 +147,10 @@ final class ITSEC_Setup {
147
  ITSEC_Core::get_scheduler()->register_events();
148
  }
149
 
 
 
 
 
150
  // Update stored build number.
151
  ITSEC_Modules::set_setting( 'global', 'build', ITSEC_Core::get_plugin_build() );
152
  }
147
  ITSEC_Core::get_scheduler()->register_events();
148
  }
149
 
150
+ if ( $build < 4094 ) {
151
+ ITSEC_Core::get_scheduler()->register_events();
152
+ }
153
+
154
  // Update stored build number.
155
  ITSEC_Modules::set_setting( 'global', 'build', ITSEC_Core::get_plugin_build() );
156
  }
history.txt CHANGED
@@ -729,3 +729,39 @@
729
  Bug Fix: Fixed situation that could cause lockout notifications being sent for whitelisted IPs.
730
  Bug Fix: Fixed issue where saving Global Settings would be blocked by an unwritable "Path to Log Files" path when the "Log Type" is set to "Database Only".
731
  Bug Fix: Fixed issue that prevented log database entries from purging and log file entries from rotating on a schedule.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
  Bug Fix: Fixed situation that could cause lockout notifications being sent for whitelisted IPs.
730
  Bug Fix: Fixed issue where saving Global Settings would be blocked by an unwritable "Path to Log Files" path when the "Log Type" is set to "Database Only".
731
  Bug Fix: Fixed issue that prevented log database entries from purging and log file entries from rotating on a schedule.
732
+ 7.0.0 - 2018-05-24 - Chris Jean & Timothy Jacobs
733
+ New Feature: Added support for the new WordPress privacy features.
734
+ Enhancement: Added minimal API for adding additional entries to the Security admin menu.
735
+ Enhancement: File Change Scan uses a new batching mechanism to prevent crashing on hosts but still generating only one report per-day.
736
+ Enhancement: Introduce Distributed Storage framework for reducing the amount of data stored in the WordPress options table. This should improve performance for large sites using File Change.
737
+ Enhancement: Introduced Login Interstitial framework to consolidate code between Password Requirements & Two Factor.
738
+ Bug Fix: Added ability to show object data for classes that are not loaded to the Logs page.
739
+ Bug Fix: Changed the rules generated by the Filter Suspicious Query Strings feature in order to avoid blocking privacy export/erasure request confirmations.
740
+ Bug Fix: Ensure all users with the `manage_options` capability are available when selecting contacts in the Notification Center.
741
+ Bug Fix: Fix clearing or previous file scans results.
742
+ Bug Fix: Fix warnings on debug file change log items.
743
+ Bug Fix: Fixed logging system references to "fatal-error" that should be "fatal".
744
+ Bug Fix: Improve File Change recovery system on high-traffic websites.
745
+ Bug Fix: Improve clearing of previous File Change file hashes.
746
+ Bug Fix: Improved detection of REST API requests on sites without a home dir.
747
+ Bug Fix: Internal links to a filtered logs page.
748
+ Bug Fix: Prevent PHP warning about converting an array to a string when adding notification data.
749
+ Bug Fix: Prevent PHP warning when completing database backups that are not emailed to any recipients.
750
+ Bug Fix: Properly enforce strong passwords when on the WP Login Reset Password page.
751
+ Bug Fix: Resolve warnings when upgrading file change settings.
752
+ Minor: File Scan "chunk" option is removed.
753
+ Minor: Make recovering file scan log smaller.
754
+ Minor: Page Load Scheduler: Unschedule single events before running them. This mirrors the behavior of the WP Cron scheduler.
755
+ Minor: Security Digest now includes all lockouts that have occurred since the last email.
756
+ Minor: Shrink storage size of file scans.
757
+ Minor: Specifying a manual file scan list has been removed.
758
+ Minor: Track raw memory used by the file change scanner as well.
759
+ Minor: Updated list of File Change excluded file types to include more media extensions.
760
+ Misc: Added comment to prevent Tide from marking the plugin as not compatible with PHP 5.3.
761
+ Tweak: Add description for File Change recovery related logs.
762
+ Tweak: Don't report removed files if the removal is caused by a new file extension being excluded.
763
+ Tweak: File Change: Move "latest_changes" entry to a separate storage bucket to improve performance on large sites.
764
+ Tweak: File Change: Only scan a maximum of 10 plugins in a single chunk.
765
+ 7.0.1 - 2018-05-25 - Chris Jean & Timothy Jacobs
766
+ Bug Fix: Fixed an "Uncaught Error: Call to undefined function esc_like()" error that could occur when exporting or erasing personal data.
767
+ Bug Fix: Skip recovery if File Change storage is empty.
readme.txt CHANGED
@@ -2,8 +2,8 @@
2
  Contributors: ithemes, chrisjean, gerroald, mattdanner, timothyblynjacobs
3
  Tags: security, security plugin, malware, hack, secure, block, SSL, admin, htaccess, lockdown, login, protect, protection, anti virus, attack, injection, login security, maintenance, permissions, prevention, authentication, administration, password, brute force, ban, permissions, bots, user agents, xml rpc, security log
4
  Requires at least: 4.7
5
- Tested up to: 4.9.4
6
- Stable tag: 6.9.2
7
  Requires PHP: 5.2
8
  License: GPLv2 or later
9
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -189,6 +189,44 @@ Free support may be available with the help of the community in the <a href="htt
189
 
190
  == Changelog ==
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  = 6.9.2 =
193
  * Bug Fix: Fixed situation that could cause lockout notifications being sent for whitelisted IPs.
194
  * Bug Fix: Fixed issue where saving Global Settings would be blocked by an unwritable "Path to Log Files" path when the "Log Type" is set to "Database Only".
@@ -416,5 +454,5 @@ Free support may be available with the help of the community in the <a href="htt
416
 
417
  == Upgrade Notice ==
418
 
419
- = 6.9.2 =
420
- Version 6.9.2 contains various bug fixes. It is recommended for all users.
2
  Contributors: ithemes, chrisjean, gerroald, mattdanner, timothyblynjacobs
3
  Tags: security, security plugin, malware, hack, secure, block, SSL, admin, htaccess, lockdown, login, protect, protection, anti virus, attack, injection, login security, maintenance, permissions, prevention, authentication, administration, password, brute force, ban, permissions, bots, user agents, xml rpc, security log
4
  Requires at least: 4.7
5
+ Tested up to: 4.9.6
6
+ Stable tag: 7.0.1
7
  Requires PHP: 5.2
8
  License: GPLv2 or later
9
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
189
 
190
  == Changelog ==
191
 
192
+ = 7.0.1 =
193
+ * Bug Fix: Fixed an "Uncaught Error: Call to undefined function esc_like()" error that could occur when exporting or erasing personal data.
194
+ * Bug Fix: Skip recovery if File Change storage is empty.
195
+
196
+ = 7.0.0 =
197
+ * New Feature: Added support for the new WordPress privacy features.
198
+ * Enhancement: Added minimal API for adding additional entries to the Security admin menu.
199
+ * Enhancement: File Change Scan uses a new batching mechanism to prevent crashing on hosts but still generating only one report per-day.
200
+ * Enhancement: Introduce Distributed Storage framework for reducing the amount of data stored in the WordPress options table. This should improve performance for large sites using File Change.
201
+ * Enhancement: Introduced Login Interstitial framework to consolidate code between Password Requirements & Two Factor.
202
+ * Bug Fix: Added ability to show object data for classes that are not loaded to the Logs page.
203
+ * Bug Fix: Changed the rules generated by the Filter Suspicious Query Strings feature in order to avoid blocking privacy export/erasure request confirmations.
204
+ * Bug Fix: Ensure all users with the `manage_options` capability are available when selecting contacts in the Notification Center.
205
+ * Bug Fix: Fix clearing or previous file scans results.
206
+ * Bug Fix: Fix warnings on debug file change log items.
207
+ * Bug Fix: Fixed logging system references to "fatal-error" that should be "fatal".
208
+ * Bug Fix: Improve File Change recovery system on high-traffic websites.
209
+ * Bug Fix: Improve clearing of previous File Change file hashes.
210
+ * Bug Fix: Improved detection of REST API requests on sites without a home dir.
211
+ * Bug Fix: Internal links to a filtered logs page.
212
+ * Bug Fix: Prevent PHP warning about converting an array to a string when adding notification data.
213
+ * Bug Fix: Prevent PHP warning when completing database backups that are not emailed to any recipients.
214
+ * Bug Fix: Properly enforce strong passwords when on the WP Login Reset Password page.
215
+ * Bug Fix: Resolve warnings when upgrading file change settings.
216
+ * Minor: File Scan "chunk" option is removed.
217
+ * Minor: Make recovering file scan log smaller.
218
+ * Minor: Page Load Scheduler: Unschedule single events before running them. This mirrors the behavior of the WP Cron scheduler.
219
+ * Minor: Security Digest now includes all lockouts that have occurred since the last email.
220
+ * Minor: Shrink storage size of file scans.
221
+ * Minor: Specifying a manual file scan list has been removed.
222
+ * Minor: Track raw memory used by the file change scanner as well.
223
+ * Minor: Updated list of File Change excluded file types to include more media extensions.
224
+ * Misc: Added comment to prevent Tide from marking the plugin as not compatible with PHP 5.3.
225
+ * Tweak: Add description for File Change recovery related logs.
226
+ * Tweak: Don't report removed files if the removal is caused by a new file extension being excluded.
227
+ * Tweak: File Change: Move "latest_changes" entry to a separate storage bucket to improve performance on large sites.
228
+ * Tweak: File Change: Only scan a maximum of 10 plugins in a single chunk.
229
+
230
  = 6.9.2 =
231
  * Bug Fix: Fixed situation that could cause lockout notifications being sent for whitelisted IPs.
232
  * Bug Fix: Fixed issue where saving Global Settings would be blocked by an unwritable "Path to Log Files" path when the "Log Type" is set to "Database Only".
454
 
455
  == Upgrade Notice ==
456
 
457
+ = 7.0.1 =
458
+ Version 7.0.0 contains important additions to support privacy controls, bug fixes, and various enhancements. It is recommended for all users.