iThemes Security (formerly Better WP Security) - Version 7.3.0

Version Description

  • Enhancement: Add Per-Content SSL toggle to the upcoming Block Editor interface.
  • Enhancement: Add filter to the recipients list for email notifications: "itsec_notification_{$notification}email_recipients" and "itsec_notificationemail_recipients".
  • Enhancement: Add define "ITSEC_DISABLE_TEMP_WHITELIST" to disable the Temporary IP Whitelisting for logged-in administrators.
  • Enhancement: Improve redirecting after processing a login interstitial from a front-end login form.
  • Enhancement: Add loopback IP detection to Security Check.
  • Enhancement: Detect Server IPs in Security Check.
  • Tweak: Add additional safety checks when writing to system config files. This will log a "Critical Issue" when the writing of an empty or partial config file is detected and prevented.
  • Tweak: Improve File Change locking to help prevent failing scans on sites with inconsistent cron scheduling.
  • Tweak: Improve "System Tweaks Suspicious Query Strings SQLI" to reduce false positives.
  • Tweak: Improve "System Tweaks Disable PHP" to block PHP files in apache configurations that serve files with a trailing dot.
  • Tweak: Remove "Seznam Bot" from HackRepair List as it isn't present in the latest version.
  • Bug Fix: Include Hide Backend token when emailing a password reset URL.
  • Bug Fix: Notification Center - Only send notifications to users with an exact role match of selected roles instead of a fuzzy match based on selected capabilities.
  • Bug Fix: Error when trying to edit reusable blocks with per-post SSL enabled.
  • Bug Fix: Resolve warnings on PHP 5.2.
Download this release

Release Info

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

Code changes from version 7.2.0 to 7.3.0

Files changed (53) hide show
  1. better-wp-security.php +5 -3
  2. core/admin-pages/js/settings.js +2 -0
  3. core/admin-pages/module-settings.php +11 -0
  4. core/admin-pages/page-logs.php +10 -0
  5. core/admin-pages/page-settings.php +10 -0
  6. core/core.php +17 -4
  7. core/history.txt +33 -1
  8. core/lib.php +113 -3
  9. core/lib/class-itsec-lib-config-file.php +14 -0
  10. core/lib/class-itsec-lib-fingerprinting.php +9 -2
  11. core/lib/class-itsec-lib-login-interstitial.php +548 -292
  12. core/lib/class-itsec-lib-remote-messages.php +171 -0
  13. core/lib/class-itsec-mail.php +28 -0
  14. core/lib/form.php +10 -0
  15. core/lib/log-util.php +13 -2
  16. core/lib/login-interstitial/abstract-itsec-login-interstitial.php +138 -0
  17. core/lib/login-interstitial/class-itsec-login-interstitial-config-driven.php +154 -0
  18. core/lib/login-interstitial/class-itsec-login-interstitial-session.php +505 -0
  19. core/lib/login-interstitial/index.php +1 -0
  20. core/lib/login-interstitial/util.js +131 -0
  21. core/lib/mail-templates/large-button.html +29 -0
  22. core/lib/mail-templates/small-code.html +23 -0
  23. core/lib/schema.php +2 -1
  24. core/lib/validator.php +7 -1
  25. core/lockout.php +83 -6
  26. core/modules.php +9 -3
  27. core/modules/404-detection/class-itsec-four-oh-four.php +7 -7
  28. core/modules/backup/class-itsec-backup.php +24 -19
  29. core/modules/backup/settings-page.php +2 -2
  30. core/modules/ban-users/lists/hackrepair-apache.inc +0 -1
  31. core/modules/ban-users/lists/hackrepair-litespeed.inc +0 -1
  32. core/modules/ban-users/lists/hackrepair-nginx.inc +0 -1
  33. core/modules/brute-force/class-itsec-brute-force.php +5 -2
  34. core/modules/brute-force/logs.php +13 -4
  35. core/modules/file-change/logs.php +5 -1
  36. core/modules/file-change/scanner.php +65 -14
  37. core/modules/global/logs.php +52 -0
  38. core/modules/global/settings.php +1 -0
  39. core/modules/global/validator.php +4 -2
  40. core/modules/hide-backend/class-itsec-hide-backend.php +3 -1
  41. core/modules/malware/class-itsec-malware-scanner.php +13 -4
  42. core/modules/malware/logs.php +2 -0
  43. core/modules/notification-center/class-notification-center.php +42 -7
  44. core/modules/password-requirements/class-itsec-password-requirements.php +4 -2
  45. core/modules/security-check/active.php +27 -0
  46. core/modules/security-check/feedback-renderer.php +3 -0
  47. core/modules/security-check/scanner.php +69 -0
  48. core/modules/ssl/active.php +4 -6
  49. core/modules/ssl/class-itsec-ssl-admin.php +36 -2
  50. core/modules/ssl/js/block-editor.js +37 -0
  51. core/modules/system-tweaks/config-generators.php +7 -7
  52. history.txt +18 -1
  53. readme.txt +21 -4
better-wp-security.php CHANGED
@@ -6,15 +6,17 @@
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.2.0
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
13
  */
14
 
15
  function itsec_load_textdomain() {
16
-
17
- if ( function_exists( 'get_user_locale' ) && is_admin() ) {
 
 
18
  $locale = get_user_locale();
19
  } else {
20
  $locale = get_locale();
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.3.0
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
13
  */
14
 
15
  function itsec_load_textdomain() {
16
+
17
+ if ( function_exists( 'determine_locale' ) ) {
18
+ $locale = determine_locale();
19
+ } elseif ( function_exists( 'get_user_locale' ) && is_admin() ) {
20
  $locale = get_user_locale();
21
  } else {
22
  $locale = get_locale();
core/admin-pages/js/settings.js CHANGED
@@ -665,6 +665,8 @@ var itsecSettingsPage = {
665
  itsecSettingsPage.isModuleActive( module );
666
  }, 1000 );
667
 
 
 
668
  return;
669
  }
670
 
665
  itsecSettingsPage.isModuleActive( module );
666
  }, 1000 );
667
 
668
+ itsecSettingsPage.showErrors( results.errors, results.module, 'closed', 'error' );
669
+
670
  return;
671
  }
672
 
core/admin-pages/module-settings.php CHANGED
@@ -194,10 +194,21 @@ class ITSEC_Module_Settings_Page {
194
  */
195
  public function render( $form ) {
196
 
 
 
197
  ?>
198
  <div class="itsec-settings-module-description">
199
  <?php $this->render_description( $form ); ?>
200
  </div>
 
 
 
 
 
 
 
 
 
201
  <div class="itsec-settings-module-settings">
202
  <?php $this->render_settings( $form ); ?>
203
  </div>
194
  */
195
  public function render( $form ) {
196
 
197
+ $messages = ITSEC_Lib_Remote_Messages::get_messages_for_placement( array( 'module' => $this->id ) );
198
+
199
  ?>
200
  <div class="itsec-settings-module-description">
201
  <?php $this->render_description( $form ); ?>
202
  </div>
203
+ <?php if ( $messages ) : ?>
204
+ <div class="itsec-settings-module-service-status">
205
+ <?php foreach ( $messages as $message ): ?>
206
+ <div class="notice notice-alt notice-<?php echo esc_attr( $message['type'] ); ?> below-h2">
207
+ <p><?php echo $message['message']; ?></p>
208
+ </div>
209
+ <?php endforeach; ?>
210
+ </div>
211
+ <?php endif; ?>
212
  <div class="itsec-settings-module-settings">
213
  <?php $this->render_settings( $form ); ?>
214
  </div>
core/admin-pages/page-logs.php CHANGED
@@ -348,11 +348,21 @@ final class ITSEC_Logs_Page {
348
  }
349
 
350
  $details = wp_list_sort( $details, 'order', 'ASC', true );
 
351
  }
352
 
353
  ob_start();
354
 
355
  ?>
 
 
 
 
 
 
 
 
 
356
  <table class="form-table">
357
  <?php foreach ( $details as $row ) : ?>
358
  <tr>
348
  }
349
 
350
  $details = wp_list_sort( $details, 'order', 'ASC', true );
351
+ $messages = ITSEC_Lib_Remote_Messages::get_messages_for_placement( array( 'logs' => array( 'module' => $entry['module'], 'code' => $entry['code'] ) ) );
352
  }
353
 
354
  ob_start();
355
 
356
  ?>
357
+ <?php if ( $messages ) : ?>
358
+ <div class="itsec-logs-service-status">
359
+ <?php foreach ( $messages as $message ): ?>
360
+ <div class="notice notice-alt notice-<?php echo esc_attr( $message['type'] ); ?> below-h2">
361
+ <p><?php echo $message['message']; ?></p>
362
+ </div>
363
+ <?php endforeach; ?>
364
+ </div>
365
+ <?php endif; ?>
366
  <table class="form-table">
367
  <?php foreach ( $details as $row ) : ?>
368
  <tr>
core/admin-pages/page-settings.php CHANGED
@@ -494,6 +494,16 @@ final class ITSEC_Settings_Page {
494
  <div id="itsec-sidebar-widget-<?php echo $id; ?>" class="postbox itsec-sidebar-widget">
495
  <h3 class="hndle ui-sortable-handle"><span><?php echo esc_html( $widget->title ); ?></span></h3>
496
  <div class="inside">
 
 
 
 
 
 
 
 
 
 
497
  <?php $this->get_widget_settings( $id, $form, true ); ?>
498
  </div>
499
  </div>
494
  <div id="itsec-sidebar-widget-<?php echo $id; ?>" class="postbox itsec-sidebar-widget">
495
  <h3 class="hndle ui-sortable-handle"><span><?php echo esc_html( $widget->title ); ?></span></h3>
496
  <div class="inside">
497
+ <?php if ( $messages = ITSEC_Lib_Remote_Messages::get_messages_for_placement( array( 'widget' => $id ) ) ) : ?>
498
+ <div class="itsec-widgets-service-status">
499
+ <?php foreach ( $messages as $message ): ?>
500
+ <div class="notice notice-alt notice-<?php echo esc_attr( $message['type'] ); ?> below-h2">
501
+ <p><?php echo $message['message']; ?></p>
502
+ </div>
503
+ <?php endforeach; ?>
504
+ </div>
505
+ <?php endif; ?>
506
+
507
  <?php $this->get_widget_settings( $id, $form, true ); ?>
508
  </div>
509
  </div>
core/core.php CHANGED
@@ -24,7 +24,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
24
  *
25
  * @access private
26
  */
27
- private $plugin_build = 4108;
28
 
29
  /**
30
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
@@ -50,7 +50,8 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
50
  $current_time_gmt,
51
  $is_iwp_call,
52
  $request_type,
53
- $wp_upload_dir;
 
54
 
55
 
56
  /**
@@ -121,6 +122,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
121
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-password-requirements.php' );
122
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-login-interstitial.php' );
123
  require( $this->plugin_dir . 'core/lib/class-itsec-lib-distributed-storage.php' );
 
124
 
125
  require( $this->plugin_dir . 'core/lib/class-itsec-scheduler.php' );
126
  require( $this->plugin_dir . 'core/lib/class-itsec-job.php' );
@@ -162,14 +164,16 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
162
  add_action( 'admin_bar_menu', array( $this, 'modify_admin_bar' ), 99 );
163
  }
164
 
165
- $login_interstitial = new ITSEC_Lib_Login_Interstitial();
166
- $login_interstitial->run();
167
 
168
  if ( defined( 'ITSEC_USE_CRON' ) && ITSEC_USE_CRON !== ITSEC_Lib::use_cron() ) {
169
  ITSEC_Modules::set_setting( 'global', 'use_cron', ITSEC_USE_CRON );
170
  }
171
 
172
  do_action( 'itsec_initialized' );
 
 
173
  }
174
 
175
  private function setup_scheduler() {
@@ -258,6 +262,15 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
258
  return $self->scheduler;
259
  }
260
 
 
 
 
 
 
 
 
 
 
261
  /**
262
  * Retrieve the global instance of the Sync API.
263
  *
24
  *
25
  * @access private
26
  */
27
+ private $plugin_build = 4112;
28
 
29
  /**
30
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
50
  $current_time_gmt,
51
  $is_iwp_call,
52
  $request_type,
53
+ $wp_upload_dir,
54
+ $login_interstitial;
55
 
56
 
57
  /**
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
+ require( $this->plugin_dir . 'core/lib/class-itsec-lib-remote-messages.php' );
126
 
127
  require( $this->plugin_dir . 'core/lib/class-itsec-scheduler.php' );
128
  require( $this->plugin_dir . 'core/lib/class-itsec-job.php' );
164
  add_action( 'admin_bar_menu', array( $this, 'modify_admin_bar' ), 99 );
165
  }
166
 
167
+ $this->login_interstitial = new ITSEC_Lib_Login_Interstitial();
168
+ $this->login_interstitial->run();
169
 
170
  if ( defined( 'ITSEC_USE_CRON' ) && ITSEC_USE_CRON !== ITSEC_Lib::use_cron() ) {
171
  ITSEC_Modules::set_setting( 'global', 'use_cron', ITSEC_USE_CRON );
172
  }
173
 
174
  do_action( 'itsec_initialized' );
175
+
176
+ ITSEC_Lib_Remote_Messages::init();
177
  }
178
 
179
  private function setup_scheduler() {
262
  return $self->scheduler;
263
  }
264
 
265
+ /**
266
+ * Get the login interstitial library instance.
267
+ *
268
+ * @return ITSEC_Lib_Login_Interstitial
269
+ */
270
+ public static function get_login_interstitial() {
271
+ return self::get_instance()->login_interstitial;
272
+ }
273
+
274
  /**
275
  * Retrieve the global instance of the Sync API.
276
  *
core/history.txt CHANGED
@@ -753,4 +753,36 @@
753
  Bug Fix: Improve detection of blocking the File Change Scan from being scheduled if one is already being run.
754
  Bug Fix: Prevent infinite recursion error when trying to access directories outside of the allowed file tree.
755
  4.8.1 - 2018-10-10 - Chris Jean & Timothy Jacobs
756
- Enhancement: Allow for selecting the particular Proxy header a server is configured to use. Improve the language to indicate the importance of configuring this setting. H/t Filippo Cavallarin CEO at wearesegment.com
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  Bug Fix: Improve detection of blocking the File Change Scan from being scheduled if one is already being run.
754
  Bug Fix: Prevent infinite recursion error when trying to access directories outside of the allowed file tree.
755
  4.8.1 - 2018-10-10 - Chris Jean & Timothy Jacobs
756
+ Enhancement: Allow for selecting the particular Proxy header a server is configured to use. Improve the language to indicate the importance of configuring this setting. H/t Filippo Cavallarin CEO at wearesegment.com
757
+ 4.8.2 - 2018-10-10 - Chris Jean & Timothy Jacobs
758
+ Bug Fix: Fix issue with saving Global settings if Security Check Pro has detected the correct Proxy Header to use.
759
+ 4.8.3 - 2018-11-01 - Chris Jean & Timothy Jacobs
760
+ Enhancement: Add support for displaying status messages about services that might be encountering issues without updating the plugin.
761
+ 4.8.4 - 2018-12-04 - Chris Jean & Timothy Jacobs
762
+ Enhancement: Add Per-Content SSL toggle to the upcoming Block Editor interface.
763
+ Enhancement: Add filter to the recipients list for email notifications: "itsec_notification_{$notification}_email_recipients" and "itsec_notification_email_recipients".
764
+ Enhancement: Detect Server IPs in Security Check.
765
+ Tweak: Improve File Change locking to help prevent failing scans on sites with inconsistent cron scheduling.
766
+ Tweak: Improve "System Tweaks – Suspicious Query Strings – SQLI" to reduce false positives.
767
+ Tweak: Improve "System Tweaks – Disable PHP" to block PHP files in apache configurations that serve files with a trailing dot.
768
+ Tweak: Add additional safety checks when writing to system config files.
769
+ Tweak: Remove "Seznam Bot" from HackRepair List as it isn't present in the latest version.
770
+ Bug Fix: Notification Center - Only send notifications to users with an exact role match of selected roles instead of a fuzzy match based on selected capabilities.
771
+ Bug Fix: Resolve warnings on PHP 5.2.
772
+ 4.8.5 - 2018-12-04 - Chris Jean & Timothy Jacobs
773
+ Bug Fix: Don't try to get users with the selected role if no roles are selected.
774
+ 4.8.6 - 2018-12-11 - Chris Jean & Timothy Jacobs
775
+ Bug Fix: Only re-add Trusted Devices restricted capabilities filter if it was registered in the first place.
776
+ Bug Fix: Error when trying to edit reusable blocks with per-post SSL enabled.
777
+ 4.9.0 - 2019-01-10 - Chris Jean & Timothy Jacobs
778
+ Enhancement: Add loopback IP detection to Security Check.
779
+ Enhancement: Add define "ITSEC_DISABLE_TEMP_WHITELIST" to disable the Temporary IP Whitelisting for logged-in administrators.
780
+ Tweak: Only run Remote Messages API on Pro versions.
781
+ 4.9.1 - 2019-01-14 - Chris Jean & Timothy Jacobs
782
+ Bug Fix: Styling issue that made "Identified Loopback IP" look like an error message instead of a success.
783
+ 5.0.0 - 2019-01-16 - Chris Jean & Timothy Jacobs
784
+ Enhancement: Add support for number inputs and validators.
785
+ 5.1.0 - 2019-02-13 - Chris Jean & Timothy Jacobs
786
+ Enhancement: Improve redirecting after processing a login interstitial from a front-end login form.
787
+ Tweak: Add display description for log when safe guarding against an empty config file write.
788
+ Bug Fix: Include Hide Backend token when emailing a password reset URL.
core/lib.php CHANGED
@@ -1294,15 +1294,15 @@ final class ITSEC_Lib {
1294
  $suhosin = preg_split( '/\s*,\s*/', (string) ini_get( 'suhosin.executor.func.blacklist' ) );
1295
  }
1296
 
1297
- if ( ! \is_callable( $func ) ) {
1298
  return $cache[ $func ] = false;
1299
  }
1300
 
1301
- if ( \in_array( $func, $disabled, true ) ) {
1302
  return $cache[ $func ] = false;
1303
  }
1304
 
1305
- if ( \in_array( $func, $suhosin, true ) ) {
1306
  return $cache[ $func ] = false;
1307
  }
1308
 
@@ -1575,4 +1575,114 @@ final class ITSEC_Lib {
1575
  public static function clear_cookie( $name ) {
1576
  setcookie( $name, ' ', ITSEC_Core::get_current_time_gmt() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, false );
1577
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1578
  }
1294
  $suhosin = preg_split( '/\s*,\s*/', (string) ini_get( 'suhosin.executor.func.blacklist' ) );
1295
  }
1296
 
1297
+ if ( ! is_callable( $func ) ) {
1298
  return $cache[ $func ] = false;
1299
  }
1300
 
1301
+ if ( in_array( $func, $disabled, true ) ) {
1302
  return $cache[ $func ] = false;
1303
  }
1304
 
1305
+ if ( in_array( $func, $suhosin, true ) ) {
1306
  return $cache[ $func ] = false;
1307
  }
1308
 
1575
  public static function clear_cookie( $name ) {
1576
  setcookie( $name, ' ', ITSEC_Core::get_current_time_gmt() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, false );
1577
  }
1578
+
1579
+ /**
1580
+ * Is the current request a loopback request.
1581
+ *
1582
+ * @return bool
1583
+ */
1584
+ public static function is_loopback_request() {
1585
+ return in_array( self::get_ip(), ITSEC_Modules::get_setting( 'global', 'server_ips' ), true );
1586
+ }
1587
+
1588
+ /**
1589
+ * Version of {@see wp_slash()} that won't cast numbers to strings.
1590
+ *
1591
+ * @param array|string $value
1592
+ *
1593
+ * @return array|string
1594
+ */
1595
+ public static function slash( $value ) {
1596
+ if ( is_array( $value ) ) {
1597
+ foreach ( $value as $k => $v ) {
1598
+ if ( is_array( $v ) ) {
1599
+ $value[ $k ] = self::slash( $v );
1600
+ } elseif ( is_string( $v ) ) {
1601
+ $value[ $k ] = addslashes( $v );
1602
+ }
1603
+ }
1604
+ } elseif ( is_string( $value ) ) {
1605
+ $value = addslashes( $value );
1606
+ }
1607
+
1608
+ return $value;
1609
+ }
1610
+
1611
+ /**
1612
+ * Format as a ISO 8601 date.
1613
+ *
1614
+ * @param int|string $date Epoch or strtotime compatible date.
1615
+ *
1616
+ * @return string|false
1617
+ */
1618
+ public static function to_rest_date( $date = 0 ) {
1619
+ if ( ! $date ) {
1620
+ $date = ITSEC_Core::get_current_time_gmt();
1621
+ } elseif ( ! is_int( $date ) ) {
1622
+ $date = strtotime( $date );
1623
+ }
1624
+
1625
+ return gmdate( 'Y-m-d\TH:i:sP', $date );
1626
+ }
1627
+
1628
+ /**
1629
+ * Flatten an array.
1630
+ *
1631
+ * @param array $array
1632
+ *
1633
+ * @return array
1634
+ */
1635
+ public static function flatten( $array ) {
1636
+ if ( ! is_array( $array ) ) {
1637
+ return array( $array );
1638
+ }
1639
+
1640
+ $merge = array();
1641
+
1642
+ foreach ( $array as $value ) {
1643
+ $merge[] = self::flatten( $value );
1644
+ }
1645
+
1646
+ return $merge ? call_user_func_array( 'array_merge', $merge ) : array();
1647
+ }
1648
+
1649
+ /**
1650
+ * Preload REST API requests.
1651
+ *
1652
+ * @param array $requests
1653
+ *
1654
+ * @return array
1655
+ */
1656
+ public static function preload_rest_requests( $requests ) {
1657
+ $preload = array();
1658
+
1659
+ foreach ( $requests as $key => $config ) {
1660
+ if ( is_string( $config ) ) {
1661
+ $key = $config;
1662
+ $config = array( 'route' => $config );
1663
+ }
1664
+
1665
+ $request = new WP_REST_Request(
1666
+ isset( $config['method'] ) ? $config['method'] : 'GET',
1667
+ $config['route']
1668
+ );
1669
+
1670
+ if ( ! empty( $config['query'] ) ) {
1671
+ $request->set_query_params( $config['query'] );
1672
+ }
1673
+
1674
+ $response = rest_do_request( $request );
1675
+
1676
+ if ( $response->get_status() >= 200 && $response->get_status() < 300 ) {
1677
+ rest_send_allow_header( $response, rest_get_server(), $request );
1678
+
1679
+ $preload[ $key ] = array(
1680
+ 'body' => rest_get_server()->response_to_data( $response, ! empty( $config['embed'] ) ),
1681
+ 'headers' => $response->get_headers()
1682
+ );
1683
+ }
1684
+ }
1685
+
1686
+ return $preload;
1687
+ }
1688
  }
core/lib/class-itsec-lib-config-file.php CHANGED
@@ -465,6 +465,20 @@ class ITSEC_Lib_Config_File {
465
  return $contents;
466
  }
467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
 
469
  $modification = ltrim( $modification, "\x0B\r\n\0" );
470
  $modification = rtrim( $modification, " \t\x0B\r\n\0" );
465
  return $contents;
466
  }
467
 
468
+ if ( ! $contents && in_array( $type, array( 'apache', 'wp-config' ), true ) ) {
469
+ $display_file = str_replace( '\\', '/', $file );
470
+ $abspath = str_replace( '\\', '/', ABSPATH );
471
+ $display_file = preg_replace( '/^' . preg_quote( $abspath, '/' ) . '/', '', $display_file );
472
+ $display_file = ltrim( $display_file, '/' );
473
+
474
+ $error = new WP_Error( "itsec-config-file-update-empty::{$type}", sprintf(
475
+ __( 'Empty file encountered when attempting to update <code>%1$s</code>. Manual configuration for the <code>%1$s</code> file can be found on the Security > Settings page in the Advanced section.', 'better-wp-security' ),
476
+ $display_file
477
+ ) );
478
+ ITSEC_Log::add_critical_issue( 'core', $error->get_error_code(), $error );
479
+
480
+ return $error;
481
+ }
482
 
483
  $modification = ltrim( $modification, "\x0B\r\n\0" );
484
  $modification = rtrim( $modification, " \t\x0B\r\n\0" );
core/lib/class-itsec-lib-fingerprinting.php CHANGED
@@ -162,7 +162,14 @@ class ITSEC_Lib_Fingerprinting {
162
 
163
  require_once( ITSEC_Core::get_core_dir() . 'lib/class-itsec-lib-canonical-roles.php' );
164
 
165
- return ITSEC_Lib_Canonical_Roles::is_user_at_least( $role, $user );
 
 
 
 
 
 
 
166
  }
167
 
168
  /**
@@ -190,4 +197,4 @@ class ITSEC_Lib_Fingerprinting {
190
 
191
  return self::$sources;
192
  }
193
- }
162
 
163
  require_once( ITSEC_Core::get_core_dir() . 'lib/class-itsec-lib-canonical-roles.php' );
164
 
165
+ $had_filter = remove_filter( 'user_has_cap', array( 'ITSEC_Fingerprinting', 'restrict_capabilities' ), 10 );
166
+ $applies = ITSEC_Lib_Canonical_Roles::is_user_at_least( $role, $user );
167
+
168
+ if ( $had_filter ) {
169
+ add_filter( 'user_has_cap', array( 'ITSEC_Fingerprinting', 'restrict_capabilities' ), 10, 4 );
170
+ }
171
+
172
+ return $applies;
173
  }
174
 
175
  /**
197
 
198
  return self::$sources;
199
  }
200
+ }
core/lib/class-itsec-lib-login-interstitial.php CHANGED
@@ -1,14 +1,29 @@
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 */
@@ -17,6 +32,9 @@ class ITSEC_Lib_Login_Interstitial {
17
  /** @var string */
18
  private $session_token;
19
 
 
 
 
20
  /**
21
  * Initialize the module.
22
  *
@@ -35,29 +53,25 @@ class ITSEC_Lib_Login_Interstitial {
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' ), -1000, 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
 
@@ -66,36 +80,43 @@ class ITSEC_Lib_Login_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
 
@@ -104,33 +125,107 @@ class ITSEC_Lib_Login_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
  */
@@ -141,30 +236,65 @@ class ITSEC_Lib_Login_Interstitial {
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' )
@@ -176,6 +306,8 @@ class ITSEC_Lib_Login_Interstitial {
176
 
177
  /**
178
  * Force the requested interstitial to be displayed if the user is already logged-in.
 
 
179
  */
180
  public function force_interstitial() {
181
 
@@ -183,9 +315,9 @@ class ITSEC_Lib_Login_Interstitial {
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
 
@@ -193,18 +325,22 @@ class ITSEC_Lib_Login_Interstitial {
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 ] ] ) ) {
@@ -215,6 +351,8 @@ class ITSEC_Lib_Login_Interstitial {
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
@@ -231,112 +369,188 @@ class ITSEC_Lib_Login_Interstitial {
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
- remove_action( 'wp_login', array( $this, 'wp_login' ), - 1000 );
279
- do_action( 'wp_login', $user->user_login, $user );
280
 
281
- /**
282
- * Fires when a user is re-logged back in after submitting an interstitial.
283
- *
284
- * @param WP_User $user
285
- */
286
- do_action( 'itsec_login_interstitial_logged_in', $user );
287
 
288
- if ( $GLOBALS['interim_login'] = isset( $_REQUEST['interim-login'] ) ) {
289
- $this->interim_login();
290
  }
291
 
292
- if ( empty( $_REQUEST['redirect_to'] ) ) {
293
- $redirect_to = admin_url( 'index.php' );
294
- $requested = '';
295
- } else {
296
- $redirect_to = $requested = $_REQUEST['redirect_to'];
297
  }
298
 
299
- $redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested, $user );
300
- wp_safe_redirect( $redirect_to );
 
 
 
 
301
 
302
- die;
 
 
 
 
 
303
  }
304
 
305
  /**
306
  * Ajax Handler.
 
 
307
  */
308
  public function ajax_handler() {
309
 
310
- if ( empty( $_POST['itsec_interstitial_action'] ) ) {
311
- return;
312
- }
313
 
314
- $action = $_POST['itsec_interstitial_action'];
 
 
 
 
 
315
 
316
- if ( empty( $this->registered[ $action ]['ajax_handler'] ) ) {
317
- wp_send_json_error( array( 'message' => esc_html__( 'Invalid Interstitial Action', 'better-wp-security' ) ) );
318
  }
319
 
320
- $opts = $this->registered[ $action ];
 
 
 
 
 
 
321
 
322
- $user = $this->get_user( $action, true );
323
 
324
- if ( is_wp_error( $user ) ) {
325
- wp_send_json_error( array( 'message' => $user->get_error_message() ) );
326
  }
327
 
328
- if ( ! $this->result( $opts['show_to_user'], array( $user, isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) ) ) ) {
329
- wp_send_json_error( array( 'message' => esc_html__( 'Unsupported Interstitial', 'better-wp-security' ) ) );
 
 
330
  }
331
 
332
  $data = $_POST;
333
- unset( $data['itsec_interstitial_user'], $data['itsec_interstitial_token'] );
334
 
335
- call_user_func( $opts['ajax_handler'], $user, $data );
 
336
  }
337
 
338
  /**
339
  * Handle displaying the interstitial form.
 
 
340
  */
341
  public function display() {
342
 
@@ -346,49 +560,33 @@ class ITSEC_Lib_Login_Interstitial {
346
  return;
347
  }
348
 
349
- $opts = $this->registered[ $action ];
350
-
351
- $user = null;
352
- $token = isset( $_REQUEST['itsec_interstitial_token'] ) ? $_REQUEST['itsec_interstitial_token'] : null;
353
-
354
- $user = $this->get_user( $action );
355
-
356
- if ( ! $user ) {
357
- wp_safe_redirect( set_url_scheme( wp_login_url(), 'login_post' ) );
358
- die;
359
- }
360
 
361
- if ( ! $this->result( $opts['show_to_user'], array( $user, isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) ) ) ) {
362
  wp_safe_redirect( set_url_scheme( wp_login_url(), 'login_post' ) );
363
  die;
364
  }
365
 
366
- $this->login_html( $user, $action, $token );
367
  die;
368
  }
369
 
370
  /**
371
  * Display an interstitial form during the login process.
372
  *
373
- * @param WP_User $user
374
- * @param string $action
375
- * @param string $token
376
  */
377
- protected function login_html( $user, $action, $token = null ) {
378
-
379
- $wp_login_url = set_url_scheme( wp_login_url(), 'login_post' );
380
- $wp_login_url = add_query_arg( 'action', "itsec-{$action}", $wp_login_url );
381
-
382
- if ( isset( $_GET['wpe-login'] ) && ! preg_match( '/[&?]wpe-login=/', $wp_login_url ) ) {
383
- $wp_login_url = add_query_arg( 'wpe-login', $_GET['wpe-login'], $wp_login_url );
384
- }
385
-
386
- $interim_login = isset( $_REQUEST['interim-login'] );
387
- $redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : '';
388
 
389
- $rememberme = ! empty( $_REQUEST['rememberme'] );
 
 
390
 
391
- wp_enqueue_script( 'user-profile' );
 
392
 
393
  // Prevent JetPack from attempting to SSO the update password form.
394
  add_filter( 'jetpack_sso_allowed_actions', '__return_empty_array' );
@@ -397,41 +595,29 @@ class ITSEC_Lib_Login_Interstitial {
397
  require_once( dirname( __FILE__ ) . '/includes/function.login-header.php' );
398
  }
399
 
400
- $opts = $this->registered[ $action ];
401
-
402
  login_header();
 
 
403
  ?>
404
 
405
  <?php if ( $this->error ) : ?>
406
  <div id="login-error" class="message" style="border-left-color: #dc3232;">
407
  <?php echo $this->error->get_error_message(); ?>
408
  </div>
409
- <?php elseif ( $message = $this->result( $opts['info_message'], array( $user ) ) ): ?>
410
  <p class="message"><?php echo $message; ?></p>
411
  <?php endif; ?>
412
 
413
  <form name="itsec-<?php echo esc_attr( $action ); ?>" id="itsec-<?php echo esc_attr( $action ); ?>"
414
  action="<?php echo esc_url( $wp_login_url ); ?>" method="post" autocomplete="off">
415
 
416
- <?php call_user_func( $opts['render'], $user, compact( 'token', 'wp_login_url', 'redirect_to', 'rememberme' ) ); ?>
417
-
418
- <?php if ( $interim_login ) : ?>
419
- <input type="hidden" name="interim-login" value="1"/>
420
- <?php else : ?>
421
- <input type="hidden" name="redirect_to" value="<?php echo esc_url( $redirect_to ); ?>"/>
422
- <?php endif; ?>
423
 
424
- <input type="hidden" name="rememberme" id="rememberme" value="<?php echo esc_attr( $rememberme ); ?>"/>
425
  <input type="hidden" name="action" value="<?php echo esc_attr( "itsec-{$action}" ); ?>">
426
 
427
- <?php if ( null !== $token ): ?>
428
- <input type="hidden" name="itsec_interstitial_user" value="<?php echo esc_attr( $user->ID ); ?>">
429
- <input type="hidden" name="itsec_interstitial_token" value="<?php echo esc_attr( $token ); ?>">
430
- <?php endif; ?>
431
-
432
- <?php if ( isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ], $this->registered[ $_REQUEST[ self::SHOW_AFTER_LOGIN ] ] ) ) : ?>
433
- <input type="hidden" name="<?php echo esc_attr( self::SHOW_AFTER_LOGIN ); ?>" value="<?php echo esc_attr( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ); ?>">
434
- <?php endif; ?>
435
  </form>
436
 
437
  <p id="backtoblog">
@@ -473,11 +659,11 @@ class ITSEC_Lib_Login_Interstitial {
473
 
474
  <?php if ( $customize_login ) : ?>
475
  <script type="text/javascript">
476
- setTimeout( function () {
477
  new wp.customize.Messenger( {
478
  url : '<?php echo wp_customize_url(); ?>',
479
- channel: 'login'
480
- } ).send( 'login' )
481
  }, 1000 );
482
  </script>
483
  <?php endif; ?>
@@ -486,26 +672,164 @@ class ITSEC_Lib_Login_Interstitial {
486
  <?php die;
487
  }
488
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  /**
490
  * Get the next interstitial to be displayed.
491
  *
492
- * @param string $current The interstitial that was just submitted.
493
- * @param WP_User $user
494
  *
495
  * @return string|false
496
  */
497
- private function get_next_interstitial( $current, $user ) {
498
 
499
- $past_current = false;
 
 
500
 
501
- foreach ( $this->get_applicable_interstitials( $user ) as $handler => $opts ) {
502
- if ( $handler === $current ) {
503
- $past_current = true;
504
- continue;
505
  }
 
506
 
507
- if ( $past_current ) {
508
- return $handler;
 
509
  }
510
  }
511
 
@@ -523,9 +847,9 @@ class ITSEC_Lib_Login_Interstitial {
523
 
524
  $applicable = array();
525
 
526
- foreach ( $this->registered as $action => $opts ) {
527
  if ( $this->is_interstitial_applicable( $action, $user ) ) {
528
- $applicable[ $action ] = $opts;
529
  }
530
  }
531
 
@@ -535,21 +859,20 @@ class ITSEC_Lib_Login_Interstitial {
535
  /**
536
  * Is the interstitial applicable to the given user.
537
  *
538
- * @param string $interstitial
539
- *
540
  * @param WP_User $user
541
  *
542
  * @return bool
543
  */
544
- private function is_interstitial_applicable( $interstitial, $user ) {
545
 
546
- $opts = $this->registered[ $interstitial ];
547
 
548
- if ( ! $this->result( $opts['show_to_user'], array( $user, isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) ) ) ) {
549
  return false;
550
  }
551
 
552
- if ( ! did_action( 'login_init' ) && $this->result( $opts['wp_login_only'], array( $user ) ) ) {
553
  return false;
554
  }
555
 
@@ -557,83 +880,86 @@ class ITSEC_Lib_Login_Interstitial {
557
  }
558
 
559
  /**
560
- * Handle checking for and validating the token, if it does not exist, will redirect with error message.
561
  *
562
- * @param string $action
563
- * @param bool $return_error
564
  *
565
- * @return WP_Error|array Array with token and user.
566
  */
567
- private function handle_token( $action, $return_error = false ) {
568
 
569
- $is_valid = true;
570
- $user = null;
 
 
571
 
572
- if ( empty( $_REQUEST['itsec_interstitial_user'] ) || empty( $_REQUEST['itsec_interstitial_token'] ) ) {
573
- $is_valid = false;
574
- } elseif ( ( ! $user = get_userdata( $_REQUEST['itsec_interstitial_user'] ) ) || ! $this->verify_token( $user, $_REQUEST['itsec_interstitial_token'] ) ) {
575
- $is_valid = false;
576
  }
577
 
578
- if ( ! $is_valid ) {
579
- if ( $return_error ) {
580
- return new WP_Error(
581
- 'itsec-login-interstitial-invalid-token',
582
- esc_html__( 'Sorry, this request has expired. Please log in again.', 'better-wp-security' )
583
- );
584
- }
585
 
586
- $this->redirect_invalid_token( $action );
 
587
  }
588
 
589
- return array( $user, $_REQUEST['itsec_interstitial_token'] );
 
 
 
 
 
 
590
  }
591
 
592
  /**
593
- * Get the current user for the interstitial.
594
  *
595
- * @param string $action
596
- * @param bool $return_error
597
  *
598
- * @return WP_Error|WP_User|null
599
  */
600
- private function get_user( $action, $return_error = false ) {
601
 
602
- $opts = $this->registered[ $action ];
 
 
 
603
 
604
- if ( false === $opts['force_completion'] ) {
605
- return is_user_logged_in() ? wp_get_current_user() : null;
606
  }
607
 
608
- if ( isset( $_REQUEST['itsec_interstitial_user'] ) || true === $opts['force_completion'] ) {
609
- $maybe = $this->handle_token( $action, $return_error );
610
 
611
- return is_wp_error( $maybe ) ? $maybe : $maybe[0];
 
612
  }
613
 
614
- $user = wp_get_current_user();
615
-
616
- if ( $user && $user->exists() && ! call_user_func( $opts['force_completion'], $user ) ) {
617
- return $user;
618
- }
619
 
620
- if ( $return_error ) {
621
- return new WP_Error(
622
- 'itsec-login-interstitial-invalid-token',
623
- esc_html__( 'Sorry, this request has expired. Please log in again.', 'better-wp-security' )
624
- );
625
  }
626
 
627
- $this->redirect_invalid_token( $action );
628
  }
629
 
630
  /**
631
  * Redirect back to the login page with a message that the token is invalid.
632
- *
633
- * @param string $action
634
  */
635
- private function redirect_invalid_token( $action ) {
636
- $redirect = add_query_arg( 'itsec_interstitial_expired', $action, wp_login_url() );
 
 
 
 
 
 
637
  wp_safe_redirect( set_url_scheme( $redirect, 'login_post' ) );
638
  die;
639
  }
@@ -643,90 +969,20 @@ class ITSEC_Lib_Login_Interstitial {
643
  *
644
  * @param WP_User $user
645
  */
646
- private function destroy_session( $user ) {
647
  WP_Session_Tokens::get_instance( $user->ID )->destroy( $this->session_token ? $this->session_token : wp_get_session_token() );
648
  wp_clear_auth_cookie();
649
  }
650
 
651
  /**
652
- * Verify that the token is valid.
653
- *
654
- * @param WP_User $user
655
- * @param string $key
656
- *
657
- * @return bool
658
- */
659
- private function verify_token( $user, $key ) {
660
- $expected = get_user_meta( $user->ID, self::META_KEY, true );
661
-
662
- if ( ! $expected || ! is_array( $expected ) ) {
663
- return false;
664
- }
665
-
666
- if ( empty( $expected['expires'] ) || $expected['expires'] < ITSEC_Core::get_current_time_gmt() ) {
667
- return false;
668
- }
669
-
670
- return hash_equals( $expected['key'], $key );
671
- }
672
-
673
- /**
674
- * Set the token for a user.
675
- *
676
- * @param WP_User $user
677
- *
678
- * @return string
679
- */
680
- private function set_token( $user ) {
681
- $key = $this->generate_token();
682
-
683
- update_user_meta( $user->ID, self::META_KEY, array(
684
- 'key' => $key,
685
- 'expires' => ITSEC_Core::get_current_time_gmt() + HOUR_IN_SECONDS
686
- ) );
687
-
688
- return $key;
689
- }
690
-
691
- /**
692
- * Generate a token to be used to verify intent of submitting a login interstitial.
693
  *
694
- * We can't use nonces here because the WordPress Session Tokens won't be initialized yet.
 
695
  *
696
- * @return string
697
- */
698
- private function generate_token() {
699
- return sha1( wp_generate_password( 32, true, true ) );
700
- }
701
-
702
- /**
703
- * Delete the token for a user.
704
- *
705
- * @param WP_User $user
706
- */
707
- private function delete_token( $user ) {
708
- delete_user_meta( $user->ID, self::META_KEY );
709
- }
710
-
711
- /**
712
- * Try and get a value from the provider.
713
- *
714
- * If it is a function, will call the function with the provided args.
715
- *
716
- * @param bool|callable $provider
717
- * @param array $args
718
- *
719
- * @return bool|mixed
720
  */
721
- private function result( $provider, $args = array() ) {
722
- if ( is_bool( $provider ) ) {
723
- return $provider;
724
- }
725
-
726
- if ( is_callable( $provider, true ) ) {
727
- return call_user_func_array( $provider, $args );
728
- }
729
-
730
- return $provider;
731
  }
732
- }
1
  <?php
2
 
3
+ require_once( dirname( __FILE__ ) . '/login-interstitial/class-itsec-login-interstitial-session.php' );
4
+ require_once( dirname( __FILE__ ) . '/login-interstitial/abstract-itsec-login-interstitial.php' );
5
+ require_once( dirname( __FILE__ ) . '/login-interstitial/class-itsec-login-interstitial-config-driven.php' );
6
+
7
  /**
8
  * Class ITSEC_Lib_Login_Interstitial
9
  */
10
  class ITSEC_Lib_Login_Interstitial {
11
 
12
  const SHOW_AFTER_LOGIN = 'itsec_after_interstitial';
 
13
  const AJAX = 'itsec-login-interstitial-ajax';
14
 
15
+ const R_USER = 'itsec_interstitial_user';
16
+ const R_TOKEN = 'itsec_interstitial_token';
17
+ const R_SESSION = 'itsec_interstitial_session';
18
+ const R_EXPIRED = 'itsec_interstitial_expired';
19
+ const R_INTERSTITIAL = 'itsec_interstitial';
20
+ const R_ASYNC_ACTION = 'itsec_interstitial_async_action';
21
+ const R_GET_STATE = 'itsec_interstitial_get_state';
22
+
23
+ const C_SAME_BROWSER = 'itsec_interstitial_browser';
24
+ const SAME_BROWSER_PAYLOAD = 'same-browser';
25
+
26
+ /** @var ITSEC_Login_Interstitial[] */
27
  private $registered = array();
28
 
29
  /** @var WP_Error */
32
  /** @var string */
33
  private $session_token;
34
 
35
+ /** @var ITSEC_Login_Interstitial_Session|null */
36
+ private $current_session;
37
+
38
  /**
39
  * Initialize the module.
40
  *
53
  return;
54
  }
55
 
56
+ uasort( $this->registered, array( $this, '_sort_interstitials' ) );
57
 
58
+ add_action( 'login_enqueue_scripts', array( $this, 'enqueue' ) );
59
+ add_action( 'wp_login', array( $this, 'wp_login' ), - 1000, 2 );
60
  add_action( 'wp_login_errors', array( $this, 'handle_token_expired' ) );
61
  add_action( 'login_init', array( $this, 'force_interstitial' ) );
62
  add_action( 'login_form', array( $this, 'ferry_after_login' ) );
63
  add_filter( 'auth_cookie', array( $this, 'capture_session_token' ), 10, 5 );
64
 
65
+ add_action( 'wp_ajax_' . self::AJAX, array( $this, 'ajax_handler' ) );
66
+ add_action( 'wp_ajax_nopriv_' . self::AJAX, array( $this, 'ajax_handler' ) );
67
 
68
+ foreach ( $this->registered as $id => $interstitial ) {
69
+ if ( $interstitial->has_async_action() ) {
70
+ add_action( "login_form_itsec-{$id}", array( $this, 'async_action' ), 8 );
71
  }
72
 
73
+ add_action( "login_form_itsec-{$id}", array( $this, 'submit' ), 9 );
74
  add_action( "login_form_itsec-{$id}", array( $this, 'display' ) );
 
 
 
 
 
 
 
75
  }
76
  }
77
 
80
  *
81
  * @api
82
  *
83
+ * @param string $slug
84
+ * @param ITSEC_Login_Interstitial|callable $render_or_class
85
+ * @param array $opts
86
  *
87
  * @return bool
88
  */
89
+ public function register( $slug, $render_or_class, $opts = array() ) {
90
+
91
+ if ( $render_or_class instanceof ITSEC_Login_Interstitial ) {
92
+ $this->registered[ $slug ] = $render_or_class;
93
+
94
+ return true;
95
+ }
96
+
97
  $opts = wp_parse_args( $opts, array(
98
  'force_completion' => true, // Will logout the user's session before displaying the interstitial.
99
  'show_to_user' => true, // Boolean or callable.
100
  'wp_login_only' => false, // Only show the interstitial if the login form is submitted from wp-login.php,
101
  'submit' => false, // Callable called with user when submitting the form.
102
+ 'async_action' => false, // Callable called when a user clicks a link to perform an interstitial action.
103
  'info_message' => false,
104
  'after_submit' => false,
105
  'ajax_handler' => false,
106
  'priority' => 5,
107
  ) );
108
 
109
+ $opts['render'] = $render_or_class;
110
 
111
+ try {
112
+ $this->registered[ $slug ] = new ITSEC_Login_Interstitial_Config_Driven( $opts );
113
+ } catch ( Exception $e ) {
114
+ /** @noinspection ForgottenDebugOutputInspection */
115
+ error_log( $e->getMessage() );
116
 
 
117
  return false;
118
  }
119
 
 
 
120
  return true;
121
  }
122
 
125
  *
126
  * @api
127
  *
128
+ * @param ITSEC_Login_Interstitial_Session $session
 
129
  *
130
  * @return void
131
  */
132
+ public function show_interstitial( ITSEC_Login_Interstitial_Session $session ) {
133
 
134
+ if ( ! isset( $this->registered[ $session->get_current_interstitial() ] ) ) {
135
  return;
136
  }
137
 
138
+ $this->current_session = $session;
139
 
140
+ $interstitial = $this->registered[ $session->get_current_interstitial() ];
141
+
142
+ if ( $interstitial->is_completion_forced( $session ) ) {
143
+ $this->destroy_session_token( $session->get_user() );
 
144
  }
145
 
146
+ $this->login_html( $session );
147
  die;
148
  }
149
 
150
+ /**
151
+ * Handle proceeding the session to the next interstitial.
152
+ *
153
+ * @api
154
+ *
155
+ * @param ITSEC_Login_Interstitial_Session $session
156
+ */
157
+ public function proceed_to_next( ITSEC_Login_Interstitial_Session $session ) {
158
+
159
+ $current = $session->get_current_interstitial();
160
+ $session->add_completed_interstitial( $current );
161
+
162
+ $session->set_current_interstitial( $this->get_next_interstitial( $session ) );
163
+ $session->save();
164
+ }
165
+
166
+ /**
167
+ * Get the current interstitial session.
168
+ *
169
+ * @api
170
+ *
171
+ * @return ITSEC_Login_Interstitial_Session|null
172
+ */
173
+ public function get_current_session() {
174
+ return $this->current_session;
175
+ }
176
+
177
+ /**
178
+ * Get the URL to an async action.
179
+ *
180
+ * @api
181
+ *
182
+ * @param ITSEC_Login_Interstitial_Session $session
183
+ * @param string $action
184
+ *
185
+ * @return string
186
+ */
187
+ public function get_async_action_url( ITSEC_Login_Interstitial_Session $session, $action ) {
188
+
189
+ $url = $this->get_base_wp_login_url();
190
+ $url = add_query_arg( array(
191
+ 'action' => "itsec-{$session->get_current_interstitial()}",
192
+ self::R_USER => $session->get_user()->ID,
193
+ self::R_TOKEN => $session->get_signature_for_payload( $action ),
194
+ self::R_SESSION => $session->get_id(),
195
+ self::R_ASYNC_ACTION => $action,
196
+ ), $url );
197
+
198
+ return $url;
199
+ }
200
+
201
+ /**
202
+ * Initialize the same browser functionality.
203
+ *
204
+ * This sets a cookie with a signature payload.
205
+ *
206
+ * @api
207
+ *
208
+ * @param ITSEC_Login_Interstitial_Session $session
209
+ */
210
+ public function initialize_same_browser( ITSEC_Login_Interstitial_Session $session ) {
211
+ ITSEC_Lib::set_cookie( self::C_SAME_BROWSER, $session->get_signature_for_payload( self::SAME_BROWSER_PAYLOAD ) );
212
+ }
213
+
214
+ /**
215
+ * Register the interstitial helper script.
216
+ *
217
+ * @internal
218
+ */
219
+ public function enqueue() {
220
+ wp_register_script( 'itsec-login-interstitial-util', plugin_dir_url( __FILE__ ) . '/login-interstitial/util.js', array( 'jquery', 'wp-util' ), 1 );
221
+ wp_add_inline_script( 'itsec-login-interstitial-util', '(function() { window.itsecLoginInterstitial = new ITSECLoginInterstitial(); window.itsecLoginInterstitial.init() })()' );
222
+ }
223
+
224
  /**
225
  * During the login process, check if we have any interstitials to display, and display them.
226
  *
227
+ * @internal
228
+ *
229
  * @param string $username
230
  * @param WP_User $user
231
  */
236
  return;
237
  }
238
 
239
+ foreach ( $this->get_applicable_interstitials( $user ) as $action => $opts ) {
240
+ $session = ITSEC_Login_Interstitial_Session::create( $user, $action );
241
 
242
+ $this->build_session_from_global_state( $session );
243
 
244
+ if ( isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ], $this->registered[ $_REQUEST[ self::SHOW_AFTER_LOGIN ] ] ) ) {
245
+ $session->add_show_after( $_REQUEST[ self::SHOW_AFTER_LOGIN ] );
246
  }
247
+
248
+ $session->save();
249
+
250
+ $this->show_interstitial( $session );
251
  }
252
 
253
+ if ( isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ], $this->registered[ $_REQUEST[ self::SHOW_AFTER_LOGIN ] ] ) ) {
254
+ $session = ITSEC_Login_Interstitial_Session::create( $user, $_REQUEST[ self::SHOW_AFTER_LOGIN ] );
255
+ $this->build_session_from_global_state( $session );
256
+ $session->save();
257
+ $this->show_interstitial( $session );
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Build up the interstitial session configuration from the global state.
263
+ *
264
+ * This does not set the show afters because this is also used by the show after code.
265
+ *
266
+ * @param ITSEC_Login_Interstitial_Session $session
267
+ */
268
+ private function build_session_from_global_state( ITSEC_Login_Interstitial_Session $session ) {
269
+ if ( isset( $_REQUEST['interim-login'] ) ) {
270
+ $session->set_interim_login();
271
+ }
272
+
273
+ if ( ! empty( $_REQUEST['redirect_to'] ) ) {
274
+ $session->set_redirect_to( $_REQUEST['redirect_to'] );
275
+ } elseif ( ! did_action( 'login_init' ) && ( $ref = wp_get_referer() ) ) {
276
+ $session->set_redirect_to( $ref );
277
+ } elseif ( ! did_action( 'login_init' ) ) {
278
+ $session->set_redirect_to( $_SERVER['REQUEST_URI'] );
279
+ }
280
+
281
+ if ( ! empty( $_REQUEST['rememberme'] ) ) {
282
+ $session->set_remember_me();
283
  }
284
  }
285
 
286
  /**
287
  * Add a message that the interstitial expired.
288
  *
289
+ * @internal
290
+ *
291
  * @param WP_Error $errors
292
  *
293
  * @return WP_Error
294
  */
295
  public function handle_token_expired( $errors ) {
296
 
297
+ if ( isset( $_GET[ self::R_EXPIRED ] ) ) {
298
  $errors->add(
299
  'itsec-login-interstitial-invalid-token',
300
  esc_html__( 'Sorry, this request has expired. Please log in again.', 'better-wp-security' )
306
 
307
  /**
308
  * Force the requested interstitial to be displayed if the user is already logged-in.
309
+ *
310
+ * @internal
311
  */
312
  public function force_interstitial() {
313
 
315
  return;
316
  }
317
 
318
+ $slug = $_REQUEST[ self::SHOW_AFTER_LOGIN ];
319
 
320
+ if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
321
  return;
322
  }
323
 
325
  return;
326
  }
327
 
328
+ $user = wp_get_current_user();
329
+ $interstitial = $this->registered[ $slug ];
330
 
331
+ if ( ! $interstitial->show_to_user( $user, true ) ) {
332
  wp_safe_redirect( admin_url() );
333
  die;
334
  }
335
 
336
+ $session = ITSEC_Login_Interstitial_Session::create( $user, $slug );
337
+ $this->show_interstitial( $session );
338
  }
339
 
340
  /**
341
  * Ferry the after login interstitial query var into the form.
342
+ *
343
+ * @internal
344
  */
345
  public function ferry_after_login() {
346
  if ( ! empty( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) && isset( $this->registered[ $_REQUEST[ self::SHOW_AFTER_LOGIN ] ] ) ) {
351
  /**
352
  * Capture the session token to log out the user.
353
  *
354
+ * @internal
355
+ *
356
  * @param string $cookie
357
  * @param int $user_id
358
  * @param int $expiration
369
 
370
  /**
371
  * Handle submitting the interstitial form.
372
+ *
373
+ * @internal
374
  */
375
  public function submit() {
376
+ $session = $this->get_and_verify_session();
377
+ $slug = $session->get_current_interstitial();
378
+
379
+ // If we think we have all finished all the interstitials.
380
+ // We need to check because another process may have moved the interstitial forward.
381
+ if ( ! $slug ) {
382
+ // Double check to ensure we are actually finished.
383
+ if ( $next = $this->get_next_interstitial( $session ) ) {
384
+ // If not, display the next interstitial.
385
+ $session->set_current_interstitial( $next );
386
+ $session->save();
387
+ }
388
+
389
+ $this->do_next_step( $session );
390
+ }
391
 
392
+ if ( 'POST' !== $_SERVER['REQUEST_METHOD'] || empty( $_POST['action'] ) ) {
393
  return;
394
  }
395
 
396
+ if ( empty( $this->registered[ $slug ] ) ) {
397
  return;
398
  }
399
 
400
+ $requested_slug = substr( $_POST['action'], strlen( 'itsec-' ) );
401
 
402
+ if ( $slug !== $requested_slug ) {
403
+ // If we have already completed the action that was requested, then just display
404
+ // the new interstitial.
405
+ if ( $session->is_interstitial_completed( $requested_slug ) ) {
406
+ $this->show_interstitial( $session );
407
+ } else {
408
+ $this->redirect_invalid_token();
409
+ }
410
  }
411
 
412
+ $this->current_session = $session;
413
+
414
+ $interstitial = $this->registered[ $slug ];
415
+
416
+ if ( $interstitial->has_submit() ) {
417
+ $maybe_error = $interstitial->submit( $session, $_POST );
418
 
419
+ if ( is_wp_error( $maybe_error ) ) {
420
+ $this->error = $maybe_error;
421
+
422
+ return;
423
+ }
424
+ }
425
+
426
+ $interstitial->after_submit( $session, $_POST );
427
+
428
+ $this->proceed_to_next( $session );
429
+ $this->do_next_step( $session );
430
+ }
431
 
432
+ /**
433
+ * Handle an async GET action to the interstitial.
434
+ *
435
+ * @internal
436
+ */
437
+ public function async_action() {
438
+
439
+ if ( empty( $_GET[ self::R_ASYNC_ACTION ] ) ) {
440
  return;
441
  }
442
 
443
+ $session = $this->get_and_verify_session_for_async_action( true );
444
+
445
+ if ( is_wp_error( $session ) ) {
446
+ $this->display_wp_login_message( $session );
447
  }
448
 
449
+ $action = $_GET[ self::R_ASYNC_ACTION ];
450
+ $slug = $session->get_current_interstitial();
451
+
452
+ if ( empty( $this->registered[ $slug ] ) ) {
453
+ $this->display_wp_login_message( new WP_Error( 'unsupported', esc_html__( 'Unsupported Interstitial. Please login again.', 'better-wp-security' ) ) );
454
  }
455
 
456
+ $interstitial = $this->registered[ $slug ];
457
+
458
+ if ( ! $interstitial->has_async_action() ) {
459
+ $this->display_wp_login_message( new WP_Error( 'unsupported', esc_html__( 'Unsupported Interstitial. Please login again.', 'better-wp-security' ) ) );
460
  }
461
 
462
+ $args = array(
463
+ 'same_browser' => false,
464
+ );
465
+
466
+ if (
467
+ isset( $_COOKIE[ self::C_SAME_BROWSER ] ) &&
468
+ true === $session->verify_signature_for_payload( self::SAME_BROWSER_PAYLOAD, $_COOKIE[ self::C_SAME_BROWSER ] )
469
+ ) {
470
+ $args['same_browser'] = true;
471
  }
472
 
473
+ $this->current_session = $session;
474
+ $result = $interstitial->handle_async_action( $session, $action, $args );
475
 
476
+ if ( null === $result ) {
477
+ $this->display_wp_login_message( new WP_Error( 'unsupported', esc_html__( 'Unsupported Interstitial. Please login again.', 'better-wp-security' ) ) );
478
+ }
 
 
 
479
 
480
+ if ( is_wp_error( $result ) ) {
481
+ $this->display_wp_login_message( $result );
482
  }
483
 
484
+ if ( true === $result ) {
485
+ $result = array();
 
 
 
486
  }
487
 
488
+ if ( $args['same_browser'] && empty( $args['allow_same_browser'] ) ) {
489
+ $this->do_next_step( $session, array(
490
+ 'delete' => false,
491
+ 'allow_interim' => false,
492
+ ) );
493
+ }
494
 
495
+ $result = wp_parse_args( $result, array(
496
+ 'message' => esc_html__( 'Action processed. Please continue in your original browser.', 'better-wp-security' ),
497
+ 'title' => esc_html__( 'Action Processed', 'better-wp-security' ),
498
+ ) );
499
+
500
+ $this->display_wp_login_message( $result );
501
  }
502
 
503
  /**
504
  * Ajax Handler.
505
+ *
506
+ * @internal
507
  */
508
  public function ajax_handler() {
509
 
510
+ $session = $this->get_and_verify_session( true );
511
+ $get_state = ! empty( $_REQUEST[ self::R_GET_STATE ] );
 
512
 
513
+ if ( is_wp_error( $session ) ) {
514
+ if ( $get_state && is_user_logged_in() ) {
515
+ wp_send_json_success( array(
516
+ 'logged_in' => true,
517
+ ) );
518
+ }
519
 
520
+ wp_send_json_error( array( 'message' => $session->get_error_message() ) );
 
521
  }
522
 
523
+ if ( $get_state ) {
524
+ wp_send_json_success( array(
525
+ 'current' => $session->get_current_interstitial(),
526
+ 'completed' => $session->get_completed_interstitials(),
527
+ 'state' => $session->get_state(),
528
+ ) );
529
+ }
530
 
531
+ $slug = $session->get_current_interstitial();
532
 
533
+ if ( empty( $this->registered[ $slug ] ) ) {
534
+ wp_send_json_error( array( 'message' => esc_html__( 'Invalid Interstitial Action', 'better-wp-security' ) ) );
535
  }
536
 
537
+ $interstitial = $this->registered[ $slug ];
538
+
539
+ if ( ! $interstitial->has_ajax_handlers() ) {
540
+ wp_send_json_error( array( 'message' => esc_html__( 'Invalid Interstitial Action', 'better-wp-security' ) ) );
541
  }
542
 
543
  $data = $_POST;
544
+ unset( $data[ self::R_USER ], $data[ self::R_TOKEN ], $data[ self::R_SESSION ] );
545
 
546
+ $this->current_session = $session;
547
+ $interstitial->handle_ajax( $session, $data );
548
  }
549
 
550
  /**
551
  * Handle displaying the interstitial form.
552
+ *
553
+ * @internal
554
  */
555
  public function display() {
556
 
560
  return;
561
  }
562
 
563
+ $interstitial = $this->registered[ $action ];
564
+ $session = $this->get_and_verify_session();
 
 
 
 
 
 
 
 
 
565
 
566
+ if ( ! $interstitial->show_to_user( $session->get_user(), $session->is_current_requested() ) ) {
567
  wp_safe_redirect( set_url_scheme( wp_login_url(), 'login_post' ) );
568
  die;
569
  }
570
 
571
+ $this->login_html( $session );
572
  die;
573
  }
574
 
575
  /**
576
  * Display an interstitial form during the login process.
577
  *
578
+ * @internal
579
+ *
580
+ * @param ITSEC_Login_Interstitial_Session $session
581
  */
582
+ protected function login_html( ITSEC_Login_Interstitial_Session $session ) {
 
 
 
 
 
 
 
 
 
 
583
 
584
+ $user = $session->get_user();
585
+ $action = $session->get_current_interstitial();
586
+ $interstitial = $this->registered[ $action ];
587
 
588
+ $wp_login_url = $this->get_base_wp_login_url();
589
+ $wp_login_url = add_query_arg( 'action', "itsec-{$action}", $wp_login_url );
590
 
591
  // Prevent JetPack from attempting to SSO the update password form.
592
  add_filter( 'jetpack_sso_allowed_actions', '__return_empty_array' );
595
  require_once( dirname( __FILE__ ) . '/includes/function.login-header.php' );
596
  }
597
 
 
 
598
  login_header();
599
+
600
+ wp_enqueue_script( 'itsec-login-interstitial-util' );
601
  ?>
602
 
603
  <?php if ( $this->error ) : ?>
604
  <div id="login-error" class="message" style="border-left-color: #dc3232;">
605
  <?php echo $this->error->get_error_message(); ?>
606
  </div>
607
+ <?php elseif ( $message = $interstitial->get_info_message( $session ) ): ?>
608
  <p class="message"><?php echo $message; ?></p>
609
  <?php endif; ?>
610
 
611
  <form name="itsec-<?php echo esc_attr( $action ); ?>" id="itsec-<?php echo esc_attr( $action ); ?>"
612
  action="<?php echo esc_url( $wp_login_url ); ?>" method="post" autocomplete="off">
613
 
614
+ <?php $interstitial->render( $session, compact( 'wp_login_url' ) ); ?>
 
 
 
 
 
 
615
 
 
616
  <input type="hidden" name="action" value="<?php echo esc_attr( "itsec-{$action}" ); ?>">
617
 
618
+ <input type="hidden" name="<?php echo esc_attr( self::R_USER ) ?>" value="<?php echo esc_attr( $user->ID ); ?>">
619
+ <input type="hidden" name="<?php echo esc_attr( self::R_TOKEN ) ?>" value="<?php echo esc_attr( $session->get_signature() ); ?>">
620
+ <input type="hidden" name="<?php echo esc_attr( self::R_SESSION ) ?>" value="<?php echo esc_attr( $session->get_id() ); ?>">
 
 
 
 
 
621
  </form>
622
 
623
  <p id="backtoblog">
659
 
660
  <?php if ( $customize_login ) : ?>
661
  <script type="text/javascript">
662
+ setTimeout( function() {
663
  new wp.customize.Messenger( {
664
  url : '<?php echo wp_customize_url(); ?>',
665
+ channel: 'login',
666
+ } ).send( 'login' );
667
  }, 1000 );
668
  </script>
669
  <?php endif; ?>
672
  <?php die;
673
  }
674
 
675
+ /**
676
+ * Display a message on the WP-Login screen.
677
+ *
678
+ * @param WP_Error|array $message
679
+ */
680
+ private function display_wp_login_message( $message ) {
681
+ if ( ! function_exists( 'login_header' ) ) {
682
+ require_once( dirname( __FILE__ ) . '/includes/function.login-header.php' );
683
+ }
684
+
685
+ login_header();
686
+
687
+ ?>
688
+ <?php if ( is_wp_error( $message ) ) : ?>
689
+ <div id="login-error" class="message" style="border-left-color: #dc3232;">
690
+ <?php echo $message->get_error_message(); ?>
691
+ </div>
692
+ <?php elseif ( ! empty( $message['message'] ) ): ?>
693
+ <p class="message"><?php echo $message['message']; ?></p>
694
+ <?php endif; ?>
695
+
696
+ <p id="backtoblog">
697
+ <a href="<?php echo esc_url( home_url( '/' ) ); ?>" title="<?php esc_attr_e( 'Are you lost?', 'better-wp-security' ); ?>">
698
+ <?php echo esc_html( sprintf( __( '&larr; Back to %s', 'better-wp-security' ), get_bloginfo( 'title', 'display' ) ) ); ?>
699
+ </a>
700
+ </p>
701
+
702
+ </div>
703
+ <?php do_action( 'login_footer' ); ?>
704
+ <div class="clear"></div>
705
+ </body>
706
+ </html>
707
+ <?php
708
+ die;
709
+ }
710
+
711
+ /**
712
+ * Do the next step for a session.
713
+ *
714
+ * If there are more steps, show the next step, otherwise log the user in.
715
+ *
716
+ * @param ITSEC_Login_Interstitial_Session $session
717
+ * @param array $args
718
+ */
719
+ private function do_next_step( ITSEC_Login_Interstitial_Session $session, array $args = array() ) {
720
+ $args = wp_parse_args( $args, array(
721
+ 'delete' => true,
722
+ 'allow_interim' => true,
723
+ ) );
724
+
725
+ if ( $session->get_current_interstitial() ) {
726
+ $this->show_interstitial( $session );
727
+ } else {
728
+ if ( true === $args['delete'] ) {
729
+ $session->delete();
730
+ }
731
+
732
+ $this->handle_interstitials_completed( $session, $args );
733
+ }
734
+ }
735
+
736
+ /**
737
+ * Handle when all of the interstitials have been processed.
738
+ *
739
+ * @param ITSEC_Login_Interstitial_Session $session
740
+ * @param array $args
741
+ */
742
+ private function handle_interstitials_completed( ITSEC_Login_Interstitial_Session $session, array $args ) {
743
+
744
+ $user = $session->get_user();
745
+ $secure = '';
746
+
747
+ // If the user wants SSL but the session is not SSL, force a secure cookie.
748
+ if ( ! force_ssl_admin() && get_user_option( 'use_ssl', $user->ID ) ) {
749
+ $secure = true;
750
+ force_ssl_admin( true );
751
+ }
752
+
753
+ if ( ! is_user_logged_in() ) {
754
+ wp_set_auth_cookie( $user->ID, $session->is_remember_me(), $secure );
755
+
756
+ remove_action( 'wp_login', array( $this, 'wp_login' ), - 1000 );
757
+ do_action( 'wp_login', $user->user_login, $user );
758
+
759
+ /**
760
+ * Fires when a user is re-logged back in after submitting an interstitial.
761
+ *
762
+ * @param WP_User $user
763
+ */
764
+ do_action( 'itsec_login_interstitial_logged_in', $user );
765
+ }
766
+
767
+ if ( $args['allow_interim'] && $session->is_interim_login() ) {
768
+ $this->interim_login();
769
+ }
770
+
771
+ if ( $session->get_redirect_to() ) {
772
+ $redirect_to = $requested = $session->get_redirect_to();
773
+
774
+ if ( $secure && false !== strpos( $redirect_to, 'wp-admin' ) ) {
775
+ $redirect_to = preg_replace( '|^http://|', 'https://', $redirect_to );
776
+ }
777
+ } else {
778
+ $redirect_to = admin_url();
779
+ $requested = '';
780
+ }
781
+
782
+ if ( ! $redirect_to || $redirect_to === 'wp-admin/' || $redirect_to === admin_url() ) {
783
+ // If the user doesn't belong to a blog, send them to user admin. If the user can't edit posts, send them to their profile.
784
+ if ( is_multisite() && ! get_active_blog_for_user( $user->ID ) && ! is_super_admin( $user->ID ) ) {
785
+ $redirect_to = user_admin_url();
786
+ } elseif ( is_multisite() && ! $user->has_cap( 'read' ) ) {
787
+ $redirect_to = get_dashboard_url( $user->ID );
788
+ } elseif ( ! $user->has_cap( 'edit_posts' ) ) {
789
+ $redirect_to = $user->has_cap( 'read' ) ? admin_url( 'profile.php' ) : home_url();
790
+ }
791
+ }
792
+
793
+ $redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested, $user );
794
+ wp_safe_redirect( $redirect_to );
795
+
796
+ die;
797
+ }
798
+
799
+ /**
800
+ * Get the base wp login URL.
801
+ *
802
+ * @return string
803
+ */
804
+ private function get_base_wp_login_url() {
805
+ $wp_login_url = set_url_scheme( wp_login_url(), 'login_post' );
806
+
807
+ if ( isset( $_GET['wpe-login'] ) && ! preg_match( '/[&?]wpe-login=/', $wp_login_url ) ) {
808
+ $wp_login_url = add_query_arg( 'wpe-login', $_GET['wpe-login'], $wp_login_url );
809
+ }
810
+
811
+ return $wp_login_url;
812
+ }
813
+
814
  /**
815
  * Get the next interstitial to be displayed.
816
  *
817
+ * @param ITSEC_Login_Interstitial_Session $session
 
818
  *
819
  * @return string|false
820
  */
821
+ private function get_next_interstitial( ITSEC_Login_Interstitial_Session $session ) {
822
 
823
+ foreach ( $this->get_applicable_interstitials( $session->get_user() ) as $action => $interstitial ) {
824
+ if ( ! $session->is_interstitial_completed( $action ) ) {
825
+ return $action;
826
 
 
 
 
 
827
  }
828
+ }
829
 
830
+ foreach ( $session->get_show_after() as $action ) {
831
+ if ( ! $session->is_interstitial_completed( $action ) ) {
832
+ return $action;
833
  }
834
  }
835
 
847
 
848
  $applicable = array();
849
 
850
+ foreach ( $this->registered as $action => $interstitial ) {
851
  if ( $this->is_interstitial_applicable( $action, $user ) ) {
852
+ $applicable[ $action ] = $interstitial;
853
  }
854
  }
855
 
859
  /**
860
  * Is the interstitial applicable to the given user.
861
  *
862
+ * @param string $action
 
863
  * @param WP_User $user
864
  *
865
  * @return bool
866
  */
867
+ private function is_interstitial_applicable( $action, $user ) {
868
 
869
+ $interstitial = $this->registered[ $action ];
870
 
871
+ if ( ! $interstitial->show_to_user( $user, false ) ) {
872
  return false;
873
  }
874
 
875
+ if ( ! did_action( 'login_init' ) && $interstitial->show_on_wp_login_only( $user ) ) {
876
  return false;
877
  }
878
 
880
  }
881
 
882
  /**
883
+ * Get the active session.
884
  *
885
+ * @param bool $return_error
 
886
  *
887
+ * @return ITSEC_Login_Interstitial_Session|WP_Error
888
  */
889
+ private function get_and_verify_session( $return_error = false ) {
890
 
891
+ $error = new WP_Error(
892
+ 'itsec-login-interstitial-invalid-token',
893
+ esc_html__( 'Sorry, this request has expired. Please log in again.', 'better-wp-security' )
894
+ );
895
 
896
+ if ( ! isset( $_REQUEST[ self::R_USER ], $_REQUEST[ self::R_TOKEN ], $_REQUEST[ self::R_SESSION ] ) ) {
897
+ return $return_error ? $error : $this->redirect_invalid_token();
 
 
898
  }
899
 
900
+ $session = ITSEC_Login_Interstitial_Session::get( $_REQUEST[ self::R_SESSION ] );
 
 
 
 
 
 
901
 
902
+ if ( is_wp_error( $session ) ) {
903
+ return $return_error ? $error : $this->redirect_invalid_token();
904
  }
905
 
906
+ $valid = $session->verify( (int) $_REQUEST[ self::R_USER ], $_REQUEST[ self::R_TOKEN ] );
907
+
908
+ if ( true !== $valid ) {
909
+ return $return_error ? $error : $this->redirect_invalid_token();
910
+ }
911
+
912
+ return $session;
913
  }
914
 
915
  /**
916
+ * Get the active session and verify for performing an async action.
917
  *
918
+ * @param bool $return_error
 
919
  *
920
+ * @return ITSEC_Login_Interstitial_Session|WP_Error
921
  */
922
+ private function get_and_verify_session_for_async_action( $return_error = false ) {
923
 
924
+ $error = new WP_Error(
925
+ 'itsec-login-interstitial-invalid-token',
926
+ esc_html__( 'Sorry, this request has expired. Please log in again.', 'better-wp-security' )
927
+ );
928
 
929
+ if ( ! isset( $_REQUEST[ self::R_USER ], $_REQUEST[ self::R_TOKEN ], $_REQUEST[ self::R_SESSION ], $_REQUEST[ self::R_ASYNC_ACTION ] ) ) {
930
+ return $return_error ? $error : $this->redirect_invalid_token();
931
  }
932
 
933
+ $session = ITSEC_Login_Interstitial_Session::get( $_REQUEST[ self::R_SESSION ] );
 
934
 
935
+ if ( is_wp_error( $session ) ) {
936
+ return $return_error ? $error : $this->redirect_invalid_token();
937
  }
938
 
939
+ $valid = $session->verify_for_payload(
940
+ $_REQUEST[ self::R_ASYNC_ACTION ],
941
+ (int) $_REQUEST[ self::R_USER ],
942
+ $_REQUEST[ self::R_TOKEN ]
943
+ );
944
 
945
+ if ( true !== $valid ) {
946
+ return $return_error ? $error : $this->redirect_invalid_token();
 
 
 
947
  }
948
 
949
+ return $session;
950
  }
951
 
952
  /**
953
  * Redirect back to the login page with a message that the token is invalid.
 
 
954
  */
955
+ private function redirect_invalid_token() {
956
+
957
+ if ( ! isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ] ) && is_user_logged_in() ) {
958
+ wp_safe_redirect( admin_url() );
959
+ die;
960
+ }
961
+
962
+ $redirect = add_query_arg( self::R_EXPIRED, 1, wp_login_url() );
963
  wp_safe_redirect( set_url_scheme( $redirect, 'login_post' ) );
964
  die;
965
  }
969
  *
970
  * @param WP_User $user
971
  */
972
+ private function destroy_session_token( $user ) {
973
  WP_Session_Tokens::get_instance( $user->ID )->destroy( $this->session_token ? $this->session_token : wp_get_session_token() );
974
  wp_clear_auth_cookie();
975
  }
976
 
977
  /**
978
+ * Sort interstitials according to priority.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979
  *
980
+ * @param ITSEC_Login_Interstitial $a
981
+ * @param ITSEC_Login_Interstitial $b
982
  *
983
+ * @return int
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  */
985
+ private function _sort_interstitials( $a, $b ) {
986
+ return $a->get_priority() - $b->get_priority();
 
 
 
 
 
 
 
 
987
  }
988
+ }
core/lib/class-itsec-lib-remote-messages.php ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_Lib_Remote_Messages {
4
+
5
+ const URL = 'https://ithemes.com/api/itsec-service-status.json';
6
+ const OPTION = 'itsec_remote_messages';
7
+ const EVENT = 'remote-messages';
8
+
9
+ /** @var array */
10
+ private static $_response;
11
+
12
+ /**
13
+ * Initialize the Remote Messages library.
14
+ */
15
+ public static function init() {
16
+ if ( ITSEC_Core::is_pro() ) {
17
+ add_action( 'itsec_scheduled_' . self::EVENT, array( __CLASS__, 'run_event' ) );
18
+ }
19
+ }
20
+
21
+ public static function get_actions() {
22
+
23
+ $response = self::get_response();
24
+
25
+ return isset( $response['actions'] ) ? $response['actions'] : array();
26
+ }
27
+
28
+ public static function has_action( $action ) {
29
+ return in_array( $action, self::get_actions(), true );
30
+ }
31
+
32
+ public static function get_raw_messages() {
33
+ $response = self::get_response();
34
+
35
+ return isset( $response['messages'] ) ? $response['messages'] : array();
36
+ }
37
+
38
+ public static function get_messages_for_placement( $placement ) {
39
+
40
+ $matched = array();
41
+
42
+ foreach ( self::get_raw_messages() as $message ) {
43
+ if ( in_array( $placement, $message['placement'], true ) ) {
44
+ $matched[] = array(
45
+ 'message' => $message['message'],
46
+ 'type' => $message['type'],
47
+ );
48
+ }
49
+ }
50
+
51
+ return $matched;
52
+ }
53
+
54
+ /**
55
+ * Run the event to fetch the data.
56
+ *
57
+ * @param ITSEC_Job $job
58
+ */
59
+ public static function run_event( $job ) {
60
+
61
+ $response = wp_remote_get( self::URL, array(
62
+ 'user-agent' => 'WordPress',
63
+ ) );
64
+
65
+ if ( is_wp_error( $response ) ) {
66
+ $job->reschedule_in( 5 * MINUTE_IN_SECONDS );
67
+
68
+ return;
69
+ }
70
+
71
+ $data = wp_remote_retrieve_body( $response );
72
+
73
+ if ( ! $data ) {
74
+ $job->reschedule_in( 5 * MINUTE_IN_SECONDS );
75
+
76
+ return;
77
+ }
78
+
79
+ $json = json_decode( $data, true );
80
+
81
+ if ( ! $json ) {
82
+ $job->reschedule_in( 5 * MINUTE_IN_SECONDS );
83
+
84
+ return;
85
+ }
86
+
87
+ $json = wp_parse_args( $json, array(
88
+ 'ttl' => HOUR_IN_SECONDS,
89
+ 'messages' => array(),
90
+ 'actions' => array(),
91
+ ) );
92
+
93
+ $sanitized = array(
94
+ 'messages' => array(),
95
+ 'actions' => wp_parse_slug_list( $json['actions'] ),
96
+ );
97
+
98
+ foreach ( $json['messages'] as $message ) {
99
+ $sanitized['messages'][] = array(
100
+ 'message' => self::sanitize_message( $message['message'] ),
101
+ 'type' => self::sanitize_type( $message['type'] ),
102
+ 'placement' => $message['placement'],
103
+ );
104
+ }
105
+
106
+ update_site_option( self::OPTION, array(
107
+ 'response' => $sanitized,
108
+ 'ttl' => $json['ttl'],
109
+ 'requested' => ITSEC_Core::get_current_time_gmt(),
110
+ ) );
111
+ }
112
+
113
+ private static function sanitize_message( $message ) {
114
+ return wp_kses( $message, array( 'a' => array( 'href' => true ) ) );
115
+ }
116
+
117
+ private static function sanitize_type( $type ) {
118
+ if ( in_array( $type, array( 'success', 'info', 'warning', 'error' ), true ) ) {
119
+ return $type;
120
+ }
121
+
122
+ return 'info';
123
+ }
124
+
125
+ private static function get_response() {
126
+
127
+ if ( ! ITSEC_Core::is_pro() ) {
128
+ return array();
129
+ }
130
+
131
+ if ( isset( self::$_response ) ) {
132
+ return self::$_response;
133
+ }
134
+
135
+ $data = get_site_option( self::OPTION, array() );
136
+ $data = wp_parse_args( $data, array(
137
+ 'response' => array(),
138
+ 'requested' => 0,
139
+ 'ttl' => 0,
140
+ ) );
141
+
142
+ if ( ! $data['response'] ) {
143
+ self::schedule_check();
144
+
145
+ return self::$_response = array();
146
+ }
147
+
148
+ if ( $data['requested'] + $data['ttl'] < ITSEC_Core::get_current_time_gmt() ) {
149
+ self::schedule_check();
150
+ $events = ITSEC_Core::get_scheduler()->get_single_events();
151
+
152
+ foreach ( $events as $event ) {
153
+ if ( self::EVENT === $event['id'] && $event['fire_at'] + HOUR_IN_SECONDS > ITSEC_Core::get_current_time_gmt() ) {
154
+ return self::$_response = $data['response'];
155
+ }
156
+ }
157
+
158
+ return self::$_response = array();
159
+ }
160
+
161
+ return self::$_response = $data['response'];
162
+ }
163
+
164
+ private static function schedule_check() {
165
+ $s = ITSEC_Core::get_scheduler();
166
+
167
+ if ( ! $s->is_single_scheduled( self::EVENT, null ) ) {
168
+ $s->schedule_once( ITSEC_Core::get_current_time_gmt() + 60, self::EVENT );
169
+ }
170
+ }
171
+ }
core/lib/class-itsec-mail.php CHANGED
@@ -169,6 +169,17 @@ final class ITSEC_Mail {
169
  return $module;
170
  }
171
 
 
 
 
 
 
 
 
 
 
 
 
172
  public function add_section_heading( $content, $icon_type = false ) {
173
  $this->add_html( $this->get_section_heading( $content, $icon_type ) );
174
  }
@@ -236,6 +247,23 @@ final class ITSEC_Mail {
236
  return $module;
237
  }
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  public function add_lockouts_table( $lockouts ) {
240
  $entry = $this->get_template( 'lockouts-entry.html' );
241
  $entries = '';
169
  return $module;
170
  }
171
 
172
+ public function add_small_code( $content ) {
173
+ $this->add_html( $this->get_small_code( $content ) );
174
+ }
175
+
176
+ public function get_small_code( $content ) {
177
+ $module = $this->get_template( 'small-code.html' );
178
+ $module = $this->replace( $module, 'content', $content );
179
+
180
+ return $module;
181
+ }
182
+
183
  public function add_section_heading( $content, $icon_type = false ) {
184
  $this->add_html( $this->get_section_heading( $content, $icon_type ) );
185
  }
247
  return $module;
248
  }
249
 
250
+ public function add_large_button( $link_text, $href, $style = 'default' ) {
251
+ $this->add_html( $this->get_large_button( $link_text, $href, $style ) );
252
+ }
253
+
254
+ public function get_large_button( $link_text, $href, $style = 'default' ) {
255
+
256
+ $module = $this->get_template( 'large-button.html' );
257
+ $module = $this->replace_all( $module, array(
258
+ 'href' => $href,
259
+ 'link_text' => $link_text,
260
+ 'bk_color' => 'blue' === $style ? '#0085E0' : '#FFCD08',
261
+ 'txt_color' => 'blue' === $style ? '#FFFFFF' : '#2E280E',
262
+ ) );
263
+
264
+ return $module;
265
+ }
266
+
267
  public function add_lockouts_table( $lockouts ) {
268
  $entry = $this->get_template( 'lockouts-entry.html' );
269
  $entries = '';
core/lib/form.php CHANGED
@@ -352,6 +352,16 @@ final class ITSEC_Form {
352
  $this->add_custom_input( $var, $options );
353
  }
354
 
 
 
 
 
 
 
 
 
 
 
355
  public function add_textarea( $var, $options = array() ) {
356
  if ( ! is_array( $options ) ) {
357
  $options = array( 'value' => $options );
352
  $this->add_custom_input( $var, $options );
353
  }
354
 
355
+ public function add_html5_input( $var, $type, $options = array() ) {
356
+ if ( ! is_array( $options ) ) {
357
+ $options = array( 'value' => $options );
358
+ }
359
+
360
+ $options['type'] = $type;
361
+
362
+ $this->add_custom_input( $var, $options );
363
+ }
364
+
365
  public function add_textarea( $var, $options = array() ) {
366
  if ( ! is_array( $options ) ) {
367
  $options = array( 'value' => $options );
core/lib/log-util.php CHANGED
@@ -59,7 +59,7 @@ final class ITSEC_Log_Util {
59
 
60
 
61
  $get_count = false;
62
- $min_timestamp = false;
63
 
64
  if ( isset( $filters['__get_count'] ) ) {
65
  if ( $filters['__get_count'] ) {
@@ -74,6 +74,12 @@ final class ITSEC_Log_Util {
74
  unset( $filters['__min_timestamp'] );
75
  }
76
 
 
 
 
 
 
 
77
  $limit = max( 0, min( 100, intval( $limit ) ) );
78
  $page = max( 1, intval( $page ) );
79
 
@@ -171,10 +177,15 @@ final class ITSEC_Log_Util {
171
  }
172
 
173
  if ( false !== $min_timestamp ) {
174
- $where_entries[] = 'init_timestamp>%s';
175
  $prepare_args[] = date( 'Y-m-d H:i:s', $min_timestamp );
176
  }
177
 
 
 
 
 
 
178
  $query .= ' WHERE ' . implode( ' AND ', $where_entries );
179
 
180
 
59
 
60
 
61
  $get_count = false;
62
+ $min_timestamp = $max_timestamp = false;
63
 
64
  if ( isset( $filters['__get_count'] ) ) {
65
  if ( $filters['__get_count'] ) {
74
  unset( $filters['__min_timestamp'] );
75
  }
76
 
77
+ if ( isset( $filters['__max_timestamp'] ) ) {
78
+ $max_timestamp = $filters['__max_timestamp'];
79
+ unset( $filters['__max_timestamp'] );
80
+ }
81
+
82
+
83
  $limit = max( 0, min( 100, intval( $limit ) ) );
84
  $page = max( 1, intval( $page ) );
85
 
177
  }
178
 
179
  if ( false !== $min_timestamp ) {
180
+ $where_entries[] = 'timestamp>%s';
181
  $prepare_args[] = date( 'Y-m-d H:i:s', $min_timestamp );
182
  }
183
 
184
+ if ( false !== $max_timestamp ) {
185
+ $where_entries[] = 'timestamp<%s';
186
+ $prepare_args[] = date( 'Y-m-d H:i:s', $max_timestamp );
187
+ }
188
+
189
  $query .= ' WHERE ' . implode( ' AND ', $where_entries );
190
 
191
 
core/lib/login-interstitial/abstract-itsec-login-interstitial.php ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Login_Interstitial
5
+ */
6
+ abstract class ITSEC_Login_Interstitial {
7
+
8
+ /**
9
+ * Should this interstitial be shown to the given user.
10
+ *
11
+ * @param WP_User $user
12
+ * @param bool $is_requested
13
+ *
14
+ * @return bool
15
+ */
16
+ public function show_to_user( WP_User $user, $is_requested ) {
17
+ return true;
18
+ }
19
+
20
+ /**
21
+ * Only show this interstitial if the user logged-in via wp-login.php.
22
+ *
23
+ * @param WP_User $user
24
+ *
25
+ * @return bool
26
+ */
27
+ public function show_on_wp_login_only( WP_User $user ) {
28
+ return false;
29
+ }
30
+
31
+ /**
32
+ * Render the interstitial.
33
+ *
34
+ * @param ITSEC_Login_Interstitial_Session $session
35
+ * @param array $args
36
+ *
37
+ * @return string
38
+ */
39
+ abstract public function render( ITSEC_Login_Interstitial_Session $session, array $args );
40
+
41
+ /**
42
+ * Must this interstitial be completed by the given user.
43
+ *
44
+ * @param ITSEC_Login_Interstitial_Session $session
45
+ *
46
+ * @return bool
47
+ */
48
+ public function is_completion_forced( ITSEC_Login_Interstitial_Session $session ) {
49
+ return true;
50
+ }
51
+
52
+ /**
53
+ * Is there a submit handler.
54
+ *
55
+ * @return bool
56
+ */
57
+ public function has_submit() {
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Handle submitting the interstitial.
63
+ *
64
+ * @param ITSEC_Login_Interstitial_Session $session
65
+ * @param array $data
66
+ */
67
+ public function submit( ITSEC_Login_Interstitial_Session $session, array $data ) { }
68
+
69
+ /**
70
+ * Does the interstitial have async GET actions.
71
+ *
72
+ * @return bool
73
+ */
74
+ public function has_async_action() {
75
+ return false;
76
+ }
77
+
78
+ /**
79
+ * Handle an async action.
80
+ *
81
+ * @param ITSEC_Login_Interstitial_Session $session
82
+ * @param string $action
83
+ * @param array $args
84
+ *
85
+ * @return true|array|WP_Error|void
86
+ * True if success.
87
+ * Array if success with output customizations.
88
+ * WP_Error if error.
89
+ * Void/null if action not processed.
90
+ * Or display custom HTML and die.
91
+ */
92
+ public function handle_async_action( ITSEC_Login_Interstitial_Session $session, $action, array $args ) { }
93
+
94
+ /**
95
+ * Does the interstitial have ajax handlers.
96
+ *
97
+ * @return bool
98
+ */
99
+ public function has_ajax_handlers() {
100
+ return false;
101
+ }
102
+
103
+ /**
104
+ * Handle an ajax request.
105
+ *
106
+ * @param ITSEC_Login_Interstitial_Session $session
107
+ * @param array $data
108
+ */
109
+ public function handle_ajax( ITSEC_Login_Interstitial_Session $session, array $data ) { }
110
+
111
+ /**
112
+ * Get an info message to display above the interstitial form.
113
+ *
114
+ * @param ITSEC_Login_Interstitial_Session $session
115
+ *
116
+ * @return string
117
+ */
118
+ public function get_info_message( ITSEC_Login_Interstitial_Session $session ) {
119
+ return '';
120
+ }
121
+
122
+ /**
123
+ * Execute code after the interstitial has been submitted.
124
+ *
125
+ * @param ITSEC_Login_Interstitial_Session $session
126
+ * @param array $data
127
+ */
128
+ public function after_submit( ITSEC_Login_Interstitial_Session $session, array $data ) { }
129
+
130
+ /**
131
+ * Get the priority. A higher priority number is displayed later.
132
+ *
133
+ * @return int
134
+ */
135
+ public function get_priority() {
136
+ return 5;
137
+ }
138
+ }
core/lib/login-interstitial/class-itsec-login-interstitial-config-driven.php ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Login_Interstitial_Config_Driven
5
+ */
6
+ class ITSEC_Login_Interstitial_Config_Driven extends ITSEC_Login_Interstitial {
7
+
8
+ /** @var array */
9
+ private $config;
10
+
11
+ /**
12
+ * ITSEC_Login_Interstitial_Config_Driven constructor.
13
+ *
14
+ * @param array $config
15
+ *
16
+ * @throws InvalidArgumentException
17
+ */
18
+ public function __construct( array $config ) {
19
+ $this->config = wp_parse_args( $config, array(
20
+ 'force_completion' => true, // Will logout the user's session before displaying the interstitial.
21
+ 'show_to_user' => true, // Boolean or callable.
22
+ 'wp_login_only' => false, // Only show the interstitial if the login form is submitted from wp-login.php,
23
+ 'submit' => false, // Callable called with user when submitting the form.
24
+ 'async_action' => false, // Callable called when a user clicks a link to perform an interstitial action.
25
+ 'info_message' => false,
26
+ 'after_submit' => false,
27
+ 'ajax_handler' => false,
28
+ 'priority' => 5,
29
+ ) );
30
+
31
+ if ( ! is_bool( $this->config['show_to_user'] ) && ! is_callable( $this->config['show_to_user'] ) ) {
32
+ throw new InvalidArgumentException( 'Show to user is required.' );
33
+ }
34
+
35
+ if ( ! is_bool( $this->config['force_completion'] ) && ! is_callable( $this->config['force_completion'] ) ) {
36
+ throw new InvalidArgumentException( 'Force completion is required.' );
37
+ }
38
+ }
39
+
40
+ /**
41
+ * @inheritDoc
42
+ */
43
+ public function render( ITSEC_Login_Interstitial_Session $session, array $args ) {
44
+ call_user_func( $this->config['render'], $session->get_user(), array_merge( compact( 'session' ), $args ) );
45
+ }
46
+
47
+ /**
48
+ * @inheritDoc
49
+ */
50
+ public function show_to_user( WP_User $user, $is_requested ) {
51
+ return $this->result( $this->config['show_to_user'], array( $user, $is_requested ) );
52
+ }
53
+
54
+ /**
55
+ * @inheritDoc
56
+ */
57
+ public function show_on_wp_login_only( WP_User $user ) {
58
+ return $this->result( $this->config['wp_login_only'], array( $user ) );
59
+ }
60
+
61
+ /**
62
+ * @inheritDoc
63
+ */
64
+ public function is_completion_forced( ITSEC_Login_Interstitial_Session $session ) {
65
+ return $this->result( $this->config['force_completion'], $session->get_user() );
66
+ }
67
+
68
+ /**
69
+ * @inheritdoc
70
+ */
71
+ public function has_submit() {
72
+ return (bool) $this->config['submit'];
73
+ }
74
+
75
+ /**
76
+ * @inheritDoc
77
+ */
78
+ public function submit( ITSEC_Login_Interstitial_Session $session, array $data ) {
79
+ return call_user_func( $this->config['submit'], $session->get_user(), $data );
80
+ }
81
+
82
+ /**
83
+ * @inheritDoc
84
+ */
85
+ public function has_async_action() {
86
+ return (bool) $this->config['async_action'];
87
+ }
88
+
89
+ /**
90
+ * @inheritDoc
91
+ */
92
+ public function handle_async_action( ITSEC_Login_Interstitial_Session $session, $action, array $args ) {
93
+ return call_user_func( $this->config['async_action'], $session, $action, $args );
94
+ }
95
+
96
+ /**
97
+ * @inheritDoc
98
+ */
99
+ public function has_ajax_handlers() {
100
+ return (bool) $this->config['ajax_handler'];
101
+ }
102
+
103
+ /**
104
+ * @inheritDoc
105
+ */
106
+ public function handle_ajax( ITSEC_Login_Interstitial_Session $session, array $data ) {
107
+ call_user_func( $this->config['ajax_handler'], $session->get_user(), $data );
108
+ }
109
+
110
+ /**
111
+ * @inheritDoc
112
+ */
113
+ public function get_info_message( ITSEC_Login_Interstitial_Session $session ) {
114
+ return $this->result( $this->config['info_message'], array( $session->get_user() ) );
115
+ }
116
+
117
+ /**
118
+ * @inheritDoc
119
+ */
120
+ public function after_submit( ITSEC_Login_Interstitial_Session $session, array $data ) {
121
+ if ( is_callable( $this->config['after_submit'] ) ) {
122
+ call_user_func( $this->config['after_submit'], $session->get_user(), $data );
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @inheritDoc
128
+ */
129
+ public function get_priority() {
130
+ return $this->config['priority'];
131
+ }
132
+
133
+ /**
134
+ * Try and get a value from the provider.
135
+ *
136
+ * If it is a function, will call the function with the provided args.
137
+ *
138
+ * @param bool|callable $provider
139
+ * @param array $args
140
+ *
141
+ * @return bool|mixed
142
+ */
143
+ private function result( $provider, $args = array() ) {
144
+ if ( is_bool( $provider ) ) {
145
+ return $provider;
146
+ }
147
+
148
+ if ( is_callable( $provider, true ) ) {
149
+ return call_user_func_array( $provider, $args );
150
+ }
151
+
152
+ return $provider;
153
+ }
154
+ }
core/lib/login-interstitial/class-itsec-login-interstitial-session.php ADDED
@@ -0,0 +1,505 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_Login_Interstitial_Session {
4
+
5
+ const META_KEY = '_itsec_login_interstitial_state';
6
+
7
+ /** @var int */
8
+ private $id;
9
+
10
+ /** @var WP_User */
11
+ private $user;
12
+
13
+ /** @var array */
14
+ private $data;
15
+
16
+ /**
17
+ * ITSEC_Lib_Login_Interstitial_State constructor.
18
+ *
19
+ * @param WP_User $user
20
+ * @param int $id
21
+ * @param array $data
22
+ */
23
+ public function __construct( WP_User $user, $id, $data ) {
24
+ $this->user = $user;
25
+ $this->id = $id;
26
+ $this->data = $data;
27
+ }
28
+
29
+ /**
30
+ * Set the interstitial that is currently being processed.
31
+ *
32
+ * @param string $action
33
+ *
34
+ * @return $this
35
+ */
36
+ public function set_current_interstitial( $action ) {
37
+ $this->data['current'] = $action;
38
+ $this->data['state'] = array();
39
+
40
+ return $this;
41
+ }
42
+
43
+ /**
44
+ * Get the current interstitial being processed.
45
+ *
46
+ * @return string
47
+ */
48
+ public function get_current_interstitial() {
49
+ return $this->data['current'];
50
+ }
51
+
52
+ /**
53
+ * Mark that this session completed an interstitial.
54
+ *
55
+ * @param string $action
56
+ *
57
+ * @return $this
58
+ */
59
+ public function add_completed_interstitial( $action ) {
60
+ $this->data['completed'][] = $action;
61
+
62
+ return $this;
63
+ }
64
+
65
+ /**
66
+ * Get the completed interstitials.
67
+ *
68
+ * @return string[]
69
+ */
70
+ public function get_completed_interstitials() {
71
+ return $this->data['completed'];
72
+ }
73
+
74
+ /**
75
+ * Add an interstitial to display after the user finishes all required interstitials.
76
+ *
77
+ * @param string $action
78
+ *
79
+ * @return $this
80
+ */
81
+ public function add_show_after( $action ) {
82
+ $this->data['show_after'][] = $action;
83
+
84
+ return $this;
85
+ }
86
+
87
+ /**
88
+ * Get the interstitials to display after the user finishes all required interstitials.
89
+ *
90
+ * @return string[]
91
+ */
92
+ public function get_show_after() {
93
+ return $this->data['show_after'];
94
+ }
95
+
96
+ /**
97
+ * Is remember me enabled.
98
+ *
99
+ * @return bool
100
+ */
101
+ public function is_remember_me() {
102
+ return ! empty( $this->data['remember_me'] );
103
+ }
104
+
105
+ /**
106
+ * Set the remember me value.
107
+ *
108
+ * @param bool $remember
109
+ *
110
+ * @return $this
111
+ */
112
+ public function set_remember_me( $remember = true ) {
113
+ $this->data['remember_me'] = $remember;
114
+
115
+ return $this;
116
+ }
117
+
118
+ /**
119
+ * Get the redirect URI.
120
+ *
121
+ * @return string
122
+ */
123
+ public function get_redirect_to() {
124
+ return empty( $this->data['redirect_to'] ) ? '' : $this->data['redirect_to'];
125
+ }
126
+
127
+ /**
128
+ * Set the redirect URI.
129
+ *
130
+ * @param string $redirect
131
+ *
132
+ * @return $this
133
+ */
134
+ public function set_redirect_to( $redirect ) {
135
+ $this->data['redirect_to'] = $redirect;
136
+
137
+ return $this;
138
+ }
139
+
140
+ /**
141
+ * Is this an interim login.
142
+ *
143
+ * @return bool
144
+ */
145
+ public function is_interim_login() {
146
+ return ! empty( $this->data['interim_login'] );
147
+ }
148
+
149
+ /**
150
+ * Set whether this is an interim login.
151
+ *
152
+ * @param bool $is_interim
153
+ *
154
+ * @return $this
155
+ */
156
+ public function set_interim_login( $is_interim = true ) {
157
+ $this->data['interim_login'] = $is_interim;
158
+
159
+ return $this;
160
+ }
161
+
162
+ /**
163
+ * Get state for the current interstitial.
164
+ *
165
+ * @return array
166
+ */
167
+ public function get_state() {
168
+ return $this->data['state'];
169
+ }
170
+
171
+ /**
172
+ * Set the public state.
173
+ *
174
+ * This is only around for the duration of this interstitial.
175
+ *
176
+ * @param array $state
177
+ *
178
+ * @return $this
179
+ */
180
+ public function set_state( array $state ) {
181
+ $this->data['state'] = $state;
182
+
183
+ return $this;
184
+ }
185
+
186
+ /**
187
+ * Verify the session.
188
+ *
189
+ * @param int $user_id
190
+ * @param string $signature
191
+ *
192
+ * @return true|WP_Error
193
+ */
194
+ public function verify( $user_id, $signature ) {
195
+ if ( $this->is_expired() ) {
196
+ return new WP_Error( 'itsec-lib-login-interstitial-verify-failed-session-expired', esc_html__( 'Session expired.', 'better-wp-security' ) );
197
+ }
198
+
199
+ $signature_verified = $this->verify_signature( $signature );
200
+
201
+ if ( is_wp_error( $signature_verified ) ) {
202
+ return $signature_verified;
203
+ }
204
+
205
+ if ( true !== $signature_verified ) {
206
+ return new WP_Error( 'itsec-lib-login-interstitial-verify-failed-invalid-signature', esc_html__( 'Invalid signature.', 'better-wp-security' ) );
207
+ }
208
+
209
+ if ( ! $user_id || $this->get_user()->ID !== $user_id ) {
210
+ return new WP_Error( 'itsec-lib-login-interstitial-verify-failed-invalid-user', esc_html__( 'Invalid user.', 'better-wp-security' ) );
211
+ }
212
+
213
+ return true;
214
+ }
215
+
216
+ /**
217
+ * Verify the session for a given payload.
218
+ *
219
+ * @param string $payload
220
+ * @param int $user_id
221
+ * @param string $signature
222
+ *
223
+ * @return true|WP_Error
224
+ */
225
+ public function verify_for_payload( $payload, $user_id, $signature ) {
226
+ if ( $this->is_expired() ) {
227
+ return new WP_Error( 'itsec-lib-login-interstitial-verify-failed-session-expired', esc_html__( 'Session expired.', 'better-wp-security' ) );
228
+ }
229
+
230
+ $signature_verified = $this->verify_signature_for_payload( $payload, $signature );
231
+
232
+ if ( is_wp_error( $signature_verified ) ) {
233
+ return $signature_verified;
234
+ }
235
+
236
+ if ( true !== $signature_verified ) {
237
+ return new WP_Error( 'itsec-lib-login-interstitial-verify-failed-invalid-signature', esc_html__( 'Invalid signature.', 'better-wp-security' ) );
238
+ }
239
+
240
+ if ( ! $user_id || $this->get_user()->ID !== $user_id ) {
241
+ return new WP_Error( 'itsec-lib-login-interstitial-verify-failed-invalid-user', esc_html__( 'Invalid user.', 'better-wp-security' ) );
242
+ }
243
+
244
+ return true;
245
+ }
246
+
247
+ /**
248
+ * Is the session expired.
249
+ *
250
+ * @return bool
251
+ */
252
+ public function is_expired() {
253
+ return $this->data['created_at'] + HOUR_IN_SECONDS < ITSEC_Core::get_current_time_gmt();
254
+ }
255
+
256
+ /**
257
+ * Verify the signature.
258
+ *
259
+ * @param string $actual
260
+ *
261
+ * @return bool|WP_Error
262
+ */
263
+ public function verify_signature( $actual ) {
264
+ $expected = $this->get_signature();
265
+
266
+ if ( is_wp_error( $expected ) ) {
267
+ return $expected;
268
+ }
269
+
270
+ return hash_equals( $expected, $actual );
271
+ }
272
+
273
+ /**
274
+ * Get the signature for the session state.
275
+ *
276
+ * @return string|WP_Error
277
+ */
278
+ public function get_signature() {
279
+ $to_hash = sprintf(
280
+ '%s|%s|%s|%s',
281
+ $this->get_user()->ID,
282
+ $this->get_id(),
283
+ $this->data['created_at'],
284
+ $this->data['uuid']
285
+ );
286
+
287
+ $hash = hash_hmac( 'sha1', $to_hash, wp_salt() );
288
+
289
+ if ( ! $hash ) {
290
+ return new WP_Error( 'itsec-lib-login-interstitial-signature-failed', esc_html__( 'Could not calculate signature.', 'better-wp-security' ) );
291
+ }
292
+
293
+ return $hash;
294
+ }
295
+
296
+ /**
297
+ * Verify the signature for a given async action.
298
+ *
299
+ * @param string $payload
300
+ * @param string $actual
301
+ *
302
+ * @return bool|WP_Error
303
+ */
304
+ public function verify_signature_for_payload( $payload, $actual ) {
305
+ $expected = $this->get_signature_for_payload( $payload );
306
+
307
+ if ( is_wp_error( $expected ) ) {
308
+ return $expected;
309
+ }
310
+
311
+ return hash_equals( $expected, $actual );
312
+ }
313
+
314
+ /**
315
+ * Get the signature for a payload.
316
+ *
317
+ * @param string $payload
318
+ *
319
+ * @return string|WP_Error
320
+ */
321
+ public function get_signature_for_payload( $payload ) {
322
+ $to_hash = sprintf(
323
+ '%s|%s|%s|%s|%s',
324
+ $this->get_user()->ID,
325
+ $this->get_id(),
326
+ $this->data['created_at'],
327
+ $this->data['uuid'],
328
+ $payload
329
+ );
330
+
331
+ $hash = hash_hmac( 'sha1', $to_hash, wp_salt() );
332
+
333
+ if ( ! $hash ) {
334
+ return new WP_Error( 'itsec-lib-login-interstitial-signature-failed', esc_html__( 'Could not calculate signature.', 'better-wp-security' ) );
335
+ }
336
+
337
+ return $hash;
338
+ }
339
+
340
+ /**
341
+ * Was the given interstitial completed.
342
+ *
343
+ * @param string $interstitial
344
+ *
345
+ * @return bool
346
+ */
347
+ public function is_interstitial_completed( $interstitial ) {
348
+ return in_array( $interstitial, $this->get_completed_interstitials(), true );
349
+ }
350
+
351
+ /**
352
+ * Is the given interstitial forced.
353
+ *
354
+ * @param string $interstitial
355
+ *
356
+ * @return bool
357
+ */
358
+ public function is_interstitial_requested( $interstitial ) {
359
+ return in_array( $interstitial, $this->get_show_after(), true );
360
+ }
361
+
362
+ /**
363
+ * Is the current interstitial forced to display.
364
+ *
365
+ * @return bool
366
+ */
367
+ public function is_current_requested() {
368
+ return $this->is_interstitial_requested( $this->get_current_interstitial() );
369
+ }
370
+
371
+ /**
372
+ * Get the session ID.
373
+ *
374
+ * @return int
375
+ */
376
+ public function get_id() {
377
+ return $this->id;
378
+ }
379
+
380
+ /**
381
+ * Get the session's user.
382
+ *
383
+ * @return WP_User
384
+ */
385
+ public function get_user() {
386
+ return $this->user;
387
+ }
388
+
389
+ /**
390
+ * Save the session.
391
+ *
392
+ * @return bool
393
+ */
394
+ public function save() {
395
+ return update_metadata_by_mid( 'user', $this->get_id(), $this->data, self::META_KEY );
396
+ }
397
+
398
+ /**
399
+ * Delete the session state.
400
+ *
401
+ * @return bool
402
+ */
403
+ public function delete() {
404
+ $deleted = delete_metadata_by_mid( 'user', $this->get_id() );
405
+
406
+ foreach ( get_user_meta( $this->get_user()->ID, self::META_KEY ) as $entry ) {
407
+ if ( ! isset( $entry['created_at'] ) || $entry['created_at'] + HOUR_IN_SECONDS < ITSEC_Core::get_current_time_gmt() ) {
408
+ delete_user_meta( $this->get_user()->ID, self::META_KEY, $entry );
409
+ }
410
+ }
411
+
412
+ return $deleted;
413
+ }
414
+
415
+ /**
416
+ * Create a new state session.
417
+ *
418
+ * @param WP_User $user The user to create the session for.
419
+ * @param string $current The current interstitial.
420
+ *
421
+ * @return ITSEC_Login_Interstitial_Session|WP_Error
422
+ */
423
+ public static function create( WP_User $user, $current = '' ) {
424
+
425
+ $data = array(
426
+ 'uuid' => wp_generate_uuid4(),
427
+ 'current' => $current,
428
+ 'completed' => array(),
429
+ 'created_at' => ITSEC_Core::get_current_time_gmt(),
430
+ 'show_after' => array(),
431
+ 'redirect_to' => '',
432
+ 'remember_me' => false,
433
+ 'interim_login' => false,
434
+ 'state' => array(),
435
+ );
436
+
437
+ if ( ! $mid = add_user_meta( $user->ID, self::META_KEY, $data ) ) {
438
+ return new WP_Error( 'itsec-lib-login-interstitial-save-failed', esc_html__( 'Failed to create interstitial state.', 'better-wp-security' ) );
439
+ }
440
+
441
+ return new self( $user, $mid, $data );
442
+ }
443
+
444
+ /**
445
+ * Get a state session.
446
+ *
447
+ * @param int $id
448
+ *
449
+ * @return ITSEC_Login_Interstitial_Session|WP_Error
450
+ */
451
+ public static function get( $id ) {
452
+
453
+ $row = get_metadata_by_mid( 'user', $id );
454
+
455
+ if (
456
+ ! $row ||
457
+ $row->meta_key !== self::META_KEY ||
458
+ ! self::validate_meta( $row->meta_value ) ||
459
+ ! $user = get_userdata( $row->user_id )
460
+ ) {
461
+ return new WP_Error( 'itsec-lib-login-interstitial-not-found', esc_html__( 'Interstitial state not found.', 'better-wp-security' ) );
462
+ }
463
+
464
+ return new self( $user, $id, $row->meta_value );
465
+ }
466
+
467
+ /**
468
+ * Get all interstitials for a user.
469
+ *
470
+ * @param WP_User $user
471
+ *
472
+ * @return ITSEC_Login_Interstitial_Session[]
473
+ */
474
+ public static function get_all( WP_User $user ) {
475
+
476
+ global $wpdb;
477
+
478
+ $mids = $wpdb->get_col( $wpdb->prepare(
479
+ "SELECT `umeta_id` FROM {$wpdb->usermeta} WHERE `meta_key` = %s AND `user_id` = %d",
480
+ self::META_KEY,
481
+ $user->ID
482
+ ) );
483
+
484
+ $sessions = array();
485
+
486
+ foreach ( $mids as $meta_id ) {
487
+ if ( ! is_wp_error( $session = self::get( $meta_id ) ) ) {
488
+ $sessions[] = $session;
489
+ }
490
+ }
491
+
492
+ return $sessions;
493
+ }
494
+
495
+ /**
496
+ * Validate the meta value is valid.
497
+ *
498
+ * @param mixed $meta_value
499
+ *
500
+ * @return bool
501
+ */
502
+ private static function validate_meta( $meta_value ) {
503
+ return is_array( $meta_value ) && isset( $meta_value['uuid'], $meta_value['created_at'], $meta_value['completed'], $meta_value['current'], $meta_value['show_after'] );
504
+ }
505
+ }
core/lib/login-interstitial/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/lib/login-interstitial/util.js ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ( function( $ ) {
2
+
3
+ var VARS = [
4
+ 'itsec_interstitial_user',
5
+ 'itsec_interstitial_token',
6
+ 'itsec_interstitial_session',
7
+ ];
8
+
9
+ function ITSECLoginInterstitial( $el, options ) {
10
+
11
+ if ( $.isPlainObject( $el ) ) {
12
+ options = $el;
13
+ $el = null;
14
+ }
15
+
16
+ if ( !$el ) {
17
+ $( 'form' ).each( function() {
18
+ var $form = $( this );
19
+
20
+ if ( $form.attr( 'id' ).indexOf( 'itsec-' ) === 0 ) {
21
+ $el = $form;
22
+ return false;
23
+ }
24
+ } );
25
+ }
26
+
27
+ if ( !$el ) {
28
+ throw Error( 'No $el found.' );
29
+ }
30
+
31
+ this.$el = $el;
32
+ this.options = $.extend( {
33
+ checkInterval: 5000,
34
+ onStateChange: $.noop,
35
+ onProgressed : ( function() {
36
+ this.submitToProceed();
37
+ } ).bind( this ),
38
+ }, options || {} );
39
+
40
+ this.current = $el.prop( 'id' ).replace( 'itsec-', '' );
41
+ this.vars = {};
42
+ this.intervalId = null;
43
+ this.currentState = [];
44
+ }
45
+
46
+ /**
47
+ * Initialize the interstitial.
48
+ */
49
+ ITSECLoginInterstitial.prototype.init = function() {
50
+ for ( var i = 0; i < VARS.length; i++ ) {
51
+ this.vars[ VARS[ i ] ] = $( 'input[name="' + VARS[ i ] + '"]', this.$el ).val();
52
+ }
53
+
54
+ this.intervalId = setInterval( this.checkIfProgressed.bind( this ), this.options.checkInterval );
55
+ };
56
+
57
+ /**
58
+ * Make an ajax request.
59
+ *
60
+ * @return {$.promise}
61
+ */
62
+ ITSECLoginInterstitial.prototype.ajax = function( data ) {
63
+ return wp.ajax.post(
64
+ 'itsec-login-interstitial-ajax',
65
+ $.extend( {}, this.vars, data ),
66
+ );
67
+ };
68
+
69
+ /**
70
+ * Fetch the latest interstitial state.
71
+ *
72
+ * @return {$.promise}
73
+ */
74
+ ITSECLoginInterstitial.prototype.fetchState = function() {
75
+ return wp.ajax.post(
76
+ 'itsec-login-interstitial-ajax',
77
+ $.extend( { itsec_interstitial_get_state: true }, this.vars ),
78
+ );
79
+ };
80
+
81
+ ITSECLoginInterstitial.prototype.checkIfProgressed = function() {
82
+ this.fetchState().then( ( function( response ) {
83
+ if ( response.logged_in || response.current !== this.current ) {
84
+ this.options.onProgressed( response );
85
+ } else if ( JSON.stringify( response.state ) !== JSON.stringify( this.currentState ) ) {
86
+ this.options.onStateChange( response.state, this.currentState );
87
+ this.currentState = response.state;
88
+ }
89
+ } ).bind( this ) ).fail( ( function( response ) {
90
+ console.error( response );
91
+ clearInterval( this.intervalId );
92
+ } ).bind( this ) );
93
+ };
94
+
95
+ ITSECLoginInterstitial.prototype.submitToProceed = function() {
96
+
97
+ var $form = $( '<form />' )
98
+ .prop( 'method', 'post' )
99
+ .prop( 'action', this.$el.attr( 'action' ) )
100
+ .css( { display: 'none' } );
101
+
102
+ $form.append(
103
+ $( '<input />' )
104
+ .prop( 'type', 'hidden' )
105
+ .prop( 'name', 'action' )
106
+ .prop( 'value', 'itsec-' + this.current ),
107
+ );
108
+
109
+ for ( var i = 0; i < VARS.length; i++ ) {
110
+ $form.append(
111
+ $( '<input />' )
112
+ .prop( 'type', 'hidden' )
113
+ .prop( 'name', VARS[ i ] )
114
+ .prop( 'value', this.vars[ VARS[ i ] ] ),
115
+ );
116
+ }
117
+
118
+ $form.appendTo( document.body );
119
+ $form.submit();
120
+ };
121
+
122
+ ITSECLoginInterstitial.prototype.setOnProgressed = function( callback ) {
123
+ this.options.onProgressed = callback;
124
+ };
125
+
126
+ ITSECLoginInterstitial.prototype.setOnStateChange = function( callback ) {
127
+ this.options.onStateChange = callback;
128
+ };
129
+
130
+ window.ITSECLoginInterstitial = ITSECLoginInterstitial;
131
+ } )( jQuery );
core/lib/mail-templates/large-button.html ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <tr>
2
+ <td align="center" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
3
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
4
+ <tr>
5
+ <td align="center" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
6
+ <table class="container" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
7
+ <tr>
8
+ <td class="section-padding section-padding-bottom" align="center" valign="top" width="600" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;padding-top: 20px;padding-right: 20px;padding-left: 20px;padding-bottom: 20px;">
9
+ <table width="100%" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
10
+ <tr>
11
+ <td style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
12
+ <table class="module-button" border="0" cellspacing="0" cellpadding="0" align="center" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
13
+ <tr>
14
+ <td class="border-radius" align="center" bgcolor="{{ $bk_color }}" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;-webkit-border-radius: 5px;-moz-border-radius: 5px;border-radius: 5px;">
15
+ <a class="border-radius" href="{{ $href }}" target="_blank" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: {{ $txt_color }};font-family: Helvetica;font-size: 24px;line-height: 100%;text-align: center;text-decoration: none;background-color: {{ $bk_color }};border: 1px solid {{ $bk_color }};display: inline-block;font-weight: normal;padding-top: 20px;padding-right: 40px;padding-bottom: 20px;padding-left: 40px;-webkit-border-radius: 5px;-moz-border-radius: 5px;border-radius: 5px;">{{ $link_text }}</a>
16
+ </td>
17
+ </tr>
18
+ </table>
19
+ </td>
20
+ </tr>
21
+ </table>
22
+ </td>
23
+ </tr>
24
+ </table>
25
+ </td>
26
+ </tr>
27
+ </table>
28
+ </td>
29
+ </tr>
core/lib/mail-templates/small-code.html ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <tr>
2
+ <td class="details-box-container" align="center" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;padding-top: 20px;padding-bottom: 20px;">
3
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
4
+ <tr>
5
+ <td align="center" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
6
+ <table class="details-box container" border="0" cellpadding="0" cellspacing="0" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;background-color: #DDDDDD;border: 1px solid #DDDDDD;">
7
+ <tr>
8
+ <td class="section-padding" align="center" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;padding-top: 10px;padding-right: 20px;padding-left: 20px;">
9
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
10
+ <tr>
11
+ <td class="container-cell" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #545454;font-family: monospace;font-size: 16px;font-weight: bolder;line-height: 150%;text-align: center;padding-bottom: 10px;">
12
+ {{ $content }}
13
+ </td>
14
+ </tr>
15
+ </table>
16
+ </td>
17
+ </tr>
18
+ </table>
19
+ </td>
20
+ </tr>
21
+ </table>
22
+ </td>
23
+ </tr>
core/lib/schema.php CHANGED
@@ -113,7 +113,8 @@ CREATE TABLE {$wpdb->base_prefix}itsec_fingerprints (
113
  PRIMARY KEY (fingerprint_id),
114
  UNIQUE KEY fingerprint_user__hash (fingerprint_user,fingerprint_hash),
115
  UNIQUE KEY fingerprint_uuid (fingerprint_uuid)
116
- ) $charset_collate;";
 
117
 
118
  require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
119
  dbDelta( $tables );
113
  PRIMARY KEY (fingerprint_id),
114
  UNIQUE KEY fingerprint_user__hash (fingerprint_user,fingerprint_hash),
115
  UNIQUE KEY fingerprint_uuid (fingerprint_uuid)
116
+ ) $charset_collate;
117
+ ";
118
 
119
  require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
120
  dbDelta( $tables );
core/lib/validator.php CHANGED
@@ -185,6 +185,12 @@ abstract class ITSEC_Validator {
185
  } else {
186
  $error = sprintf( __( 'The %1$s value must be a positive integer.', 'better-wp-security' ), $name );
187
  }
 
 
 
 
 
 
188
  } else if ( 'email' === $type ) {
189
  $this->settings[$var] = sanitize_text_field( $this->settings[$var] );
190
 
@@ -320,7 +326,7 @@ abstract class ITSEC_Validator {
320
  }
321
  } elseif ( 'canonical-roles' === $type ) {
322
  $roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
323
-
324
  if ( is_array( $this->settings[$var] ) ) {
325
  $invalid_entries = array();
326
 
185
  } else {
186
  $error = sprintf( __( 'The %1$s value must be a positive integer.', 'better-wp-security' ), $name );
187
  }
188
+ } else if ( 'number' === $type ) {
189
+ if ( is_numeric($this->settings[ $var ] ) ) {
190
+ $this->settings[ $var ] = (float) $this->settings[ $var ];
191
+ } else {
192
+ $error = sprintf( __( 'The %1$s value must be a number.', 'better-wp-security' ), $name );
193
+ }
194
  } else if ( 'email' === $type ) {
195
  $this->settings[$var] = sanitize_text_field( $this->settings[$var] );
196
 
326
  }
327
  } elseif ( 'canonical-roles' === $type ) {
328
  $roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
329
+
330
  if ( is_array( $this->settings[$var] ) ) {
331
  $invalid_entries = array();
332
 
core/lockout.php CHANGED
@@ -76,7 +76,9 @@ final class ITSEC_Lockout {
76
  add_filter( 'authenticate', array( $this, 'check_authenticate_lockout' ), 30 );
77
 
78
  // Updated temp whitelist to ensure that admin users are automatically added.
79
- add_action( 'init', array( $this, 'update_temp_whitelist' ), 0 );
 
 
80
 
81
  //Register all plugin modules
82
  add_action( 'plugins_loaded', array( $this, 'register_modules' ) );
@@ -127,6 +129,10 @@ final class ITSEC_Lockout {
127
  */
128
  public function check_for_host_lockouts() {
129
 
 
 
 
 
130
  $host = ITSEC_Lib::get_ip();
131
 
132
  if ( $this->is_host_locked_out( $host ) || ITSEC_Lib::is_ip_blacklisted() ) {
@@ -564,8 +570,8 @@ final class ITSEC_Lockout {
564
  'current' => true,
565
  ) );
566
 
567
- $where = $limit = '';
568
- $wheres = array();
569
 
570
  switch ( $type ) {
571
 
@@ -591,6 +597,16 @@ final class ITSEC_Lockout {
591
  $wheres[] = "`lockout_start_gmt` > '{$after}'";
592
  }
593
 
 
 
 
 
 
 
 
 
 
 
594
  if ( $wheres ) {
595
  $where = ' WHERE ' . implode( ' AND ', $wheres );
596
  }
@@ -599,15 +615,40 @@ final class ITSEC_Lockout {
599
  $limit = ' LIMIT ' . absint( $args['limit'] );
600
  }
601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  if ( isset( $args['return'] ) && 'count' === $args['return'] ) {
603
  $select = 'SELECT COUNT(1) as COUNT';
604
  $is_count = true;
605
  } else {
606
- $select = 'SELECT *';
607
  $is_count = false;
608
  }
609
 
610
- $results = $wpdb->get_results( "{$select} FROM `" . $wpdb->base_prefix . "itsec_lockouts`" . $where . $limit . ';', ARRAY_A );
 
 
 
 
 
 
611
 
612
  if ( $is_count && $results ) {
613
  return $results[0]['COUNT'];
@@ -712,6 +753,11 @@ final class ITSEC_Lockout {
712
  * @return bool
713
  */
714
  public function is_visitor_temp_whitelisted() {
 
 
 
 
 
715
  $whitelist = $this->get_temp_whitelist();
716
  $ip = ITSEC_Lib::get_ip();
717
 
@@ -988,6 +1034,37 @@ final class ITSEC_Lockout {
988
  $this->lockout_modules = apply_filters( 'itsec_lockout_modules', $this->lockout_modules );
989
  }
990
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
991
  /**
992
  * Process clearing lockouts on view log page
993
  *
@@ -1017,7 +1094,7 @@ final class ITSEC_Lockout {
1017
  )
1018
  );
1019
 
1020
- return $success === false ? false : true;
1021
 
1022
  }
1023
  }
76
  add_filter( 'authenticate', array( $this, 'check_authenticate_lockout' ), 30 );
77
 
78
  // Updated temp whitelist to ensure that admin users are automatically added.
79
+ if ( ! defined( 'ITSEC_DISABLE_TEMP_WHITELIST' ) || ! ITSEC_DISABLE_TEMP_WHITELIST ) {
80
+ add_action( 'init', array( $this, 'update_temp_whitelist' ), 0 );
81
+ }
82
 
83
  //Register all plugin modules
84
  add_action( 'plugins_loaded', array( $this, 'register_modules' ) );
129
  */
130
  public function check_for_host_lockouts() {
131
 
132
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
133
+ return;
134
+ }
135
+
136
  $host = ITSEC_Lib::get_ip();
137
 
138
  if ( $this->is_host_locked_out( $host ) || ITSEC_Lib::is_ip_blacklisted() ) {
570
  'current' => true,
571
  ) );
572
 
573
+ $where = $limit = $join = $order = '';
574
+ $wheres = $prepare = array();
575
 
576
  switch ( $type ) {
577
 
597
  $wheres[] = "`lockout_start_gmt` > '{$after}'";
598
  }
599
 
600
+ if ( ! empty( $args['search'] ) ) {
601
+ $search = '%' . $wpdb->esc_like( $args['search'] ) . '%';
602
+ $prepare = array_merge( $prepare, array_pad( array(), 6, $search ) );
603
+
604
+ $u = $wpdb->users;
605
+ $l = $wpdb->base_prefix . 'itsec_lockouts';
606
+ $join .= " LEFT JOIN `{$u}` ON ( `{$l}`.`lockout_user` = `{$u}`.`ID` )";
607
+ $wheres[] = "( `{$u}`.`user_login` LIKE %s OR `{$u}`.`user_email` LIKE %s OR `{$u}`.`user_nicename` LIKE %s OR `{$u}`.`display_name` LIKE %s OR `{$l}`.`lockout_username` LIKE %s or `{$l}`.`lockout_host` LIKE %s)";
608
+ }
609
+
610
  if ( $wheres ) {
611
  $where = ' WHERE ' . implode( ' AND ', $wheres );
612
  }
615
  $limit = ' LIMIT ' . absint( $args['limit'] );
616
  }
617
 
618
+ if ( ! empty( $args['orderby'] ) ) {
619
+ $columns = array( 'lockout_id', 'lockout_start', 'lockout_expire' );
620
+ $direction = isset( $args['order'] ) ? $args['order'] : 'DESC';
621
+
622
+ if ( ! in_array( $args['orderby'], $columns, true ) ) {
623
+ _doing_it_wrong( __METHOD__, "Orderby must be one of 'lockout_id', 'lockout_start', or 'lockout_expire'.", 4109 );
624
+
625
+ return array();
626
+ }
627
+
628
+ if ( ! in_array( $direction, array( 'ASC', 'DESC' ), true ) ) {
629
+ _doing_it_wrong( __METHOD__, "Order must be one of 'ASC' or 'DESC'.", 4109 );
630
+
631
+ return array();
632
+ }
633
+
634
+ $order = " ORDER BY `{$args['orderby']}` $direction";
635
+ }
636
+
637
  if ( isset( $args['return'] ) && 'count' === $args['return'] ) {
638
  $select = 'SELECT COUNT(1) as COUNT';
639
  $is_count = true;
640
  } else {
641
+ $select = "SELECT `{$wpdb->base_prefix}itsec_lockouts`.*";
642
  $is_count = false;
643
  }
644
 
645
+ $sql = "{$select} FROM `{$wpdb->base_prefix}itsec_lockouts` {$join}{$where}{$order}{$limit};";
646
+
647
+ if ( $prepare ) {
648
+ $sql = $wpdb->prepare( $sql, $prepare );
649
+ }
650
+
651
+ $results = $wpdb->get_results( $sql, ARRAY_A );
652
 
653
  if ( $is_count && $results ) {
654
  return $results[0]['COUNT'];
753
  * @return bool
754
  */
755
  public function is_visitor_temp_whitelisted() {
756
+
757
+ if ( defined( 'ITSEC_DISABLE_TEMP_WHITELIST' ) && ITSEC_DISABLE_TEMP_WHITELIST ) {
758
+ return false;
759
+ }
760
+
761
  $whitelist = $this->get_temp_whitelist();
762
  $ip = ITSEC_Lib::get_ip();
763
 
1034
  $this->lockout_modules = apply_filters( 'itsec_lockout_modules', $this->lockout_modules );
1035
  }
1036
 
1037
+ /**
1038
+ * Get all the registered lockout modules.
1039
+ *
1040
+ * @return array
1041
+ */
1042
+ public function get_lockout_modules() {
1043
+ return $this->lockout_modules;
1044
+ }
1045
+
1046
+ /**
1047
+ * Get lockout details.
1048
+ *
1049
+ * @param int $id
1050
+ *
1051
+ * @return array|false
1052
+ */
1053
+ public function get_lockout( $id ) {
1054
+ global $wpdb;
1055
+
1056
+ $results = $wpdb->get_results( $wpdb->prepare(
1057
+ "SELECT * FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_id` = %d",
1058
+ $id
1059
+ ), ARRAY_A );
1060
+
1061
+ if ( ! is_array( $results ) || ! isset( $results[0] ) ) {
1062
+ return false;
1063
+ }
1064
+
1065
+ return $results[0];
1066
+ }
1067
+
1068
  /**
1069
  * Process clearing lockouts on view log page
1070
  *
1094
  )
1095
  );
1096
 
1097
+ return (bool) $success;
1098
 
1099
  }
1100
  }
core/modules.php CHANGED
@@ -468,7 +468,9 @@ final class ITSEC_Modules {
468
  $was_active = $self->_active_modules[ $module_id ];
469
  }
470
 
471
- self::load_module_file( 'activate.php', $module_id );
 
 
472
 
473
  $self->_active_modules[ $module_id ] = true;
474
  self::set_active_modules( $self->_active_modules );
@@ -556,7 +558,7 @@ final class ITSEC_Modules {
556
  * module slugs, ':all' to load the files from all modules, or ':active' to load the
557
  * files from active modules.
558
  *
559
- * @return bool True if a module matching the $modules parameter is found, false otherwise.
560
  */
561
  public static function load_module_file( $file, $modules = ':all' ) {
562
  $self = self::get_instance();
@@ -580,7 +582,11 @@ final class ITSEC_Modules {
580
 
581
  foreach ( $modules as $module ) {
582
  if ( ! empty( $self->_module_paths[$module] ) && file_exists( "{$self->_module_paths[$module]}/{$file}" ) ) {
583
- include_once( "{$self->_module_paths[$module]}/{$file}" );
 
 
 
 
584
  }
585
  }
586
 
468
  $was_active = $self->_active_modules[ $module_id ];
469
  }
470
 
471
+ if ( is_wp_error( $error = self::load_module_file( 'activate.php', $module_id ) ) ) {
472
+ return $error;
473
+ }
474
 
475
  $self->_active_modules[ $module_id ] = true;
476
  self::set_active_modules( $self->_active_modules );
558
  * module slugs, ':all' to load the files from all modules, or ':active' to load the
559
  * files from active modules.
560
  *
561
+ * @return bool|WP_Error True if a module matching the $modules parameter is found, false otherwise.
562
  */
563
  public static function load_module_file( $file, $modules = ':all' ) {
564
  $self = self::get_instance();
582
 
583
  foreach ( $modules as $module ) {
584
  if ( ! empty( $self->_module_paths[$module] ) && file_exists( "{$self->_module_paths[$module]}/{$file}" ) ) {
585
+ $returned = include_once( "{$self->_module_paths[$module]}/{$file}" );
586
+
587
+ if ( is_wp_error( $returned ) ) {
588
+ return $returned;
589
+ }
590
  }
591
  }
592
 
core/modules/404-detection/class-itsec-four-oh-four.php CHANGED
@@ -29,14 +29,14 @@ class ITSEC_Four_Oh_Four {
29
 
30
  $uri = explode( '?', $_SERVER['REQUEST_URI'] );
31
 
32
- if ( in_array( '/' . ITSEC_Lib::get_request_path(), $this->settings['white_list'], true ) ) {
33
- return; // white listed page.
34
- }
35
-
36
- ITSEC_Log::add_notice( 'four_oh_four', 'found_404', array( 'SERVER' => $_SERVER ) );
37
-
38
- if ( ! in_array( '.' . pathinfo( $uri[0], PATHINFO_EXTENSION ), $this->settings['types'], true ) ) {
39
  $itsec_lockout->do_lockout( 'four_oh_four' );
 
 
40
  }
41
  }
42
 
29
 
30
  $uri = explode( '?', $_SERVER['REQUEST_URI'] );
31
 
32
+ if (
33
+ ! in_array( '/' . ITSEC_Lib::get_request_path(), $this->settings['white_list'], true ) &&
34
+ ! in_array( '.' . pathinfo( $uri[0], PATHINFO_EXTENSION ), $this->settings['types'], true )
35
+ ) {
36
+ ITSEC_Log::add_notice( 'four_oh_four', 'found_404', array( 'SERVER' => $_SERVER ) );
 
 
37
  $itsec_lockout->do_lockout( 'four_oh_four' );
38
+ } else {
39
+ do_action( 'itsec_four_oh_four_whitelisted', $uri );
40
  }
41
  }
42
 
core/modules/backup/class-itsec-backup.php CHANGED
@@ -65,13 +65,13 @@ class ITSEC_Backup {
65
  /**
66
  * Public function to get lock and call backup.
67
  *
68
- * Attempts to get a lock to prevent concurrant backups and calls the backup function itself.
69
  *
70
  * @since 4.0.0
71
  *
72
  * @param boolean $one_time whether this is a one time backup
73
  *
74
- * @return mixed false on error or nothing
75
  */
76
  public function do_backup( $one_time = false ) {
77
 
@@ -79,21 +79,25 @@ class ITSEC_Backup {
79
  return new WP_Error( 'itsec-backup-do-backup-already-running', __( 'Unable to create a backup at this time since a backup is currently being created. If you wish to create an additional backup, please wait a few minutes before trying again.', 'better-wp-security' ) );
80
  }
81
 
82
-
83
  ITSEC_Lib::set_minimum_memory_limit( '256M' );
84
- $this->execute_backup( $one_time );
85
  ITSEC_Lib::release_lock( 'backup' );
86
 
87
  switch ( $this->settings['method'] ) {
88
-
89
  case 0:
90
- return __( 'Backup complete. The backup was sent to the selected email recipients and was saved locally.', 'better-wp-security' );
 
91
  case 1:
92
- return __( 'Backup complete. The backup was sent to the selected email recipients.', 'better-wp-security' );
 
93
  default:
94
- return __( 'Backup complete. The backup was saved locally.', 'better-wp-security' );
95
-
96
  }
 
 
 
 
97
  }
98
 
99
  /**
@@ -105,13 +109,11 @@ class ITSEC_Backup {
105
  *
106
  * @param bool $one_time whether this is a one-time backup
107
  *
108
- * @return void
109
  */
110
  private function execute_backup( $one_time = false ) {
111
  global $wpdb;
112
 
113
-
114
-
115
  require_once( ITSEC_Core::get_core_dir() . 'lib/class-itsec-lib-directory.php' );
116
 
117
  $dir = $this->settings['location'];
@@ -226,6 +228,14 @@ class ITSEC_Backup {
226
  $mail_success = null;
227
  }
228
 
 
 
 
 
 
 
 
 
229
  if ( 1 === $this->settings['method'] ) {
230
  @unlink( $file );
231
  } else if ( $this->settings['retain'] > 0 ) {
@@ -248,13 +258,6 @@ class ITSEC_Backup {
248
  }
249
  }
250
 
251
-
252
- $log_data = array(
253
- 'settings' => $this->settings,
254
- 'mail_success' => $mail_success,
255
- 'file' => $backup_file,
256
- );
257
-
258
  if ( 0 === $this->settings['method'] ) {
259
  if ( false === $mail_success ) {
260
  ITSEC_Log::add_warning( 'backup', 'email-failed-file-stored', $log_data );
@@ -270,6 +273,8 @@ class ITSEC_Backup {
270
  } else {
271
  ITSEC_Log::add_notice( 'backup', 'file-stored', $log_data );
272
  }
 
 
273
  }
274
 
275
  private function send_mail( $file ) {
65
  /**
66
  * Public function to get lock and call backup.
67
  *
68
+ * Attempts to get a lock to prevent concurrent backups and calls the backup function itself.
69
  *
70
  * @since 4.0.0
71
  *
72
  * @param boolean $one_time whether this is a one time backup
73
  *
74
+ * @return array|WP_Error false on error or nothing
75
  */
76
  public function do_backup( $one_time = false ) {
77
 
79
  return new WP_Error( 'itsec-backup-do-backup-already-running', __( 'Unable to create a backup at this time since a backup is currently being created. If you wish to create an additional backup, please wait a few minutes before trying again.', 'better-wp-security' ) );
80
  }
81
 
 
82
  ITSEC_Lib::set_minimum_memory_limit( '256M' );
83
+ $result = $this->execute_backup( $one_time );
84
  ITSEC_Lib::release_lock( 'backup' );
85
 
86
  switch ( $this->settings['method'] ) {
 
87
  case 0:
88
+ $message = __( 'Backup complete. The backup was sent to the selected email recipients and was saved locally.', 'better-wp-security' );
89
+ break;
90
  case 1:
91
+ $message = __( 'Backup complete. The backup was sent to the selected email recipients.', 'better-wp-security' );
92
+ break;
93
  default:
94
+ $message = __( 'Backup complete. The backup was saved locally.', 'better-wp-security' );
95
+ break;
96
  }
97
+
98
+ $result['message'] = $message;
99
+
100
+ return $result;
101
  }
102
 
103
  /**
109
  *
110
  * @param bool $one_time whether this is a one-time backup
111
  *
112
+ * @return array|WP_Error
113
  */
114
  private function execute_backup( $one_time = false ) {
115
  global $wpdb;
116
 
 
 
117
  require_once( ITSEC_Core::get_core_dir() . 'lib/class-itsec-lib-directory.php' );
118
 
119
  $dir = $this->settings['location'];
228
  $mail_success = null;
229
  }
230
 
231
+ $log_data = array(
232
+ 'settings' => $this->settings,
233
+ 'mail_success' => $mail_success,
234
+ 'file' => $backup_file,
235
+ 'output_file' => $file,
236
+ 'size' => @filesize( $file ),
237
+ );
238
+
239
  if ( 1 === $this->settings['method'] ) {
240
  @unlink( $file );
241
  } else if ( $this->settings['retain'] > 0 ) {
258
  }
259
  }
260
 
 
 
 
 
 
 
 
261
  if ( 0 === $this->settings['method'] ) {
262
  if ( false === $mail_success ) {
263
  ITSEC_Log::add_warning( 'backup', 'email-failed-file-stored', $log_data );
273
  } else {
274
  ITSEC_Log::add_notice( 'backup', 'file-stored', $log_data );
275
  }
276
+
277
+ return $log_data;
278
  }
279
 
280
  private function send_mail( $file ) {
core/modules/backup/settings-page.php CHANGED
@@ -47,8 +47,8 @@ final class ITSEC_Backup_Settings_Page extends ITSEC_Module_Settings_Page {
47
  foreach ( $errors as $error ) {
48
  $message .= '<div class="error inline"><p><strong>' . $error . '</strong></p></div>';
49
  }
50
- } else if ( is_string( $result ) ) {
51
- $message = '<div class="updated fade inline"><p><strong>' . $result . '</strong></p></div>';
52
  } else {
53
  $message = '<div class="error inline"><p><strong>' . sprintf( __( 'The backup request returned an unexpected response. It returned a response of type <code>%1$s</code>.', 'better-wp-security' ), gettype( $result ) ) . '</strong></p></div>';
54
  }
47
  foreach ( $errors as $error ) {
48
  $message .= '<div class="error inline"><p><strong>' . $error . '</strong></p></div>';
49
  }
50
+ } else if ( is_array( $result ) ) {
51
+ $message = '<div class="updated fade inline"><p><strong>' . $result['message'] . '</strong></p></div>';
52
  } else {
53
  $message = '<div class="error inline"><p><strong>' . sprintf( __( 'The backup request returned an unexpected response. It returned a response of type <code>%1$s</code>.', 'better-wp-security' ), gettype( $result ) ) . '</strong></p></div>';
54
  }
core/modules/ban-users/lists/hackrepair-apache.inc CHANGED
@@ -153,7 +153,6 @@ RewriteCond %{HTTP_USER_AGENT} "PHPCrawl" [NC,OR]
153
  RewriteCond %{HTTP_USER_AGENT} "PleaseCrawl" [NC,OR]
154
  RewriteCond %{HTTP_USER_AGENT} "SBIder" [NC,OR]
155
  RewriteCond %{HTTP_USER_AGENT} "SearchmetricsBot" [NC,OR]
156
- RewriteCond %{HTTP_USER_AGENT} "SeznamBot" [NC,OR]
157
  RewriteCond %{HTTP_USER_AGENT} "Snoopy" [NC,OR]
158
  RewriteCond %{HTTP_USER_AGENT} "Steeler" [NC,OR]
159
  RewriteCond %{HTTP_USER_AGENT} "URI\:\:Fetch" [NC,OR]
153
  RewriteCond %{HTTP_USER_AGENT} "PleaseCrawl" [NC,OR]
154
  RewriteCond %{HTTP_USER_AGENT} "SBIder" [NC,OR]
155
  RewriteCond %{HTTP_USER_AGENT} "SearchmetricsBot" [NC,OR]
 
156
  RewriteCond %{HTTP_USER_AGENT} "Snoopy" [NC,OR]
157
  RewriteCond %{HTTP_USER_AGENT} "Steeler" [NC,OR]
158
  RewriteCond %{HTTP_USER_AGENT} "URI\:\:Fetch" [NC,OR]
core/modules/ban-users/lists/hackrepair-litespeed.inc CHANGED
@@ -153,7 +153,6 @@ RewriteCond %{HTTP_USER_AGENT} "PHPCrawl" [NC,OR]
153
  RewriteCond %{HTTP_USER_AGENT} "PleaseCrawl" [NC,OR]
154
  RewriteCond %{HTTP_USER_AGENT} "SBIder" [NC,OR]
155
  RewriteCond %{HTTP_USER_AGENT} "SearchmetricsBot" [NC,OR]
156
- RewriteCond %{HTTP_USER_AGENT} "SeznamBot" [NC,OR]
157
  RewriteCond %{HTTP_USER_AGENT} "Snoopy" [NC,OR]
158
  RewriteCond %{HTTP_USER_AGENT} "Steeler" [NC,OR]
159
  RewriteCond %{HTTP_USER_AGENT} "URI\:\:Fetch" [NC,OR]
153
  RewriteCond %{HTTP_USER_AGENT} "PleaseCrawl" [NC,OR]
154
  RewriteCond %{HTTP_USER_AGENT} "SBIder" [NC,OR]
155
  RewriteCond %{HTTP_USER_AGENT} "SearchmetricsBot" [NC,OR]
 
156
  RewriteCond %{HTTP_USER_AGENT} "Snoopy" [NC,OR]
157
  RewriteCond %{HTTP_USER_AGENT} "Steeler" [NC,OR]
158
  RewriteCond %{HTTP_USER_AGENT} "URI\:\:Fetch" [NC,OR]
core/modules/ban-users/lists/hackrepair-nginx.inc CHANGED
@@ -152,7 +152,6 @@ if ($http_user_agent ~* "PHPCrawl"){return 403;}
152
  if ($http_user_agent ~* "PleaseCrawl"){return 403;}
153
  if ($http_user_agent ~* "SBIder"){return 403;}
154
  if ($http_user_agent ~* "SearchmetricsBot"){return 403;}
155
- if ($http_user_agent ~* "SeznamBot"){return 403;}
156
  if ($http_user_agent ~* "Snoopy"){return 403;}
157
  if ($http_user_agent ~* "Steeler"){return 403;}
158
  if ($http_user_agent ~* "URI\:\:Fetch"){return 403;}
152
  if ($http_user_agent ~* "PleaseCrawl"){return 403;}
153
  if ($http_user_agent ~* "SBIder"){return 403;}
154
  if ($http_user_agent ~* "SearchmetricsBot"){return 403;}
 
155
  if ($http_user_agent ~* "Snoopy"){return 403;}
156
  if ($http_user_agent ~* "Steeler"){return 403;}
157
  if ($http_user_agent ~* "URI\:\:Fetch"){return 403;}
core/modules/brute-force/class-itsec-brute-force.php CHANGED
@@ -25,7 +25,7 @@ class ITSEC_Brute_Force {
25
  * @param string $username username attempted
26
  * @param string $password password attempted
27
  *
28
- * @return user object or WordPress error
29
  */
30
  public function authenticate( $user, $username = '', $password = '' ) {
31
  /** @var ITSEC_Lockout $itsec_lockout */
@@ -48,6 +48,7 @@ class ITSEC_Brute_Force {
48
  $itsec_lockout->do_lockout( 'brute_force_admin_user', $username );
49
  } else {
50
  $user_id = false;
 
51
 
52
  if ( empty( $username ) ) {
53
  $itsec_lockout->check_lockout( false, false, 'brute_force_empty_username' );
@@ -56,12 +57,14 @@ class ITSEC_Brute_Force {
56
 
57
  if ( empty( $user_id ) ) {
58
  $itsec_lockout->check_lockout( false, $username, 'brute_force_invalid_username' );
 
59
  } else {
60
  $itsec_lockout->check_lockout( $user_id, false, 'brute_force_invalid_password' );
 
61
  }
62
  }
63
 
64
- ITSEC_Log::add_notice( 'brute_force', 'invalid-login', compact( 'details', 'user', 'username', 'user_id', 'SERVER' ) );
65
 
66
  $itsec_lockout->do_lockout( 'brute_force', $username );
67
  }
25
  * @param string $username username attempted
26
  * @param string $password password attempted
27
  *
28
+ * @return WP_User|WP_Error|null
29
  */
30
  public function authenticate( $user, $username = '', $password = '' ) {
31
  /** @var ITSEC_Lockout $itsec_lockout */
48
  $itsec_lockout->do_lockout( 'brute_force_admin_user', $username );
49
  } else {
50
  $user_id = false;
51
+ $code = 'invalid-login';
52
 
53
  if ( empty( $username ) ) {
54
  $itsec_lockout->check_lockout( false, false, 'brute_force_empty_username' );
57
 
58
  if ( empty( $user_id ) ) {
59
  $itsec_lockout->check_lockout( false, $username, 'brute_force_invalid_username' );
60
+ $code = "invalid-login::username-{$username}";
61
  } else {
62
  $itsec_lockout->check_lockout( $user_id, false, 'brute_force_invalid_password' );
63
+ $code = "invalid-login::user-{$user_id}";
64
  }
65
  }
66
 
67
+ ITSEC_Log::add_notice( 'brute_force', $code, compact( 'details', 'user', 'username', 'user_id', 'SERVER' ) );
68
 
69
  $itsec_lockout->do_lockout( 'brute_force', $username );
70
  }
core/modules/brute-force/logs.php CHANGED
@@ -2,16 +2,17 @@
2
 
3
  final class ITSEC_Brute_Force_Logs {
4
  public function __construct() {
5
- add_filter( 'itsec_logs_prepare_brute_force_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ) );
6
  add_filter( 'itsec_logs_prepare_brute_force_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
 
7
  }
8
 
9
- public function filter_entry_for_list_display( $entry ) {
10
  $entry['module_display'] = esc_html__( 'Brute Force', 'better-wp-security' );
11
 
12
- if ( 'invalid-login' === $entry['code'] ) {
13
  $entry['description'] = esc_html__( 'Invalid Login', 'better-wp-security' );
14
- } else if ( 'auto-ban-admin-username' === $entry['code'] ) {
15
  $entry['description'] = esc_html__( 'Banned Use of "admin" Username', 'better-wp-security' );
16
  }
17
 
@@ -43,5 +44,13 @@ final class ITSEC_Brute_Force_Logs {
43
 
44
  return $details;
45
  }
 
 
 
 
 
 
 
 
46
  }
47
  new ITSEC_Brute_Force_Logs();
2
 
3
  final class ITSEC_Brute_Force_Logs {
4
  public function __construct() {
5
+ add_filter( 'itsec_logs_prepare_brute_force_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ), 10, 2 );
6
  add_filter( 'itsec_logs_prepare_brute_force_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
7
+ add_filter( 'itsec_logs_prepare_brute_force_filter_row_action_for_code', array( $this, 'code_row_action' ), 10, 4 );
8
  }
9
 
10
+ public function filter_entry_for_list_display( $entry, $code ) {
11
  $entry['module_display'] = esc_html__( 'Brute Force', 'better-wp-security' );
12
 
13
+ if ( 'invalid-login' === $code ) {
14
  $entry['description'] = esc_html__( 'Invalid Login', 'better-wp-security' );
15
+ } else if ( 'auto-ban-admin-username' === $code ) {
16
  $entry['description'] = esc_html__( 'Banned Use of "admin" Username', 'better-wp-security' );
17
  }
18
 
44
 
45
  return $details;
46
  }
47
+
48
+ public function code_row_action( $vars, $entry, $code, $data ) {
49
+ if ( 'invalid-login' === $code ) {
50
+ $vars = array( 'filters[10]' => 'code|invalid-login%' );
51
+ }
52
+
53
+ return $vars;
54
+ }
55
  }
56
  new ITSEC_Brute_Force_Logs();
core/modules/file-change/logs.php CHANGED
@@ -56,7 +56,11 @@ final class ITSEC_File_Change_Logs {
56
  $entry['description'] = esc_html__( 'Scan Failed', 'better-wp-security' );
57
  }
58
  } elseif ( 'rescheduling' === $code ) {
59
- $entry['description'] = esc_html__( 'Rescheduling', 'better-wp-security' );
 
 
 
 
60
  }
61
 
62
  $entry['remote_ip'] = '';
56
  $entry['description'] = esc_html__( 'Scan Failed', 'better-wp-security' );
57
  }
58
  } elseif ( 'rescheduling' === $code ) {
59
+ if ( isset( $code_data[0] ) && 'no-lock' === $code_data[0] ) {
60
+ $entry['description'] = esc_html__( 'Rescheduling: No Lock', 'better-wp-security' );
61
+ } else {
62
+ $entry['description'] = esc_html__( 'Rescheduling', 'better-wp-security' );
63
+ }
64
  }
65
 
66
  $entry['remote_ip'] = '';
core/modules/file-change/scanner.php CHANGED
@@ -130,21 +130,59 @@ class ITSEC_File_Change_Scanner {
130
  */
131
  public static function is_running( $scheduler = null, $user_initiated = null ) {
132
 
 
 
 
133
  $scheduler = $scheduler ? $scheduler : ITSEC_Core::get_scheduler();
 
134
 
135
- if ( true === $user_initiated ) {
136
- if ( $scheduler->is_single_scheduled( 'file-change-fast' ) ) {
137
- return true;
138
- }
139
- } elseif ( false === $user_initiated ) {
140
- if ( $scheduler->is_single_scheduled( 'file-change' ) ) {
141
  return true;
142
  }
143
- } elseif ( null === $user_initiated ) {
144
- return $scheduler->is_single_scheduled( 'file-change' ) || $scheduler->is_single_scheduled( 'file-change-fast' );
 
 
 
 
145
  }
146
 
147
- return ! ITSEC_File_Change::make_progress_storage()->is_empty();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  }
149
 
150
  /**
@@ -251,7 +289,7 @@ class ITSEC_File_Change_Scanner {
251
  */
252
  public static function recover() {
253
 
254
- if ( ! ITSEC_Lib::get_lock( 'file-change-recover' ) ) {
255
  ITSEC_Log::add_debug( 'file_change', 'skipping-recovery::no-lock' );
256
 
257
  return false;
@@ -260,6 +298,7 @@ class ITSEC_File_Change_Scanner {
260
  $storage = ITSEC_File_Change::make_progress_storage();
261
 
262
  if ( $storage->is_empty() ) {
 
263
  ITSEC_Log::add_debug( 'file_change', 'skipping-recovery::empty-storage', array(
264
  'backtrace' => debug_backtrace()
265
  ) );
@@ -286,7 +325,7 @@ class ITSEC_File_Change_Scanner {
286
 
287
  self::abort();
288
 
289
- ITSEC_Lib::release_lock( 'file-change-recover' );
290
 
291
  return false;
292
  }
@@ -300,7 +339,7 @@ class ITSEC_File_Change_Scanner {
300
 
301
  self::abort();
302
 
303
- ITSEC_Lib::release_lock( 'file-change-recover' );
304
 
305
  return false;
306
  }
@@ -312,7 +351,7 @@ class ITSEC_File_Change_Scanner {
312
 
313
  self::abort();
314
 
315
- ITSEC_Lib::release_lock( 'file-change-recover' );
316
 
317
  return false;
318
  }
@@ -320,7 +359,7 @@ class ITSEC_File_Change_Scanner {
320
  $job->reschedule_in( 30 );
321
 
322
  ITSEC_Log::add_debug( 'file_change', 'recovery-scheduled', compact( 'job' ) );
323
- ITSEC_Lib::release_lock( 'file-change-recover' );
324
 
325
  return true;
326
  }
@@ -379,7 +418,15 @@ class ITSEC_File_Change_Scanner {
379
  return;
380
  }
381
 
 
 
 
 
 
 
 
382
  if ( ! $this->allow_to_run( $job ) ) {
 
383
  ITSEC_Log::add_debug( 'file_change', 'rescheduling', array( 'job' => $data, 'id' => $job->get_id() ) );
384
  $job->reschedule_in( 10 * MINUTE_IN_SECONDS );
385
 
@@ -426,6 +473,8 @@ class ITSEC_File_Change_Scanner {
426
  }
427
 
428
  if ( $this->get_storage()->is_empty() ) {
 
 
429
  return;
430
  }
431
 
@@ -439,6 +488,8 @@ class ITSEC_File_Change_Scanner {
439
  $this->get_storage()->set( 'memory', $memory_used );
440
  $this->get_storage()->set( 'memory_peak', $check_memory );
441
  }
 
 
442
  }
443
 
444
  /**
130
  */
131
  public static function is_running( $scheduler = null, $user_initiated = null ) {
132
 
133
+ $storage = ITSEC_File_Change::make_progress_storage();
134
+ $id = $storage->get( 'id' );
135
+
136
  $scheduler = $scheduler ? $scheduler : ITSEC_Core::get_scheduler();
137
+ $scheduled = self::is_scheduled( $scheduler, $user_initiated );
138
 
139
+ if ( null === $user_initiated ) {
140
+ if ( ! $storage->is_empty() ) {
 
 
 
 
141
  return true;
142
  }
143
+
144
+ return $scheduled === 'user';
145
+ }
146
+
147
+ if ( true === $user_initiated ) {
148
+ return 'user' === $scheduled || $id === 'file-change-fast';
149
  }
150
 
151
+ if ( false === $user_initiated ) {
152
+ return 'scheduled' === $scheduled || $id === 'file-change';
153
+ }
154
+
155
+ return false;
156
+ }
157
+
158
+ /**
159
+ * Is there a scan scheduled.
160
+ *
161
+ * @param ITSEC_Scheduler $scheduler The scheduler to use.
162
+ * @param bool $user_initiated Whether the user initiated scan is running or the scheduled loop scan.
163
+ * Null to check either.
164
+ *
165
+ * @return bool Is it scheduled.
166
+ */
167
+ private static function is_scheduled( $scheduler, $user_initiated = null ) {
168
+
169
+ if ( true === $user_initiated ) {
170
+ return $scheduler->is_single_scheduled( 'file-change-fast', null ) ? 'user' : false;
171
+ }
172
+
173
+ if ( false === $user_initiated ) {
174
+ return $scheduler->is_single_scheduled( 'file-change', null ) ? 'scheduled' : false;
175
+ }
176
+
177
+ if ( $scheduler->is_single_scheduled( 'file-change-fast', null ) ) {
178
+ return 'user';
179
+ }
180
+
181
+ if ( $scheduler->is_single_scheduled( 'file-change', null ) ) {
182
+ return 'scheduled';
183
+ }
184
+
185
+ return false;
186
  }
187
 
188
  /**
289
  */
290
  public static function recover() {
291
 
292
+ if ( ! ITSEC_Lib::get_lock( 'file-change' ) ) {
293
  ITSEC_Log::add_debug( 'file_change', 'skipping-recovery::no-lock' );
294
 
295
  return false;
298
  $storage = ITSEC_File_Change::make_progress_storage();
299
 
300
  if ( $storage->is_empty() ) {
301
+ ITSEC_Lib::release_lock( 'file-change' );
302
  ITSEC_Log::add_debug( 'file_change', 'skipping-recovery::empty-storage', array(
303
  'backtrace' => debug_backtrace()
304
  ) );
325
 
326
  self::abort();
327
 
328
+ ITSEC_Lib::release_lock( 'file-change' );
329
 
330
  return false;
331
  }
339
 
340
  self::abort();
341
 
342
+ ITSEC_Lib::release_lock( 'file-change' );
343
 
344
  return false;
345
  }
351
 
352
  self::abort();
353
 
354
+ ITSEC_Lib::release_lock( 'file-change' );
355
 
356
  return false;
357
  }
359
  $job->reschedule_in( 30 );
360
 
361
  ITSEC_Log::add_debug( 'file_change', 'recovery-scheduled', compact( 'job' ) );
362
+ ITSEC_Lib::release_lock( 'file-change' );
363
 
364
  return true;
365
  }
418
  return;
419
  }
420
 
421
+ if ( ! ITSEC_Lib::get_lock( 'file-change', 5 * MINUTE_IN_SECONDS ) ) {
422
+ ITSEC_Log::add_debug( 'file_change', 'rescheduling::no-lock', array( 'job' => $data, 'id' => $job->get_id() ) );
423
+ $job->reschedule_in( 2 * MINUTE_IN_SECONDS );
424
+
425
+ return;
426
+ }
427
+
428
  if ( ! $this->allow_to_run( $job ) ) {
429
+ ITSEC_Lib::release_lock( 'file-change' );
430
  ITSEC_Log::add_debug( 'file_change', 'rescheduling', array( 'job' => $data, 'id' => $job->get_id() ) );
431
  $job->reschedule_in( 10 * MINUTE_IN_SECONDS );
432
 
473
  }
474
 
475
  if ( $this->get_storage()->is_empty() ) {
476
+ ITSEC_Lib::release_lock( 'file-change' );
477
+
478
  return;
479
  }
480
 
488
  $this->get_storage()->set( 'memory', $memory_used );
489
  $this->get_storage()->set( 'memory_peak', $check_memory );
490
  }
491
+
492
+ ITSEC_Lib::release_lock( 'file-change' );
493
  }
494
 
495
  /**
core/modules/global/logs.php ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class ITSEC_Global_Logs
5
+ */
6
+ class ITSEC_Global_Logs {
7
+
8
+ public function __construct() {
9
+ add_filter( 'itsec_logs_prepare_core_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ), 10, 3 );
10
+ add_filter( 'itsec_logs_prepare_core_entry_for_details_display', array( $this, 'filter_entry_for_details_display' ), 10, 4 );
11
+ add_filter( 'itsec_logs_prepare_core_filter_row_action_for_code', array( $this, 'code_row_action' ), 10, 4 );
12
+ }
13
+
14
+ public function filter_entry_for_list_display( $entry, $code, $data ) {
15
+ $entry['module_display'] = esc_html__( 'Core', 'better-wp-security' );
16
+
17
+
18
+ if ( $description = $this->get_description( $entry, $code, $data ) ) {
19
+ $entry['description'] = $description;
20
+ }
21
+
22
+ return $entry;
23
+ }
24
+
25
+ public function filter_entry_for_details_display( $details, $entry, $code, $code_data ) {
26
+ $details['module']['content'] = esc_html__( 'Core', 'better-wp-security' );
27
+
28
+ if ( $description = $this->get_description( $entry, $code, $code_data ) ) {
29
+ $details['description']['content'] = $description;
30
+ }
31
+
32
+ return $details;
33
+ }
34
+
35
+ public function code_row_action( $vars, $entry, $code, $data ) {
36
+
37
+ return $vars;
38
+ }
39
+
40
+ private function get_description( $entry, $code, $data ) {
41
+ switch ( $code ) {
42
+ case 'itsec-config-file-update-empty':
43
+ list( $type ) = $data;
44
+
45
+ return sprintf( esc_html__( 'Empty file encountered when attempting to update %s config file.', 'better-wp-security' ), '<code>' . esc_html( $type ) . '</code>' );
46
+ }
47
+
48
+ return null;
49
+ }
50
+ }
51
+
52
+ new ITSEC_Global_Logs();
core/modules/global/settings.php CHANGED
@@ -37,6 +37,7 @@ final class ITSEC_Global_Settings_New extends ITSEC_Settings {
37
  'use_cron' => true,
38
  'cron_test_time' => 0,
39
  'enable_grade_report' => false,
 
40
  );
41
  }
42
 
37
  'use_cron' => true,
38
  'cron_test_time' => 0,
39
  'enable_grade_report' => false,
40
+ 'server_ips' => array(),
41
  );
42
  }
43
 
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', 'show_new_dashboard_notice', 'proxy_override' );
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', 'enable_grade_report' ) );
25
  $this->preserve_setting_if_exists( array( 'digest_email', 'email_notifications', 'notification_email', 'backup_email', 'proxy_override' ) );
26
 
@@ -58,6 +58,8 @@ class ITSEC_Global_Validator extends ITSEC_Validator {
58
  $this->settings['lockout_message'] = trim( wp_kses( $this->settings['lockout_message'], $allowed_tags ) );
59
  $this->settings['user_lockout_message'] = trim( wp_kses( $this->settings['user_lockout_message'], $allowed_tags ) );
60
  $this->settings['community_lockout_message'] = trim( wp_kses( $this->settings['community_lockout_message'], $allowed_tags ) );
 
 
61
  }
62
 
63
  public function get_proxy_types() {
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', 'proxy_override', 'proxy', 'proxy_header', 'server_ips' );
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', 'proxy', 'proxy_header', 'server_ips' ) );
24
  $this->set_default_if_empty( array( 'log_location', 'nginx_file', 'enable_grade_report' ) );
25
  $this->preserve_setting_if_exists( array( 'digest_email', 'email_notifications', 'notification_email', 'backup_email', 'proxy_override' ) );
26
 
58
  $this->settings['lockout_message'] = trim( wp_kses( $this->settings['lockout_message'], $allowed_tags ) );
59
  $this->settings['user_lockout_message'] = trim( wp_kses( $this->settings['user_lockout_message'], $allowed_tags ) );
60
  $this->settings['community_lockout_message'] = trim( wp_kses( $this->settings['community_lockout_message'], $allowed_tags ) );
61
+
62
+ $this->sanitize_setting( 'newline-separated-ips', 'server_ips', __( 'Server IPs', 'better-wp-security' ) );
63
  }
64
 
65
  public function get_proxy_types() {
core/modules/hide-backend/class-itsec-hide-backend.php CHANGED
@@ -258,8 +258,10 @@ class ITSEC_Hide_Backend {
258
  if ( false !== strpos( $path, 'action=postpass' ) ) {
259
  // No special handling is needed for a password-protected post.
260
  return $url;
261
- } else if ( false !== strpos( $path, 'action=register' ) ) {
262
  $url = $this->add_token_to_url( $url, 'register' );
 
 
263
  } elseif ( 'wp-login.php' !== $request_path || empty( $_GET['action'] ) || 'register' !== $_GET['action'] ) {
264
  $url = $this->add_token_to_url( $url, 'login' );
265
  }
258
  if ( false !== strpos( $path, 'action=postpass' ) ) {
259
  // No special handling is needed for a password-protected post.
260
  return $url;
261
+ } elseif ( false !== strpos( $path, 'action=register' ) ) {
262
  $url = $this->add_token_to_url( $url, 'register' );
263
+ } elseif ( false !== strpos( $path, 'action=rp' ) ) {
264
+ $url = $this->add_token_to_url( $url, 'login' );
265
  } elseif ( 'wp-login.php' !== $request_path || empty( $_GET['action'] ) || 'register' !== $_GET['action'] ) {
266
  $url = $this->add_token_to_url( $url, 'login' );
267
  }
core/modules/malware/class-itsec-malware-scanner.php CHANGED
@@ -27,10 +27,19 @@ final class ITSEC_Malware_Scanner {
27
  } else if ( ! empty( $results['SYSTEM']['ERROR'] ) ) {
28
  ITSEC_Log::add_warning( 'malware', 'sucuri-system-error', compact( 'results' ) );
29
  } else if ( ! empty( $results['MALWARE']['WARN'] ) ) {
 
 
 
 
 
 
 
 
 
30
  if ( ! empty( $results['BLACKLIST']['WARN'] ) ) {
31
- ITSEC_Log::add_critical_issue( 'malware', 'found-malware-and-on-blacklist', compact( 'results' ) );
32
  } else {
33
- ITSEC_Log::add_critical_issue( 'malware', 'found-malware', compact( 'results' ) );
34
  }
35
  } else if ( ! empty( $results['BLACKLIST']['WARN'] ) ) {
36
  ITSEC_Log::add_critical_issue( 'malware', 'on-blacklist', compact( 'results' ) );
@@ -103,7 +112,7 @@ final class ITSEC_Malware_Scanner {
103
  if ( false === $response ) {
104
  $cached = false;
105
 
106
- $site_url = apply_filters( 'itsec_test_malware_scan_site_url', get_site_url() );
107
 
108
  if ( defined( 'ITSEC_TEST_MALWARE_SCAN_SITE_URL' ) ) {
109
  ITSEC_Log::add_process_update( $process_id, array( 'action' => 'define-force-site-url', 'original-site-url' => $site_url, 'define-name' => 'ITSEC_TEST_MALWARE_SCAN_SITE_URL', 'define-value' => ITSEC_TEST_MALWARE_SCAN_SITE_URL ) );
@@ -148,7 +157,7 @@ final class ITSEC_Malware_Scanner {
148
  */
149
  protected static function scan_sub_site( $site_id, $process_id ) {
150
 
151
- $url = get_site_url( $site_id );
152
  $record = array(
153
  'url' => $url,
154
  'id' => $site_id,
27
  } else if ( ! empty( $results['SYSTEM']['ERROR'] ) ) {
28
  ITSEC_Log::add_warning( 'malware', 'sucuri-system-error', compact( 'results' ) );
29
  } else if ( ! empty( $results['MALWARE']['WARN'] ) ) {
30
+ $data = compact( 'results' );
31
+
32
+ if ( ITSEC_Lib_Remote_Messages::has_action( 'malware-scanner-disable-malware-warnings' ) ) {
33
+ $data['sucuri_error'] = true;
34
+ ITSEC_Log::add_warning( 'malware', 'malware-warning-suppressed', $data );
35
+
36
+ return $results;
37
+ }
38
+
39
  if ( ! empty( $results['BLACKLIST']['WARN'] ) ) {
40
+ ITSEC_Log::add_critical_issue( 'malware', 'found-malware-and-on-blacklist', $data );
41
  } else {
42
+ ITSEC_Log::add_critical_issue( 'malware', 'found-malware', $data );
43
  }
44
  } else if ( ! empty( $results['BLACKLIST']['WARN'] ) ) {
45
  ITSEC_Log::add_critical_issue( 'malware', 'on-blacklist', compact( 'results' ) );
112
  if ( false === $response ) {
113
  $cached = false;
114
 
115
+ $site_url = apply_filters( 'itsec_test_malware_scan_site_url', get_home_url() );
116
 
117
  if ( defined( 'ITSEC_TEST_MALWARE_SCAN_SITE_URL' ) ) {
118
  ITSEC_Log::add_process_update( $process_id, array( 'action' => 'define-force-site-url', 'original-site-url' => $site_url, 'define-name' => 'ITSEC_TEST_MALWARE_SCAN_SITE_URL', 'define-value' => ITSEC_TEST_MALWARE_SCAN_SITE_URL ) );
157
  */
158
  protected static function scan_sub_site( $site_id, $process_id ) {
159
 
160
+ $url = get_home_url( $site_id );
161
  $record = array(
162
  'url' => $url,
163
  'id' => $site_id,
core/modules/malware/logs.php CHANGED
@@ -33,6 +33,8 @@ final class ITSEC_Malware_Logs {
33
  $entry['description'] = esc_html__( 'Site on Blacklist', 'better-wp-security' );
34
  } else if ( 'scan' === $entry['code'] ) {
35
  $entry['description'] = esc_html__( 'Scan', 'better-wp-security' );
 
 
36
  }
37
 
38
  return $entry;
33
  $entry['description'] = esc_html__( 'Site on Blacklist', 'better-wp-security' );
34
  } else if ( 'scan' === $entry['code'] ) {
35
  $entry['description'] = esc_html__( 'Scan', 'better-wp-security' );
36
+ } else if ( 'malware-warning-suppressed' === $entry['code'] ) {
37
+ $entry['description'] = esc_html__( 'Possible Malware, Scanner Experiencing Issues', 'better-wp-security' );
38
  }
39
 
40
  return $entry;
core/modules/notification-center/class-notification-center.php CHANGED
@@ -322,17 +322,17 @@ final class ITSEC_Notification_Center {
322
  $config = $this->get_notification( $notification );
323
 
324
  if ( self::R_ADMIN === $config['recipient'] ) {
325
- return array( get_option( 'admin_email' ) );
326
  }
327
 
328
  if ( self::R_EMAIL_LIST === $config['recipient'] ) {
329
  $settings = $this->get_notification_settings( $notification );
330
 
331
- return ! empty( $settings['email_list'] ) ? $settings['email_list'] : array();
332
  }
333
 
334
  if ( self::R_USER_LIST !== $config['recipient'] && self::R_USER_LIST_ADMIN_UPGRADE !== $config['recipient'] ) {
335
- return array();
336
  }
337
 
338
  $settings = $this->get_notification_settings( $notification );
@@ -363,8 +363,9 @@ final class ITSEC_Notification_Center {
363
  }
364
  }
365
 
366
- require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-canonical-roles.php' );
367
- $users = array_merge( $users, ITSEC_Lib_Canonical_Roles::get_users_with_canonical_role( $roles ) );
 
368
 
369
  foreach ( $users as $user ) {
370
  if ( is_object( $user ) && ! empty( $user->user_email ) ) {
@@ -376,7 +377,41 @@ final class ITSEC_Notification_Center {
376
  $addresses = array_merge( $addresses, $settings['previous_emails'] );
377
  }
378
 
379
- return array_unique( $addresses );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  }
381
 
382
  /**
@@ -1132,4 +1167,4 @@ final class ITSEC_Notification_Center {
1132
 
1133
  return $out;
1134
  }
1135
- }
322
  $config = $this->get_notification( $notification );
323
 
324
  if ( self::R_ADMIN === $config['recipient'] ) {
325
+ return $this->filter_recipients( array( get_option( 'admin_email' ) ), $notification );
326
  }
327
 
328
  if ( self::R_EMAIL_LIST === $config['recipient'] ) {
329
  $settings = $this->get_notification_settings( $notification );
330
 
331
+ return $this->filter_recipients( ! empty( $settings['email_list'] ) ? $settings['email_list'] : array(), $notification );
332
  }
333
 
334
  if ( self::R_USER_LIST !== $config['recipient'] && self::R_USER_LIST_ADMIN_UPGRADE !== $config['recipient'] ) {
335
+ return $this->filter_recipients( array(), $notification );
336
  }
337
 
338
  $settings = $this->get_notification_settings( $notification );
363
  }
364
  }
365
 
366
+ if ( $roles ) {
367
+ $users = array_merge( $users, get_users( array( 'role__in' => $roles ) ) );
368
+ }
369
 
370
  foreach ( $users as $user ) {
371
  if ( is_object( $user ) && ! empty( $user->user_email ) ) {
377
  $addresses = array_merge( $addresses, $settings['previous_emails'] );
378
  }
379
 
380
+ return $this->filter_recipients( array_unique( $addresses ), $notification );
381
+ }
382
+
383
+ /**
384
+ * Filter the recipients for a notification.
385
+ *
386
+ * @since 4.8.4
387
+ *
388
+ * @param string[] $recipients Array of email addresses.
389
+ * @param string $notification The notification slug.
390
+ *
391
+ * @return string[] Filtered array of email addresses.
392
+ */
393
+ private function filter_recipients( $recipients, $notification ) {
394
+
395
+ /**
396
+ * Fitler the email addresses that will receive the given notification.
397
+ *
398
+ * The dynamic portion of this hook '$notification' refers to the notification slug.
399
+ *
400
+ * @since 4.8.4
401
+ *
402
+ * @param string[] $recipients Array of email addresses.
403
+ */
404
+ $recipients = apply_filters( "itsec_notification_{$notification}_email_recipients", $recipients );
405
+
406
+ /**
407
+ * Filter the email addresses that will receive the given notification.
408
+ *
409
+ * @since 4.8.4
410
+ *
411
+ * @param string[] $recipients Array of email addresses.
412
+ * @param string $notification The notification slug.
413
+ */
414
+ return apply_filters( 'itsec_notification_email_recipients', $recipients, $notification );
415
  }
416
 
417
  /**
1167
 
1168
  return $out;
1169
  }
1170
+ }
core/modules/password-requirements/class-itsec-password-requirements.php CHANGED
@@ -327,7 +327,7 @@ class ITSEC_Password_Requirements {
327
  if ( ! ITSEC_Lib_Password_Requirements::is_requirement_enabled( $code ) ) {
328
  continue;
329
  }
330
-
331
  $settings = ITSEC_Lib_Password_Requirements::get_requirement_settings( $code );
332
 
333
  if ( $requirement['flag_check'] && call_user_func( $requirement['flag_check'], $user, $settings ) ) {
@@ -404,6 +404,8 @@ class ITSEC_Password_Requirements {
404
  * @param WP_User $user
405
  */
406
  public function render_interstitial( $user ) {
 
 
407
  do_action( 'itsec_password_requirements_change_form', $user );
408
  ?>
409
 
@@ -477,4 +479,4 @@ class ITSEC_Password_Requirements {
477
 
478
  return null;
479
  }
480
- }
327
  if ( ! ITSEC_Lib_Password_Requirements::is_requirement_enabled( $code ) ) {
328
  continue;
329
  }
330
+
331
  $settings = ITSEC_Lib_Password_Requirements::get_requirement_settings( $code );
332
 
333
  if ( $requirement['flag_check'] && call_user_func( $requirement['flag_check'], $user, $settings ) ) {
404
  * @param WP_User $user
405
  */
406
  public function render_interstitial( $user ) {
407
+ wp_enqueue_script( 'user-profile' );
408
+
409
  do_action( 'itsec_password_requirements_change_form', $user );
410
  ?>
411
 
479
 
480
  return null;
481
  }
482
+ }
core/modules/security-check/active.php CHANGED
@@ -6,3 +6,30 @@ function itsec_security_check_register_sync_verbs( $api ) {
6
  $api->register( 'itsec-get-security-check-modules', 'Ithemes_Sync_Verb_ITSEC_Get_Security_Check_Modules', dirname( __FILE__ ) . '/sync-verbs/itsec-get-security-check-modules.php' );
7
  }
8
  add_action( 'ithemes_sync_register_verbs', 'itsec_security_check_register_sync_verbs' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  $api->register( 'itsec-get-security-check-modules', 'Ithemes_Sync_Verb_ITSEC_Get_Security_Check_Modules', dirname( __FILE__ ) . '/sync-verbs/itsec-get-security-check-modules.php' );
7
  }
8
  add_action( 'ithemes_sync_register_verbs', 'itsec_security_check_register_sync_verbs' );
9
+
10
+ /**
11
+ * Handle the loopback callback test.
12
+ */
13
+ function itsec_security_check_loopback_callback() {
14
+ if ( ! isset( $_POST['hash'], $_POST['exp'] ) ) {
15
+ wp_die();
16
+ }
17
+
18
+ $hash = $_POST['hash'];
19
+ $exp = $_POST['exp'];
20
+
21
+ $expected = hash_hmac( 'sha1', "itsec-check-loopback|{$exp}", wp_salt() );
22
+
23
+ if ( ! hash_equals( $hash, $expected ) ) {
24
+ wp_die();
25
+ }
26
+
27
+ if ( $exp < ITSEC_Core::get_current_time_gmt() ) {
28
+ wp_die();
29
+ }
30
+
31
+ echo ITSEC_Lib::get_ip();
32
+ die;
33
+ }
34
+
35
+ add_action( 'admin_post_nopriv_itsec-check-loopback', 'itsec_security_check_loopback_callback' );
core/modules/security-check/feedback-renderer.php CHANGED
@@ -17,6 +17,9 @@ final class ITSEC_Security_Check_Feedback_Renderer {
17
  if ( isset( $section_groups['confirmation'] ) ) {
18
  self::render_sections( 'confirmation', $section_groups['confirmation'] );
19
  }
 
 
 
20
  }
21
 
22
  private static function render_sections( $status, $sections ) {
17
  if ( isset( $section_groups['confirmation'] ) ) {
18
  self::render_sections( 'confirmation', $section_groups['confirmation'] );
19
  }
20
+ if ( isset( $section_groups['error'] ) ) {
21
+ self::render_sections( 'error', $section_groups['error'] );
22
+ }
23
  }
24
 
25
  private static function render_sections( $status, $sections ) {
core/modules/security-check/scanner.php CHANGED
@@ -2,6 +2,8 @@
2
 
3
  final class ITSEC_Security_Check_Scanner {
4
  private static $available_modules;
 
 
5
  private static $feedback;
6
 
7
 
@@ -93,6 +95,8 @@ final class ITSEC_Security_Check_Scanner {
93
  self::enforce_setting( 'global', 'write_files', true, __( 'Enabled the Write to Files setting in Global Settings.', 'better-wp-security' ) );
94
 
95
  self::enforce_setting( 'online-files', 'compare_file_hashes', true, __( 'Enabled Online Files Comparison in File Change Detection.', 'better-wp-security' ) );
 
 
96
 
97
  do_action( 'itsec-security-check-after-default-checks', self::$feedback, self::$available_modules );
98
  }
@@ -219,4 +223,69 @@ final class ITSEC_Security_Check_Scanner {
219
  ITSEC_Response::set_response( '<p>' . __( 'Your site is now using Network Brute Force Protection.', 'better-wp-security' ) . '</p>' );
220
  }
221
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  }
2
 
3
  final class ITSEC_Security_Check_Scanner {
4
  private static $available_modules;
5
+
6
+ /** @var ITSEC_Security_Check_Feedback */
7
  private static $feedback;
8
 
9
 
95
  self::enforce_setting( 'global', 'write_files', true, __( 'Enabled the Write to Files setting in Global Settings.', 'better-wp-security' ) );
96
 
97
  self::enforce_setting( 'online-files', 'compare_file_hashes', true, __( 'Enabled Online Files Comparison in File Change Detection.', 'better-wp-security' ) );
98
+ self::check_server_ips();
99
+ self::do_loopback();
100
 
101
  do_action( 'itsec-security-check-after-default-checks', self::$feedback, self::$available_modules );
102
  }
223
  ITSEC_Response::set_response( '<p>' . __( 'Your site is now using Network Brute Force Protection.', 'better-wp-security' ) . '</p>' );
224
  }
225
  }
226
+
227
+ private static function check_server_ips() {
228
+
229
+ $response = dns_get_record( parse_url( site_url(), PHP_URL_HOST ), DNS_A + ( defined( 'DNS_AAAA' ) ? DNS_AAAA : 0 ) );
230
+
231
+ if ( ! $response ) {
232
+ return;
233
+ }
234
+
235
+ $ips = array();
236
+
237
+ foreach ( $response as $record ) {
238
+ if ( isset( $record['ipv6'] ) ) {
239
+ $ips[] = $record['ipv6'];
240
+ }
241
+
242
+ if ( isset( $record['ip'] ) ) {
243
+ $ips[] = $record['ip'];
244
+ }
245
+ }
246
+
247
+ if ( $ips ) {
248
+ ITSEC_Modules::set_setting( 'global', 'server_ips', array_merge( $ips, ITSEC_Modules::get_setting( 'global', 'server_ips' ) ) );
249
+
250
+ self::$feedback->add_section( 'server-ips', array( 'status' => 'action-taken' ) );
251
+ self::$feedback->add_text( __( 'Identified server IPs to determine loopback requests.', 'better-wp-security' ) );
252
+ }
253
+ }
254
+
255
+ private static function do_loopback() {
256
+ $exp = ITSEC_Core::get_current_time_gmt() + 60;
257
+ $action = 'itsec-check-loopback';
258
+ $hash = hash_hmac( 'sha1', "{$action}|{$exp}", wp_salt() );
259
+
260
+ $response = wp_remote_post( admin_url( 'admin-post.php' ), array(
261
+ 'body' => array(
262
+ 'action' => $action,
263
+ 'hash' => $hash,
264
+ 'exp' => $exp,
265
+ ),
266
+ ) );
267
+
268
+ if ( is_wp_error( $response ) ) {
269
+ self::$feedback->add_section( 'loopback', array( 'status' => 'error' ) );
270
+ self::$feedback->add_text( sprintf( __( 'Skipping loopback test: %s', 'better-wp-security' ), $response->get_error_message() ) );
271
+
272
+ return;
273
+ }
274
+
275
+ $ip = trim( wp_remote_retrieve_body( $response ) );
276
+
277
+ require_once( ITSEC_Core::get_core_dir() . '/lib/class-itsec-lib-ip-tools.php' );
278
+
279
+ if ( ! ITSEC_Lib_IP_Tools::validate( $ip ) ) {
280
+ self::$feedback->add_section( 'loopback', array( 'status' => 'error' ) );
281
+ self::$feedback->add_text( sprintf( __( 'Invalid IP returned: %s', 'better-wp-security' ), esc_attr( $ip ) ) );
282
+
283
+ return;
284
+ }
285
+
286
+ ITSEC_Modules::set_setting( 'global', 'server_ips', array_merge( array( $ip ), ITSEC_Modules::get_setting( 'global', 'server_ips' ) ) );
287
+
288
+ self::$feedback->add_section( 'loopback', array( 'status' => 'action-taken' ) );
289
+ self::$feedback->add_text( __( 'Identified loopback IP.', 'better-wp-security' ) );
290
+ }
291
  }
core/modules/ssl/active.php CHANGED
@@ -1,9 +1,7 @@
1
  <?php
2
 
3
- if ( is_admin() ) {
4
- require_once( 'class-itsec-ssl-admin.php' );
5
- $itsec_ssl_admin = new ITSEC_SSL_Admin();
6
- $itsec_ssl_admin->run( ITSEC_Core::get_instance() );
7
- }
8
 
9
- require_once( 'class-itsec-ssl.php' );
1
  <?php
2
 
3
+ require_once( dirname( __FILE__ ) . '/class-itsec-ssl-admin.php' );
4
+ $itsec_ssl_admin = new ITSEC_SSL_Admin();
5
+ $itsec_ssl_admin->run();
 
 
6
 
7
+ require_once( dirname( __FILE__ ) . '/class-itsec-ssl.php' );
core/modules/ssl/class-itsec-ssl-admin.php CHANGED
@@ -5,13 +5,47 @@ class ITSEC_SSL_Admin {
5
  $settings = ITSEC_Modules::get_settings( 'ssl' );
6
 
7
  if ( 'advanced' === $settings['require_ssl'] && 1 === $settings['frontend'] ) {
8
-
 
9
  add_action( 'post_submitbox_misc_actions', array( $this, 'ssl_enable_per_content' ) );
10
  add_action( 'save_post', array( $this, 'save_post' ) );
11
-
12
  }
13
  }
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  /**
16
  * Add checkbox to post meta for SSL
17
  *
5
  $settings = ITSEC_Modules::get_settings( 'ssl' );
6
 
7
  if ( 'advanced' === $settings['require_ssl'] && 1 === $settings['frontend'] ) {
8
+ add_action( 'init', array( $this, 'register_meta' ) );
9
+ add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor' ) );
10
  add_action( 'post_submitbox_misc_actions', array( $this, 'ssl_enable_per_content' ) );
11
  add_action( 'save_post', array( $this, 'save_post' ) );
 
12
  }
13
  }
14
 
15
+ /**
16
+ * Register the "Enable SSL" meta key.
17
+ */
18
+ public function register_meta() {
19
+ register_meta( 'post', 'itsec_enable_ssl', array(
20
+ 'single' => true,
21
+ 'type' => 'boolean',
22
+ 'sanitize_callback' => 'rest_sanitize_boolean',
23
+ 'show_in_rest' => array(
24
+ 'schema' => array(
25
+ 'type' => 'boolean',
26
+ 'context' => array( 'edit' ),
27
+ )
28
+ )
29
+ ) );
30
+ }
31
+
32
+ /**
33
+ * Enqueue the JS for the block editor to add the "Enable SSL" checkbox.
34
+ */
35
+ public function enqueue_block_editor() {
36
+ wp_enqueue_script( 'itsec-ssl-block-editor', plugins_url( 'js/block-editor.js', __FILE__ ), array(
37
+ 'wp-components',
38
+ 'wp-compose',
39
+ 'wp-element',
40
+ 'wp-edit-post',
41
+ 'wp-data',
42
+ 'wp-plugins',
43
+ ), 1, true );
44
+ wp_localize_script( 'itsec-ssl-block-editor', 'ITSECSSLBlockEditor', array(
45
+ 'enableSSL' => __( 'Enable SSL', 'better-wp-security' ),
46
+ ) );
47
+ }
48
+
49
  /**
50
  * Add checkbox to post meta for SSL
51
  *
core/modules/ssl/js/block-editor.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ( function( element, components, editPost, compose, data, plugins, l10n ) {
2
+ const el = element.createElement;
3
+ const META_KEY = 'itsec_enable_ssl';
4
+
5
+ function EnableSSL( props ) {
6
+ return el( editPost.PluginPostStatusInfo, { className: 'itsec-ssl' }, [
7
+ el( wp.components.CheckboxControl, {
8
+ checked : props.isEnabled,
9
+ label : l10n.enableSSL,
10
+ onChange: props.update,
11
+ key : 'enable-ssl',
12
+ } ),
13
+ ] );
14
+ }
15
+
16
+ plugins.registerPlugin( 'itsec-ssl', {
17
+ icon : 'hidden',
18
+ render: compose.compose( [
19
+ data.withSelect( function( select ) {
20
+ return {
21
+ isEnabled: ( select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {} )[ META_KEY ],
22
+ };
23
+ } ),
24
+ data.withDispatch( function( dispatch ) {
25
+ return {
26
+ update: function( isEnabled ) {
27
+ const edit = { meta: {} };
28
+ edit.meta[ META_KEY ] = isEnabled;
29
+
30
+ dispatch( 'core/editor' ).editPost( edit );
31
+ },
32
+ };
33
+ } ),
34
+ ] )( EnableSSL ),
35
+ } );
36
+
37
+ } )( wp.element, wp.components, wp.editPost, wp.compose, wp.data, wp.plugins, ITSECSSLBlockEditor );
core/modules/system-tweaks/config-generators.php CHANGED
@@ -81,7 +81,7 @@ final class ITSEC_System_Tweaks_Config_Generators {
81
 
82
  $rewrites .= "\n";
83
  $rewrites .= "\t\t# " . __( 'Disable PHP in Uploads - Security > Settings > System Tweaks > PHP in Uploads', 'better-wp-security' ) . "\n";
84
- $rewrites .= "\t\tRewriteRule ^$dir/.*\.(?:php[1-7]?|pht|phtml?|phps)$ - [NC,F]\n";
85
  }
86
  }
87
 
@@ -93,7 +93,7 @@ final class ITSEC_System_Tweaks_Config_Generators {
93
 
94
  $rewrites .= "\n";
95
  $rewrites .= "\t\t# " . __( 'Disable PHP in Plugins - Security > Settings > System Tweaks > PHP in Plugins', 'better-wp-security' ) . "\n";
96
- $rewrites .= "\t\tRewriteRule ^$dir/.*\.(?:php[1-7]?|pht|phtml?|phps)$ - [NC,F]\n";
97
  }
98
  }
99
 
@@ -105,7 +105,7 @@ final class ITSEC_System_Tweaks_Config_Generators {
105
 
106
  $rewrites .= "\n";
107
  $rewrites .= "\t\t# " . __( 'Disable PHP in Themes - Security > Settings > System Tweaks > PHP in Themes', 'better-wp-security' ) . "\n";
108
- $rewrites .= "\t\tRewriteRule ^$dir/.*\.(?:php[1-7]?|pht|phtml?|phps)$ - [NC,F]\n";
109
  }
110
  }
111
 
@@ -130,8 +130,8 @@ final class ITSEC_System_Tweaks_Config_Generators {
130
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} base64_decode\( [NC,OR]\n";
131
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} %24&x [NC,OR]\n";
132
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} 127\.0 [NC,OR]\n";
133
- $rewrites .= "\t\tRewriteCond %{QUERY_STRING} (globals|encode|localhost|loopback) [NC,OR]\n";
134
- $rewrites .= "\t\tRewriteCond %{QUERY_STRING} (concat|insert|union|declare) [NC,OR]\n";
135
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} %[01][0-9A-F] [NC]\n";
136
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} !^loggedout=true\n";
137
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} !^action=jetpack-sso\n";
@@ -259,8 +259,8 @@ final class ITSEC_System_Tweaks_Config_Generators {
259
  $modification .= "\tif ( \$args ~* \"base64_decode\(\" ) { set \$susquery 1; }\n";
260
  $modification .= "\tif ( \$args ~* \"%24&x\" ) { set \$susquery 1; }\n";
261
  $modification .= "\tif ( \$args ~* \"127\.0\" ) { set \$susquery 1; }\n";
262
- $modification .= "\tif ( \$args ~* \"(globals|encode|localhost|loopback)\" ) { set \$susquery 1; }\n";
263
- $modification .= "\tif ( \$args ~* \"(insert|concat|union|declare)\" ) { set \$susquery 1; }\n";
264
  $modification .= "\tif ( \$args ~* \"%[01][0-9A-F]\" ) { set \$susquery 1; }\n";
265
  $modification .= "\tif ( \$args ~ \"^loggedout=true\" ) { set \$susquery 0; }\n";
266
  $modification .= "\tif ( \$args ~ \"^action=jetpack-sso\" ) { set \$susquery 0; }\n";
81
 
82
  $rewrites .= "\n";
83
  $rewrites .= "\t\t# " . __( 'Disable PHP in Uploads - Security > Settings > System Tweaks > PHP in Uploads', 'better-wp-security' ) . "\n";
84
+ $rewrites .= "\t\tRewriteRule ^$dir/.*\.(?:php[1-7]?|pht|phtml?|phps)\\.?$ - [NC,F]\n";
85
  }
86
  }
87
 
93
 
94
  $rewrites .= "\n";
95
  $rewrites .= "\t\t# " . __( 'Disable PHP in Plugins - Security > Settings > System Tweaks > PHP in Plugins', 'better-wp-security' ) . "\n";
96
+ $rewrites .= "\t\tRewriteRule ^$dir/.*\.(?:php[1-7]?|pht|phtml?|phps)\\.?$ - [NC,F]\n";
97
  }
98
  }
99
 
105
 
106
  $rewrites .= "\n";
107
  $rewrites .= "\t\t# " . __( 'Disable PHP in Themes - Security > Settings > System Tweaks > PHP in Themes', 'better-wp-security' ) . "\n";
108
+ $rewrites .= "\t\tRewriteRule ^$dir/.*\.(?:php[1-7]?|pht|phtml?|phps)\\.?$ - [NC,F]\n";
109
  }
110
  }
111
 
130
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} base64_decode\( [NC,OR]\n";
131
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} %24&x [NC,OR]\n";
132
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} 127\.0 [NC,OR]\n";
133
+ $rewrites .= "\t\tRewriteCond %{QUERY_STRING} (^|\\W)(globals|encode|localhost|loopback)($|\\W) [NC,OR]\n";
134
+ $rewrites .= "\t\tRewriteCond %{QUERY_STRING} (^|\\W)(concat|insert|union|declare)($|\\W) [NC,OR]\n";
135
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} %[01][0-9A-F] [NC]\n";
136
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} !^loggedout=true\n";
137
  $rewrites .= "\t\tRewriteCond %{QUERY_STRING} !^action=jetpack-sso\n";
259
  $modification .= "\tif ( \$args ~* \"base64_decode\(\" ) { set \$susquery 1; }\n";
260
  $modification .= "\tif ( \$args ~* \"%24&x\" ) { set \$susquery 1; }\n";
261
  $modification .= "\tif ( \$args ~* \"127\.0\" ) { set \$susquery 1; }\n";
262
+ $modification .= "\tif ( \$args ~* \"(^|\\W)(globals|encode|localhost|loopback)($|\\W)\" ) { set \$susquery 1; }\n";
263
+ $modification .= "\tif ( \$args ~* \"(^|\\W)(insert|concat|union|declare)($|\\W)\" ) { set \$susquery 1; }\n";
264
  $modification .= "\tif ( \$args ~* \"%[01][0-9A-F]\" ) { set \$susquery 1; }\n";
265
  $modification .= "\tif ( \$args ~ \"^loggedout=true\" ) { set \$susquery 0; }\n";
266
  $modification .= "\tif ( \$args ~ \"^action=jetpack-sso\" ) { set \$susquery 0; }\n";
history.txt CHANGED
@@ -808,4 +808,21 @@
808
  Enhancement: Block access to git and svn repositories when System Tweaks -> Protect System Files is enabled.
809
  Tweak: Update jQuery Validation library to 1.17.0
810
  Bug Fix: Improve detection of blocking the File Change Scan from being scheduled if one is already being run.
811
- Bug Fix: Prevent infinite recursion error when trying to access directories outside of the allowed file tree.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
808
  Enhancement: Block access to git and svn repositories when System Tweaks -> Protect System Files is enabled.
809
  Tweak: Update jQuery Validation library to 1.17.0
810
  Bug Fix: Improve detection of blocking the File Change Scan from being scheduled if one is already being run.
811
+ Bug Fix: Prevent infinite recursion error when trying to access directories outside of the allowed file tree.
812
+ 7.3.0 - 2019-02-14 - Chris Jean & Timothy Jacobs
813
+ Enhancement: Add Per-Content SSL toggle to the upcoming Block Editor interface.
814
+ Enhancement: Add filter to the recipients list for email notifications: "itsec_notification_{$notification}_email_recipients" and "itsec_notification_email_recipients".
815
+ Enhancement: Add define "ITSEC_DISABLE_TEMP_WHITELIST" to disable the Temporary IP Whitelisting for logged-in administrators.
816
+ Enhancement: Improve redirecting after processing a login interstitial from a front-end login form.
817
+ Enhancement: Add loopback IP detection to Security Check.
818
+ Enhancement: Detect Server IPs in Security Check.
819
+ Tweak: Add additional safety checks when writing to system config files. This will log a "Critical Issue" when the writing of an empty or partial config file is detected and prevented.
820
+ Tweak: Improve File Change locking to help prevent failing scans on sites with inconsistent cron scheduling.
821
+ Tweak: Improve "System Tweaks – Suspicious Query Strings – SQLI" to reduce false positives.
822
+ Tweak: Improve "System Tweaks – Disable PHP" to block PHP files in apache configurations that serve files with a trailing dot.
823
+ Tweak: Remove "Seznam Bot" from HackRepair List as it isn't present in the latest version.
824
+ Bug Fix: Include Hide Backend token when emailing a password reset URL.
825
+ Bug Fix: Notification Center - Only send notifications to users with an exact role match of selected roles instead of a fuzzy match based on selected capabilities.
826
+ Bug Fix: Error when trying to edit reusable blocks with per-post SSL enabled.
827
+ Bug Fix: Resolve warnings on PHP 5.2.
828
+
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.8
6
- Stable tag: 7.2.0
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,23 @@ Free support may be available with the help of the community in the <a href="htt
189
 
190
  == Changelog ==
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  = 7.2.0 =
193
  * Enhancement: Allow for selecting the particular Proxy header a server is configured to use. Improve the language to indicate the importance of configuring this setting. H/t Filippo Cavallarin CEO at wearesegment.com
194
  * Enhancement: Block access to git and svn repositories when System Tweaks -> Protect System Files is enabled.
@@ -503,5 +520,5 @@ Free support may be available with the help of the community in the <a href="htt
503
 
504
  == Upgrade Notice ==
505
 
506
- = 7.2.0 =
507
- Version 7.2.0 contains important bug fixes and improvements. 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: 5.1.0
6
+ Stable tag: 7.3.0
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.3.0 =
193
+ * Enhancement: Add Per-Content SSL toggle to the upcoming Block Editor interface.
194
+ * Enhancement: Add filter to the recipients list for email notifications: "itsec_notification_{$notification}_email_recipients" and "itsec_notification_email_recipients".
195
+ * Enhancement: Add define "ITSEC_DISABLE_TEMP_WHITELIST" to disable the Temporary IP Whitelisting for logged-in administrators.
196
+ * Enhancement: Improve redirecting after processing a login interstitial from a front-end login form.
197
+ * Enhancement: Add loopback IP detection to Security Check.
198
+ * Enhancement: Detect Server IPs in Security Check.
199
+ * Tweak: Add additional safety checks when writing to system config files. This will log a "Critical Issue" when the writing of an empty or partial config file is detected and prevented.
200
+ * Tweak: Improve File Change locking to help prevent failing scans on sites with inconsistent cron scheduling.
201
+ * Tweak: Improve "System Tweaks – Suspicious Query Strings – SQLI" to reduce false positives.
202
+ * Tweak: Improve "System Tweaks – Disable PHP" to block PHP files in apache configurations that serve files with a trailing dot.
203
+ * Tweak: Remove "Seznam Bot" from HackRepair List as it isn't present in the latest version.
204
+ * Bug Fix: Include Hide Backend token when emailing a password reset URL.
205
+ * Bug Fix: Notification Center - Only send notifications to users with an exact role match of selected roles instead of a fuzzy match based on selected capabilities.
206
+ * Bug Fix: Error when trying to edit reusable blocks with per-post SSL enabled.
207
+ * Bug Fix: Resolve warnings on PHP 5.2.
208
+
209
  = 7.2.0 =
210
  * Enhancement: Allow for selecting the particular Proxy header a server is configured to use. Improve the language to indicate the importance of configuring this setting. H/t Filippo Cavallarin CEO at wearesegment.com
211
  * Enhancement: Block access to git and svn repositories when System Tweaks -> Protect System Files is enabled.
520
 
521
  == Upgrade Notice ==
522
 
523
+ = 7.3.0 =
524
+ Version 7.3.0 contains important bug fixes and improvements. It is recommended for all users.