iThemes Security (formerly Better WP Security) - Version 7.5.0

Version Description

  • Breaking Change: iThemes Security requires PHP 5.4 or later.
  • Enhancement: New Lockout Template screen.
  • Enhancement: Add confirmation button to Login Interstitial Async Actions when on a different device.
  • Enhancement: Add filter to "Lookup IP" link.
  • Developer Note: There were significant changes to the internals of the iThemes Security Lockout API in this release. If you are using the ITSEC_Lockout class directly, all the API functions will continue to work, but will emit deprecation notices when legacy behavior is being used. Please update any integrations.
  • Bug Fix: Brute Force module reporting invalid logins using an email address incorrectly.
  • Bug Fix: Improve lockout compatibility with caching plugins.
  • Bug Fix: Fix admin notice not being dismissed due to a REST API route that was more narrowly defined than necessary.
  • Bug Fix: Admin Notices list did not refresh after dismissing a notice.
  • Bug Fix: Strong Passwords zxcvbn Library was not evaluating penalty strings correctly.
  • Bug Fix: Fix PHP warning if there are multiple detected proxy headers.
Download this release

Release Info

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

Code changes from version 7.4.1 to 7.5.0

Files changed (61) hide show
  1. better-wp-security.php +12 -1
  2. core/core.php +2 -1
  3. core/history.txt +22 -0
  4. core/lib.php +139 -13
  5. core/lib/class-itsec-lib-email-confirmation.php +31 -0
  6. core/lib/class-itsec-lib-login-interstitial.php +115 -29
  7. core/lib/class-itsec-lib-login.php +58 -0
  8. core/lib/class-itsec-lib-opaque-tokens.php +116 -0
  9. core/lib/class-itsec-mail.php +15 -6
  10. core/lib/class-itsec-scheduler-cron.php +3 -1
  11. core/lib/class-itsec-scheduler-page-load.php +1 -0
  12. core/lib/class-itsec-scheduler.php +10 -1
  13. core/lib/fingerprinting/class-itsec-fingerprint.php +71 -2
  14. core/lib/itsec-zxcvbn-php/matchers/dictionary.php +3 -3
  15. core/lib/lockout/abstract-context.php +47 -0
  16. core/lib/lockout/class-host-context.php +120 -0
  17. core/lib/lockout/class-lockout.php +169 -0
  18. core/lib/lockout/class-user-context.php +33 -0
  19. core/lib/lockout/class-username-context.php +33 -0
  20. core/lib/lockout/execute-lock/abstract-context.php +83 -0
  21. core/lib/lockout/execute-lock/class-host-context.php +115 -0
  22. core/lib/lockout/execute-lock/class-user-context.php +40 -0
  23. core/lib/lockout/execute-lock/class-username-context.php +40 -0
  24. core/lib/lockout/execute-lock/index.php +1 -0
  25. core/lib/lockout/execute-lock/source/class-configurable.php +20 -0
  26. core/lib/lockout/execute-lock/source/class-lockout-module.php +20 -0
  27. core/lib/lockout/execute-lock/source/index.php +1 -0
  28. core/lib/lockout/execute-lock/source/interface-source.php +16 -0
  29. core/lib/lockout/index.php +1 -0
  30. core/lib/login-interstitial/abstract-itsec-login-interstitial.php +8 -2
  31. core/lib/login-interstitial/class-itsec-login-interstitial-session.php +61 -2
  32. core/lib/mail-templates/magic-link.html +991 -0
  33. core/lib/schema.php +11 -0
  34. core/lib/validator.php +5 -1
  35. core/lockout.php +565 -465
  36. core/modules/404-detection/class-itsec-four-oh-four.php +5 -3
  37. core/modules/brute-force/class-itsec-brute-force.php +23 -17
  38. core/modules/core/class-rest-core-admin-notices-controller.php +2 -2
  39. core/modules/core/img/security-ebook.png +0 -0
  40. core/modules/core/img/sync-logo.png +0 -0
  41. core/modules/core/sidebar-widget-mail-list-signup.php +1 -1
  42. core/modules/core/sidebar-widget-sync-cross-promo.php +4 -5
  43. core/modules/email-confirmation/active.php +4 -0
  44. core/modules/email-confirmation/class-itsec-email-confirmation.php +51 -0
  45. core/modules/email-confirmation/index.php +1 -0
  46. core/modules/global/settings-page.php +1 -1
  47. core/modules/global/settings.php +6 -0
  48. core/modules/ipcheck/class-itsec-ipcheck.php +14 -2
  49. core/modules/pro/settings-page.php +15 -0
  50. core/modules/security-check/scanner.php +2 -0
  51. core/modules/system-tweaks/class-itsec-system-tweaks.php +4 -0
  52. core/package.json +3 -1
  53. core/templates/index.php +1 -0
  54. core/templates/lockout/icon.svg +1 -0
  55. core/templates/lockout/index.php +1 -0
  56. core/templates/lockout/lamp-light.svg +1 -0
  57. core/templates/lockout/lockout.css +163 -0
  58. core/templates/lockout/lockout.php +36 -0
  59. history.txt +12 -0
  60. package.json +3 -1
  61. readme.txt +18 -5
better-wp-security.php CHANGED
@@ -6,12 +6,23 @@
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.4.1
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' ) ) {
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.5.0
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
13
+ * Requires PHP: 5.4
14
  */
15
 
16
+ if ( version_compare( phpversion(), '5.4.0', '<' ) ) {
17
+ function itsec_free_minimum_php_version_notice() {
18
+ echo '<div class="notice notice-error"><p>' . esc_html__( 'iThemes Security requires PHP 5.4 or higher.', 'better-wp-security' ) . '</p></div>';
19
+ }
20
+
21
+ add_action( 'admin_notices', 'itsec_free_minimum_php_version_notice' );
22
+
23
+ return;
24
+ }
25
+
26
  function itsec_load_textdomain() {
27
 
28
  if ( function_exists( 'determine_locale' ) ) {
core/core.php CHANGED
@@ -24,7 +24,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
24
  *
25
  * @access private
26
  */
27
- private $plugin_build = 4114;
28
 
29
  /**
30
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
@@ -318,6 +318,7 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
318
  ITSEC_Modules::register_module( 'database-prefix', "$path/modules/database-prefix", 'always-active' );
319
  ITSEC_Modules::register_module( 'backup', "$path/modules/backup", 'default-active' );
320
  ITSEC_Modules::register_module( 'core', "$path/modules/core", 'always-active' );
 
321
  ITSEC_Modules::register_module( 'file-change', "$path/modules/file-change" );
322
  ITSEC_Modules::register_module( 'file-permissions', "$path/modules/file-permissions", 'always-active' );
323
  ITSEC_Modules::register_module( 'hide-backend', "$path/modules/hide-backend", 'always-active' );
24
  *
25
  * @access private
26
  */
27
+ private $plugin_build = 4115;
28
 
29
  /**
30
  * Used to distinguish between a user modifying settings and the API modifying settings (such as from Sync
318
  ITSEC_Modules::register_module( 'database-prefix', "$path/modules/database-prefix", 'always-active' );
319
  ITSEC_Modules::register_module( 'backup', "$path/modules/backup", 'default-active' );
320
  ITSEC_Modules::register_module( 'core', "$path/modules/core", 'always-active' );
321
+ ITSEC_Modules::register_module( 'email-confirmation', "$path/modules/email-confirmation", 'always-active' );
322
  ITSEC_Modules::register_module( 'file-change', "$path/modules/file-change" );
323
  ITSEC_Modules::register_module( 'file-permissions', "$path/modules/file-permissions", 'always-active' );
324
  ITSEC_Modules::register_module( 'hide-backend', "$path/modules/hide-backend", 'always-active' );
core/history.txt CHANGED
@@ -811,3 +811,25 @@
811
  Bug Fix: Hide Backend Bypass.
812
  Bug Fix: Strict Standards error during Sync request.
813
  Bug Fix: wp_die() if a login interstitial session fails to be created instead of throwing a fatal error.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
  Bug Fix: Hide Backend Bypass.
812
  Bug Fix: Strict Standards error during Sync request.
813
  Bug Fix: wp_die() if a login interstitial session fails to be created instead of throwing a fatal error.
814
+ 5.3.0 - 2019-09-04 - Timothy Jacobs
815
+ Breaking Change: iThemes Security requires PHP 5.4 or later.
816
+ Enhancement: New Lockout Template screen.
817
+ Bug Fix: Brute Force module reporting invalid logins using an email address incorrectly.
818
+ Developer Note: There were significant changes to the internals of the iThemes Security Lockout API in this release. If you are using the ITSEC_Lockout class directly, all the API functions will continue to work, but will emit deprecation notices.
819
+ 5.3.1 - 2019-09-05 - Timothy Jacobs
820
+ Bug Fix: PHP Warning while logging interstitial updates.
821
+ 5.3.2 - 2019-09-09 - Timothy Jacobs
822
+ Enhancement: Add confirmation button to Login Interstitial Async Actions when on a different device.
823
+ Bug Fix: Fix admin notice not being dismissed due to a REST API route that was more narrowly defined than necessary.
824
+ 5.3.3 - 2019-10-01 - Timothy Jacobs
825
+ Bug Fix: Strong Passwords zxcvbn Library was not evaluating penalty strings correctly.
826
+ 5.4.0 - 2019-10-29 - Timothy Jacobs
827
+ Enhancement: Add filter to "Lookup IP" link.
828
+ Bug Fix: PHP warning when inserting lockouts.
829
+ 5.4.1 - 2019-11-12 - Timothy Jacobs
830
+ Bug Fix: Improve lockout compatibility with caching plugins.
831
+ Bug Fix: Admin Notices list did not refresh after dismissing a notice.
832
+ Bug Fix: Fix PHP warning if there are multiple detected proxy headers.
833
+ 5.4.2 - 2019-11-14 - Timothy Jacobs
834
+ Tweak: Add stub Passwordless Login settings page.
835
+ Bug Fix: PHP warning if lockout_active field is missing.
core/lib.php CHANGED
@@ -5,9 +5,9 @@
5
  *
6
  * Various static functions to provide information to modules and other areas throughout the plugin.
7
  *
 
8
  * @package iThemes_Security
9
  *
10
- * @since 4.0.0
11
  */
12
  final class ITSEC_Lib {
13
  /**
@@ -274,9 +274,9 @@ final class ITSEC_Lib {
274
  /**
275
  * Determines whether a given IP address is whiteliste
276
  *
277
- * @param string $ip ip to check (can be in CIDR notation)
278
- * @param array $whitelisted_ips ip list to compare to if not yet saved to options
279
- * @param boolean $current whether to whitelist the current ip or not (due to saving, etc)
280
  *
281
  * @return boolean true if whitelisted or false
282
  */
@@ -521,6 +521,56 @@ final class ITSEC_Lib {
521
  echo "<div class=\"error inline\"><p><strong>$message</strong></p></div>\n";
522
  }
523
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  /**
525
  * Get a WordPress user object.
526
  *
@@ -549,9 +599,9 @@ final class ITSEC_Lib {
549
  $type = gettype( $user );
550
  }
551
 
552
- trigger_error( "ITSEC_Lib::get_user() called with an invalid \$user argument. Received \$user variable of type: $type", E_USER_ERROR );
553
 
554
- return false;
555
  }
556
 
557
  if ( $user instanceof WP_User ) {
@@ -587,10 +637,12 @@ final class ITSEC_Lib {
587
  */
588
  public static function get_trace_ip_link( $ip = false ) {
589
  if ( empty( $ip ) ) {
590
- return 'https://www.iptrackeronline.com/ithemes.php';
591
  } else {
592
- return 'http://www.iptrackeronline.com/ithemes.php?ip_address=' . urlencode( $ip );
593
  }
 
 
594
  }
595
 
596
  /**
@@ -1189,7 +1241,7 @@ final class ITSEC_Lib {
1189
  *
1190
  * @return array
1191
  */
1192
- public static function array_insert_before( $key, $array, $new_key, $new_value) {
1193
  if ( array_key_exists( $key, $array ) ) {
1194
  $new = array();
1195
  foreach ( $array as $k => $value ) {
@@ -1272,6 +1324,9 @@ final class ITSEC_Lib {
1272
  /**
1273
  * Parse a complex header that has attributes like quality values.
1274
  *
 
 
 
1275
  * @example Parsing the Accept-Language header.
1276
  *
1277
  * "en-US,en;q=0.9,de;q=0.8" transforms to:
@@ -1282,9 +1337,6 @@ final class ITSEC_Lib {
1282
  * 'de' => [ 'q' => 0.8' ],
1283
  * ]
1284
  *
1285
- * @param string $header
1286
- *
1287
- * @return string[]
1288
  */
1289
  public static function parse_header_with_attributes( $header ) {
1290
 
@@ -1444,7 +1496,7 @@ final class ITSEC_Lib {
1444
  return false;
1445
  }
1446
 
1447
- return hash_equals( self::hash_token( $provided_token ), $hashed_token );
1448
  }
1449
 
1450
  /**
@@ -1756,4 +1808,78 @@ final class ITSEC_Lib {
1756
  public static function load( $name ) {
1757
  require_once( dirname( __FILE__ ) . "/lib/class-itsec-lib-{$name}.php" );
1758
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1759
  }
5
  *
6
  * Various static functions to provide information to modules and other areas throughout the plugin.
7
  *
8
+ * @since 4.0.0
9
  * @package iThemes_Security
10
  *
 
11
  */
12
  final class ITSEC_Lib {
13
  /**
274
  /**
275
  * Determines whether a given IP address is whiteliste
276
  *
277
+ * @param string $ip ip to check (can be in CIDR notation)
278
+ * @param array $whitelisted_ips ip list to compare to if not yet saved to options
279
+ * @param boolean $current whether to whitelist the current ip or not (due to saving, etc)
280
  *
281
  * @return boolean true if whitelisted or false
282
  */
521
  echo "<div class=\"error inline\"><p><strong>$message</strong></p></div>\n";
522
  }
523
 
524
+ /**
525
+ * Print a WP core notice styled inline.
526
+ *
527
+ * @param string|WP_Error $message
528
+ * @param string $type
529
+ *
530
+ * @return string
531
+ */
532
+ public static function inline_styled_notice( $message, $type = 'error' ) {
533
+ switch ( $type ) {
534
+ case 'error':
535
+ $bkg = '#dc3232';
536
+ $bdr = '#fbeaea';
537
+ break;
538
+ case 'warning':
539
+ $bkg = '#fff8e5';
540
+ $bdr = '#ffb900';
541
+ break;
542
+ case 'info':
543
+ $bkg = '#e5f5fa';
544
+ $bdr = '#00a0d2';
545
+ break;
546
+ case 'success':
547
+ default:
548
+ $bkg = '#ecf7ed';
549
+ $bdr = '#46b450';
550
+ break;
551
+ }
552
+
553
+ if ( is_wp_error( $message ) ) {
554
+ $messages = array();
555
+
556
+ foreach ( $message->get_error_codes() as $code ) {
557
+ foreach ( $message->get_error_messages( $code ) as $str ) {
558
+ $messages[] = $str;
559
+ }
560
+ }
561
+
562
+ $message = wp_sprintf( '%l', $messages );
563
+ }
564
+
565
+ $html = "<div style=\"background: {$bkg};border-left: 4px solid {$bdr};padding: 1px 12px; margin: 5px 0 15px;\">";
566
+ $html .= '<p style="margin: 0.5em 6px 0.5em 0;padding: 2px;vertical-align: bottom;">';
567
+ $html .= is_wp_error( $message ) ? $message->get_error_message() : $message;
568
+ $html .= '</p>';
569
+ $html .= '</div>';
570
+
571
+ return $html;
572
+ }
573
+
574
  /**
575
  * Get a WordPress user object.
576
  *
599
  $type = gettype( $user );
600
  }
601
 
602
+ error_log( 'ITSEC_Lib::get_user() called with an invalid $user argument. Received $user variable of type: ' . $type );
603
 
604
+ wp_die( 'Internal Server Error' );
605
  }
606
 
607
  if ( $user instanceof WP_User ) {
637
  */
638
  public static function get_trace_ip_link( $ip = false ) {
639
  if ( empty( $ip ) ) {
640
+ $link = 'https://www.iptrackeronline.com/ithemes.php';
641
  } else {
642
+ $link = 'http://www.iptrackeronline.com/ithemes.php?ip_address=' . urlencode( $ip );
643
  }
644
+
645
+ return apply_filters( 'itsec_ip_details_link', $link, $ip );
646
  }
647
 
648
  /**
1241
  *
1242
  * @return array
1243
  */
1244
+ public static function array_insert_before( $key, $array, $new_key, $new_value ) {
1245
  if ( array_key_exists( $key, $array ) ) {
1246
  $new = array();
1247
  foreach ( $array as $k => $value ) {
1324
  /**
1325
  * Parse a complex header that has attributes like quality values.
1326
  *
1327
+ * @param string $header
1328
+ *
1329
+ * @return string[]
1330
  * @example Parsing the Accept-Language header.
1331
  *
1332
  * "en-US,en;q=0.9,de;q=0.8" transforms to:
1337
  * 'de' => [ 'q' => 0.8' ],
1338
  * ]
1339
  *
 
 
 
1340
  */
1341
  public static function parse_header_with_attributes( $header ) {
1342
 
1496
  return false;
1497
  }
1498
 
1499
+ return hash_equals( $hashed_token, self::hash_token( $provided_token ) );
1500
  }
1501
 
1502
  /**
1808
  public static function load( $name ) {
1809
  require_once( dirname( __FILE__ ) . "/lib/class-itsec-lib-{$name}.php" );
1810
  }
1811
+
1812
+ /**
1813
+ * Combine multiple WP_Error instances.
1814
+ *
1815
+ * @param WP_Error|null ...$errors
1816
+ *
1817
+ * @return WP_Error
1818
+ */
1819
+ public static function combine_wp_error( $errors ) {
1820
+ $combined = new WP_Error();
1821
+
1822
+ self::add_to_wp_error( $combined, $errors );
1823
+
1824
+ return $combined;
1825
+ }
1826
+
1827
+ /**
1828
+ * Add the subsequent WP Error data to the first WP Error instance.
1829
+ *
1830
+ * @param WP_Error $add_to
1831
+ * @param WP_Error[]|null[] ...$errors
1832
+ */
1833
+ public static function add_to_wp_error( WP_Error $add_to, $errors ) {
1834
+ if ( ! is_array( $errors ) ) {
1835
+ $errors = func_get_args();
1836
+ array_shift( $errors );
1837
+ }
1838
+
1839
+ foreach ( $errors as $error ) {
1840
+ if ( $error ) {
1841
+ foreach ( $error->get_error_codes() as $code ) {
1842
+ $add_to->add( $code, $error->get_error_message( $code ) );
1843
+ }
1844
+ }
1845
+ }
1846
+ }
1847
+
1848
+ /**
1849
+ * Render a file with only the given vars in context.
1850
+ *
1851
+ * @param string $file
1852
+ * @param array $context
1853
+ * @param bool $echo
1854
+ *
1855
+ * @return string|void
1856
+ */
1857
+ public static function render( $file, $context = array(), $echo = true ) {
1858
+ $__echo = $echo;
1859
+ $__file = $file;
1860
+
1861
+ extract( $context, EXTR_OVERWRITE );
1862
+ unset( $file, $context, $echo );
1863
+
1864
+ if ( ! $__echo ) {
1865
+ ob_start();
1866
+ }
1867
+
1868
+ require( $__file );
1869
+
1870
+ if ( ! $__echo ) {
1871
+ return ob_get_clean() ?: '';
1872
+ }
1873
+ }
1874
+
1875
+ /**
1876
+ * Utility to mark this page as not cacheable.
1877
+ */
1878
+ public static function no_cache() {
1879
+ nocache_headers();
1880
+
1881
+ if ( ! defined( 'DONOTCACHEPAGE' ) ) {
1882
+ define( 'DONOTCACHEPAGE', true );
1883
+ }
1884
+ }
1885
  }
core/lib/class-itsec-lib-email-confirmation.php ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_Lib_Email_Confirmation {
4
+
5
+ const META_KEY = '_itsec_email_confirmed';
6
+
7
+ /**
8
+ * Is the email address confirmed for the given user.
9
+ *
10
+ * @param WP_User|int $user
11
+ *
12
+ * @return bool
13
+ */
14
+ public static function is_email_confirmed( $user ) {
15
+ $user = ITSEC_Lib::get_user( $user );
16
+
17
+ return (bool) get_user_meta( $user->ID, self::META_KEY, true );
18
+ }
19
+
20
+ /**
21
+ * Mark the email address for the given user as confirmed.
22
+ *
23
+ * @param WP_User|int $user
24
+ * @param bool $confirmed
25
+ */
26
+ public static function mark_email_as_confirmed( $user, $confirmed = true ) {
27
+ $user = ITSEC_Lib::get_user( $user );
28
+
29
+ update_user_meta( $user->ID, self::META_KEY, $confirmed );
30
+ }
31
+ }
core/lib/class-itsec-lib-login-interstitial.php CHANGED
@@ -19,6 +19,7 @@ class ITSEC_Lib_Login_Interstitial {
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';
@@ -153,6 +154,8 @@ class ITSEC_Lib_Login_Interstitial {
153
  * @api
154
  *
155
  * @param ITSEC_Login_Interstitial_Session $session
 
 
156
  */
157
  public function proceed_to_next( ITSEC_Login_Interstitial_Session $session ) {
158
 
@@ -160,6 +163,7 @@ class ITSEC_Lib_Login_Interstitial {
160
  $session->add_completed_interstitial( $current );
161
 
162
  $session->set_current_interstitial( $this->get_next_interstitial( $session ) );
 
163
  return $session->save();
164
  }
165
 
@@ -209,6 +213,13 @@ class ITSEC_Lib_Login_Interstitial {
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
  /**
@@ -243,7 +254,7 @@ class ITSEC_Lib_Login_Interstitial {
243
  wp_die( $session );
244
  }
245
 
246
- $this->build_session_from_global_state( $session );
247
 
248
  if ( isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ], $this->registered[ $_REQUEST[ self::SHOW_AFTER_LOGIN ] ] ) ) {
249
  $session->add_show_after( $_REQUEST[ self::SHOW_AFTER_LOGIN ] );
@@ -261,37 +272,12 @@ class ITSEC_Lib_Login_Interstitial {
261
  wp_die( $session );
262
  }
263
 
264
- $this->build_session_from_global_state( $session );
265
  $session->save();
266
  $this->show_interstitial( $session );
267
  }
268
  }
269
 
270
- /**
271
- * Build up the interstitial session configuration from the global state.
272
- *
273
- * This does not set the show afters because this is also used by the show after code.
274
- *
275
- * @param ITSEC_Login_Interstitial_Session $session
276
- */
277
- private function build_session_from_global_state( ITSEC_Login_Interstitial_Session $session ) {
278
- if ( isset( $_REQUEST['interim-login'] ) ) {
279
- $session->set_interim_login();
280
- }
281
-
282
- if ( ! empty( $_REQUEST['redirect_to'] ) ) {
283
- $session->set_redirect_to( $_REQUEST['redirect_to'] );
284
- } elseif ( ! did_action( 'login_init' ) && ( $ref = wp_get_referer() ) ) {
285
- $session->set_redirect_to( $ref );
286
- } elseif ( ! did_action( 'login_init' ) ) {
287
- $session->set_redirect_to( $_SERVER['REQUEST_URI'] );
288
- }
289
-
290
- if ( ! empty( $_REQUEST['rememberme'] ) ) {
291
- $session->set_remember_me();
292
- }
293
- }
294
-
295
  /**
296
  * Add a message that the interstitial expired.
297
  *
@@ -468,6 +454,12 @@ class ITSEC_Lib_Login_Interstitial {
468
  $this->display_wp_login_message( new WP_Error( 'unsupported', esc_html__( 'Unsupported Interstitial. Please login again.', 'better-wp-security' ) ) );
469
  }
470
 
 
 
 
 
 
 
471
  $args = array(
472
  'same_browser' => false,
473
  );
@@ -480,7 +472,12 @@ class ITSEC_Lib_Login_Interstitial {
480
  }
481
 
482
  $this->current_session = $session;
483
- $result = $interstitial->handle_async_action( $session, $action, $args );
 
 
 
 
 
484
 
485
  if ( null === $result ) {
486
  $this->display_wp_login_message( new WP_Error( 'unsupported', esc_html__( 'Unsupported Interstitial. Please login again.', 'better-wp-security' ) ) );
@@ -494,7 +491,7 @@ class ITSEC_Lib_Login_Interstitial {
494
  $result = array();
495
  }
496
 
497
- if ( $args['same_browser'] && empty( $args['allow_same_browser'] ) ) {
498
  $this->do_next_step( $session, array(
499
  'delete' => false,
500
  'allow_interim' => false,
@@ -719,6 +716,95 @@ class ITSEC_Lib_Login_Interstitial {
719
  die;
720
  }
721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
722
  /**
723
  * Do the next step for a session.
724
  *
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
+ const R_SAME_BROWSER_DENY = 'itsec_interstitial_browser_deny';
23
 
24
  const C_SAME_BROWSER = 'itsec_interstitial_browser';
25
  const SAME_BROWSER_PAYLOAD = 'same-browser';
154
  * @api
155
  *
156
  * @param ITSEC_Login_Interstitial_Session $session
157
+ *
158
+ * @return bool
159
  */
160
  public function proceed_to_next( ITSEC_Login_Interstitial_Session $session ) {
161
 
163
  $session->add_completed_interstitial( $current );
164
 
165
  $session->set_current_interstitial( $this->get_next_interstitial( $session ) );
166
+
167
  return $session->save();
168
  }
169
 
213
  */
214
  public function initialize_same_browser( ITSEC_Login_Interstitial_Session $session ) {
215
  ITSEC_Lib::set_cookie( self::C_SAME_BROWSER, $session->get_signature_for_payload( self::SAME_BROWSER_PAYLOAD ) );
216
+
217
+ /**
218
+ * Fires when the login interstitial initializes the Same Browser API for async actions.
219
+ *
220
+ * @param ITSEC_Login_Interstitial_Session $session
221
+ */
222
+ do_action( 'itsec_login_interstitial_initialize_same_browser', $session );
223
  }
224
 
225
  /**
254
  wp_die( $session );
255
  }
256
 
257
+ $session->initialize_from_global_state();
258
 
259
  if ( isset( $_REQUEST[ self::SHOW_AFTER_LOGIN ], $this->registered[ $_REQUEST[ self::SHOW_AFTER_LOGIN ] ] ) ) {
260
  $session->add_show_after( $_REQUEST[ self::SHOW_AFTER_LOGIN ] );
272
  wp_die( $session );
273
  }
274
 
275
+ $session->initialize_from_global_state();
276
  $session->save();
277
  $this->show_interstitial( $session );
278
  }
279
  }
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  /**
282
  * Add a message that the interstitial expired.
283
  *
454
  $this->display_wp_login_message( new WP_Error( 'unsupported', esc_html__( 'Unsupported Interstitial. Please login again.', 'better-wp-security' ) ) );
455
  }
456
 
457
+ if ( isset( $_REQUEST[ self::R_SAME_BROWSER_DENY ] ) ) {
458
+ $session->delete();
459
+ wp_redirect( wp_login_url() );
460
+ die;
461
+ }
462
+
463
  $args = array(
464
  'same_browser' => false,
465
  );
472
  }
473
 
474
  $this->current_session = $session;
475
+
476
+ if ( ! $args['same_browser'] && 'GET' === $_SERVER['REQUEST_METHOD'] ) {
477
+ $this->display_async_action_confirmation( $session, $action );
478
+ }
479
+
480
+ $result = $interstitial->handle_async_action( $session, $action, $args );
481
 
482
  if ( null === $result ) {
483
  $this->display_wp_login_message( new WP_Error( 'unsupported', esc_html__( 'Unsupported Interstitial. Please login again.', 'better-wp-security' ) ) );
491
  $result = array();
492
  }
493
 
494
+ if ( $args['same_browser'] && empty( $result['allow_same_browser'] ) ) {
495
  $this->do_next_step( $session, array(
496
  'delete' => false,
497
  'allow_interim' => false,
716
  die;
717
  }
718
 
719
+ /**
720
+ * Display a confirmation button for an async action.
721
+ *
722
+ * @param ITSEC_Login_Interstitial_Session $session
723
+ * @param string $action
724
+ */
725
+ private function display_async_action_confirmation( ITSEC_Login_Interstitial_Session $session, $action ) {
726
+ if ( ! function_exists( 'login_header' ) ) {
727
+ require_once( dirname( __FILE__ ) . '/includes/function.login-header.php' );
728
+ }
729
+
730
+ $form_action = $this->get_async_action_url( $session, $action );
731
+
732
+ login_header();
733
+ ?>
734
+ <style type="text/css">
735
+ .login h2 {
736
+ margin-bottom: 10px;
737
+ font-size: 14px;
738
+ }
739
+
740
+ .itsec-login-interstitial-confirm-async-action {
741
+ vertical-align: top;
742
+ display: block;
743
+ text-decoration: none;
744
+ height: 28px;
745
+ margin: 0 0 15px 0;
746
+ cursor: pointer;
747
+ -webkit-appearance: none;
748
+ border-radius: 3px;
749
+ white-space: nowrap;
750
+ box-sizing: border-box;
751
+ background: #0083E3;
752
+ color: #fff;
753
+ text-shadow: none;
754
+ padding: 20px 30px;
755
+ line-height: 0;
756
+ box-shadow: none;
757
+ font-weight: 300;
758
+ font-size: 1.2em;
759
+ border: none;
760
+ width: 100%;
761
+ text-align: center;
762
+ }
763
+ .itsec-login-interstitial-confirm-async-action:last-child {
764
+ margin-bottom: 0;
765
+ }
766
+ .itsec-login-interstitial-confirm-async-action:hover,
767
+ .itsec-login-interstitial-confirm-async-action:focus {
768
+ background: #006799;
769
+ color: #fff;
770
+ }
771
+ .itsec-login-interstitial-confirm-async-action.itsec-login-interstitial-confirm-async-action--deny {
772
+ background: #d54e21;
773
+ }
774
+ .itsec-login-interstitial-confirm-async-action.itsec-login-interstitial-confirm-async-action--deny:hover,
775
+ .itsec-login-interstitial-confirm-async-action.itsec-login-interstitial-confirm-async-action--deny:focus {
776
+ background: #983818;
777
+ }
778
+ </style>
779
+ <form action="<?php echo esc_url( $form_action ); ?>" method="post" autocomplete="off">
780
+
781
+ <h2><?php esc_html_e( 'Please Verify the Login Request', 'better-wp-security' ); ?></h2>
782
+
783
+ <?php do_action( 'itsec_login_interstitial_async_action_confirmation_before_confirm', $session, $action ); ?>
784
+
785
+ <button class="itsec-login-interstitial-confirm-async-action">
786
+ <?php esc_html_e( 'Confirm Login', 'better-wp-security' ); ?>
787
+ </button>
788
+ <button name="<?php echo esc_attr( self::R_SAME_BROWSER_DENY ); ?>" class="itsec-login-interstitial-confirm-async-action itsec-login-interstitial-confirm-async-action--deny">
789
+ <?php esc_html_e( 'Deny Login', 'better-wp-security' ); ?>
790
+ </button>
791
+ </form>
792
+
793
+ <p id="backtoblog">
794
+ <a href="<?php echo esc_url( home_url( '/' ) ); ?>" title="<?php esc_attr_e( 'Are you lost?', 'better-wp-security' ); ?>">
795
+ <?php echo esc_html( sprintf( __( '&larr; Back to %s', 'better-wp-security' ), get_bloginfo( 'title', 'display' ) ) ); ?>
796
+ </a>
797
+ </p>
798
+
799
+ </div>
800
+ <?php do_action( 'login_footer' ); ?>
801
+ <div class="clear"></div>
802
+ </body>
803
+ </html>
804
+ <?php
805
+ die;
806
+ }
807
+
808
  /**
809
  * Do the next step for a session.
810
  *
core/lib/class-itsec-lib-login.php ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_Lib_Login {
4
+ /**
5
+ * Get a user account by the user's provided identifier.
6
+ *
7
+ * @param string $identifier
8
+ *
9
+ * @return WP_User|null
10
+ */
11
+ public static function get_user( $identifier ) {
12
+ foreach ( self::get_user_lookup_fields() as $field ) {
13
+ if ( $user = get_user_by( $field, $identifier ) ) {
14
+ return $user;
15
+ }
16
+ }
17
+
18
+ return null;
19
+ }
20
+
21
+ /**
22
+ * Get the fields a user can provide to identify their user account.
23
+ *
24
+ * @return array
25
+ */
26
+ public static function get_user_lookup_fields() {
27
+ $fields = array( 'login', 'email' );
28
+
29
+ if ( ITSEC_Modules::is_active( 'wordpress-tweaks' ) ) {
30
+ if ( 'email' === ITSEC_Modules::get_setting( 'wordpress-tweaks', 'valid_user_login_type' ) ) {
31
+ $fields = array( 'email' );
32
+ } elseif ( 'username' === ITSEC_Modules::get_setting( 'wordpress-tweaks', 'valid_user_login_type' ) ) {
33
+ $fields = array( 'login' );
34
+ }
35
+ }
36
+
37
+ return $fields;
38
+ }
39
+
40
+ /**
41
+ * Get the input label for the lookup field.
42
+ *
43
+ * @return string
44
+ */
45
+ public static function get_user_lookup_fields_label() {
46
+ $fields = self::get_user_lookup_fields();
47
+
48
+ if ( count( $fields ) === 2 ) {
49
+ return esc_html__( 'Username or Email Address', 'better-wp-security' );
50
+ }
51
+
52
+ if ( 'email' === $fields[0] ) {
53
+ return esc_html__( 'Email Address', 'better-wp-security' );
54
+ }
55
+
56
+ return esc_html__( 'Username', 'better-wp-security' );
57
+ }
58
+ }
core/lib/class-itsec-lib-opaque-tokens.php ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_Lib_Opaque_Tokens {
4
+ const TTL = 1800; // 30 minutes
5
+ const MAX_TTL = 604800; // Maximum TTL for tokens.
6
+
7
+ const E_INVALID = 'itsec-opaque-token-invalid';
8
+ const E_MISSING = 'itsec-opaque-token-not-found';
9
+ const E_EXPIRED = 'itsec-opaque-token-expired';
10
+ const E_HASH_FAILED = 'itsec-opaque-token-hash-failed';
11
+
12
+ /**
13
+ * Verify that a token is valid and get it's data.
14
+ *
15
+ * @param string $type
16
+ * @param string $token
17
+ * @param int $ttl
18
+ *
19
+ * @return array|WP_Error
20
+ */
21
+ public static function verify_and_get_token_data( $type, $token, $ttl = self::TTL ) {
22
+ global $wpdb;
23
+
24
+ if ( $ttl > self::MAX_TTL ) {
25
+ _doing_it_wrong( __METHOD__, sprintf( 'Token ttl must not be greater than %d seconds.', self::MAX_TTL ), '5.3.0' );
26
+
27
+ return new WP_Error( self::E_INVALID, __( 'Invalid token.', 'better-wp-security' ) );
28
+ }
29
+
30
+ $token = base64_decode( $token );
31
+
32
+ if ( strpos( $token, '|' ) === false ) {
33
+ return new WP_Error( self::E_INVALID, __( 'Invalid token.', 'better-wp-security' ) );
34
+ }
35
+
36
+ list( $id, $unhashed ) = explode( '|', $token );
37
+
38
+ $data = $wpdb->get_row( $wpdb->prepare(
39
+ "SELECT * FROM {$wpdb->base_prefix}itsec_opaque_tokens WHERE token_id = %s AND token_type = %s LIMIT 1",
40
+ $id,
41
+ $type
42
+ ), ARRAY_A );
43
+
44
+ if ( ! $data ) {
45
+ return new WP_Error( self::E_MISSING, __( 'Token not found.', 'better-wp-security' ) );
46
+ }
47
+
48
+ if ( $data['token_created_at'] + $ttl > ITSEC_Core::get_current_time_gmt() ) {
49
+ self::delete_token( $token );
50
+
51
+ return new WP_Error( self::E_EXPIRED, __( 'Token expired.', 'better-wp-security' ) );
52
+ }
53
+
54
+ if ( ! ITSEC_Lib::verify_token( $unhashed, $data['token_hashed'] ) ) {
55
+ return new WP_Error( self::E_INVALID, __( 'Invalid token.', 'better-wp-security' ) );
56
+ }
57
+
58
+ return json_decode( $data['token_data'], true );
59
+ }
60
+
61
+ /**
62
+ * Create a new token.
63
+ *
64
+ * @param string $type
65
+ * @param array $data
66
+ *
67
+ * @return string|WP_Error
68
+ */
69
+ public static function create_token( $type, array $data ) {
70
+ global $wpdb;
71
+
72
+ $id = ITSEC_Lib::generate_token();
73
+ $token = ITSEC_Lib::generate_token();
74
+ $hashed = ITSEC_Lib::hash_token( $token );
75
+
76
+ if ( ! $id || ! $token || ! $hashed ) {
77
+ return new WP_Error( self::E_HASH_FAILED, __( 'Failed to generate token and hash.', 'better-wp-security' ) );
78
+ }
79
+
80
+ $wpdb->insert( $wpdb->base_prefix . 'itsec_opaque_tokens', array(
81
+ 'token_id' => $id,
82
+ 'token_hashed' => $hashed,
83
+ 'token_type' => $type,
84
+ 'token_data' => wp_json_encode( $data ),
85
+ 'token_created_at' => gmdate( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
86
+ ) );
87
+
88
+ return base64_encode( "{$id}|{$token}" );
89
+ }
90
+
91
+ /**
92
+ * Delete a token.
93
+ *
94
+ * @param string $token
95
+ */
96
+ public static function delete_token( $token ) {
97
+ global $wpdb;
98
+
99
+ $token = base64_decode( $token );
100
+ list( $id ) = explode( '|', $token );
101
+
102
+ $wpdb->delete( $wpdb->base_prefix . 'itsec_opaque_tokens', array( 'token_id' => $id ) );
103
+ }
104
+
105
+ /**
106
+ * Delete tokens that are more than 7 days old.
107
+ */
108
+ public static function delete_expired_tokens() {
109
+ global $wpdb;
110
+
111
+ $wpdb->query( $wpdb->prepare(
112
+ "DELETE FROM {$wpdb->base_prefix}itsec_opaque_tokens WHERE token_created_at < %s",
113
+ gmdate( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - self::MAX_TTL )
114
+ ) );
115
+ }
116
+ }
core/lib/class-itsec-mail.php CHANGED
@@ -272,6 +272,9 @@ final class ITSEC_Mail {
272
  if ( 'user' === $lockout['type'] ) {
273
  /* translators: 1: Username */
274
  $lockout['description'] = sprintf( wp_kses( __( '<b>User:</b> %1$s', 'better-wp-security' ), array( 'b' => array() ) ), $lockout['id'] );
 
 
 
275
  } else {
276
  /* translators: 1: Hostname */
277
  $lockout['description'] = sprintf( wp_kses( __( '<b>Host:</b> %1$s', 'better-wp-security' ), array( 'b' => array() ) ), $lockout['id'] );
@@ -338,7 +341,7 @@ final class ITSEC_Mail {
338
  $style .= 'padding:5px 10px;';
339
  }
340
 
341
- $html .= '<th style="' . $style .'">';
342
  $html .= $header;
343
  $html .= '</th>';
344
  }
@@ -421,14 +424,20 @@ final class ITSEC_Mail {
421
  /**
422
  * Add an image to the email.
423
  *
424
- * @param string $src URL of the image.
425
- * @param int $width Max width of the image in pixels.
426
  */
427
- public function add_image( $src, $width ) {
428
- $this->add_html( $this->get_image( $src, $width ) );
429
  }
430
 
431
- public function get_image( $src, $width ) {
 
 
 
 
 
 
432
  $module = $this->get_template( 'image.html' );
433
  $module = $this->replace_all( $module, array(
434
  'src' => $src,
272
  if ( 'user' === $lockout['type'] ) {
273
  /* translators: 1: Username */
274
  $lockout['description'] = sprintf( wp_kses( __( '<b>User:</b> %1$s', 'better-wp-security' ), array( 'b' => array() ) ), $lockout['id'] );
275
+ } elseif ( 'username' === $lockout['type'] ) {
276
+ /* translators: 1: Username */
277
+ $lockout['description'] = sprintf( wp_kses( __( '<b>Username:</b> %1$s', 'better-wp-security' ), array( 'b' => array() ) ), $lockout['id'] );
278
  } else {
279
  /* translators: 1: Hostname */
280
  $lockout['description'] = sprintf( wp_kses( __( '<b>Host:</b> %1$s', 'better-wp-security' ), array( 'b' => array() ) ), $lockout['id'] );
341
  $style .= 'padding:5px 10px;';
342
  }
343
 
344
+ $html .= '<th style="' . $style . '">';
345
  $html .= $header;
346
  $html .= '</th>';
347
  }
424
  /**
425
  * Add an image to the email.
426
  *
427
+ * @param string $src_or_name URL of the image or the name of the mail image.
428
+ * @param int $width Max width of the image in pixels.
429
  */
430
+ public function add_image( $src_or_name, $width ) {
431
+ $this->add_html( $this->get_image( $src_or_name, $width ) );
432
  }
433
 
434
+ public function get_image( $src_or_name, $width ) {
435
+ if ( false === strpos( $src_or_name, '.' ) ) {
436
+ $src = $this->get_image_url( $src_or_name );
437
+ } else {
438
+ $src = $src_or_name;
439
+ }
440
+
441
  $module = $this->get_template( 'image.html' );
442
  $module = $this->replace_all( $module, array(
443
  'src' => $src,
core/lib/class-itsec-scheduler-cron.php CHANGED
@@ -457,6 +457,8 @@ class ITSEC_Scheduler_Cron extends ITSEC_Scheduler {
457
  }
458
 
459
  public function uninstall() {
 
 
460
 
461
  $crons = _get_cron_array();
462
 
@@ -472,4 +474,4 @@ class ITSEC_Scheduler_Cron extends ITSEC_Scheduler {
472
 
473
  delete_site_option( self::OPTION );
474
  }
475
- }
457
  }
458
 
459
  public function uninstall() {
460
+ remove_action( self::HOOK, array( $this, 'process' ) );
461
+ remove_filter( 'cron_schedules', array( $this, 'register_cron_schedules' ) );
462
 
463
  $crons = _get_cron_array();
464
 
474
 
475
  delete_site_option( self::OPTION );
476
  }
477
+ }
core/lib/class-itsec-scheduler-page-load.php CHANGED
@@ -381,6 +381,7 @@ class ITSEC_Scheduler_Page_Load extends ITSEC_Scheduler {
381
  }
382
 
383
  public function uninstall() {
 
384
  delete_site_option( self::OPTION );
385
  }
386
  }
381
  }
382
 
383
  public function uninstall() {
384
+ remove_action( 'init', array( $this, 'init' ) );
385
  delete_site_option( self::OPTION );
386
  }
387
  }
core/lib/class-itsec-scheduler.php CHANGED
@@ -232,6 +232,15 @@ abstract class ITSEC_Scheduler {
232
  $this->custom_schedules[ $slug ] = $interval;
233
  }
234
 
 
 
 
 
 
 
 
 
 
235
  /**
236
  * Register an event loop.
237
  *
@@ -363,4 +372,4 @@ abstract class ITSEC_Scheduler {
363
  public function uninstall() {
364
 
365
  }
366
- }
232
  $this->custom_schedules[ $slug ] = $interval;
233
  }
234
 
235
+ /**
236
+ * Get a registered custom schedules.
237
+ *
238
+ * @return array
239
+ */
240
+ public function get_custom_schedules() {
241
+ return $this->custom_schedules;
242
+ }
243
+
244
  /**
245
  * Register an event loop.
246
  *
372
  public function uninstall() {
373
 
374
  }
375
+ }
core/lib/fingerprinting/class-itsec-fingerprint.php CHANGED
@@ -3,7 +3,7 @@
3
  /**
4
  * Class ITSEC_Fingerprint
5
  */
6
- class ITSEC_Fingerprint {
7
 
8
  const S_APPROVED = 'approved';
9
  const S_AUTO_APPROVED = 'auto-approved';
@@ -696,4 +696,73 @@ class ITSEC_Fingerprint {
696
 
697
  return trim( $str );
698
  }
699
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  /**
4
  * Class ITSEC_Fingerprint
5
  */
6
+ class ITSEC_Fingerprint implements JsonSerializable {
7
 
8
  const S_APPROVED = 'approved';
9
  const S_AUTO_APPROVED = 'auto-approved';
696
 
697
  return trim( $str );
698
  }
699
+
700
+ /**
701
+ * Serialize a fingerprint to JSON.
702
+ *
703
+ * For uses when you need to persist a fingerprint that hasn't been stored yet
704
+ * to short term storage.
705
+ *
706
+ * @return array
707
+ */
708
+ public function jsonSerialize() {
709
+ if ( $this->_id ) {
710
+ return null;
711
+ }
712
+
713
+ $approved_at = $this->get_approved_at();
714
+
715
+ $values = array();
716
+
717
+ foreach ( $this->get_values() as $value ) {
718
+ $values[ $value->get_source()->get_slug() ] = $value->get_value();
719
+ }
720
+
721
+ return [
722
+ 'user' => $this->get_user()->ID,
723
+ 'created_at' => $this->get_created_at()->format( 'Y-m-d H:i:s' ),
724
+ 'values' => $values,
725
+ 'status' => $this->get_status(),
726
+ 'uses' => $this->get_uses(),
727
+ 'approved_at' => $approved_at ? $approved_at->format( 'Y-m-d H:i:s' ) : null,
728
+ ];
729
+ }
730
+
731
+ /**
732
+ * Recreate a fingerprint from JSON.
733
+ *
734
+ * @param string $json
735
+ *
736
+ * @return ITSEC_Fingerprint|null
737
+ */
738
+ public static function from_json( $json ) {
739
+ $decoded = json_decode( $json, true );
740
+
741
+ if ( ! $decoded ) {
742
+ return null;
743
+ }
744
+
745
+ ITSEC_Lib::load( 'fingerprinting' );
746
+
747
+ $sources = ITSEC_Lib_Fingerprinting::get_sources();
748
+ $values = array();
749
+
750
+ foreach ( $decoded['values'] as $slug => $value ) {
751
+ if ( isset( $sources[ $slug ] ) ) {
752
+ $values[] = new ITSEC_Fingerprint_Value( $sources[ $slug ], $value );
753
+ }
754
+ }
755
+
756
+ $fingerprint = new ITSEC_Fingerprint(
757
+ get_userdata( $decoded['user'] ),
758
+ new DateTime( $decoded['created_at'], new DateTimeZone( 'UTC' ) ),
759
+ $values
760
+ );
761
+
762
+ $fingerprint->_status = $decoded['status'];
763
+ $fingerprint->_uses = $decoded['uses'];
764
+ $fingerprint->_approved_at = $decoded['approved_at'] ? new DateTime( $decoded['approved_at'], new DateTimeZone( 'UTC' ) ) : null;
765
+
766
+ return $fingerprint;
767
+ }
768
+ }
core/lib/itsec-zxcvbn-php/matchers/dictionary.php CHANGED
@@ -89,13 +89,13 @@ class ITSEC_Zxcvbn_Dictionary_Match extends ITSEC_Zxcvbn_Match {
89
  for ( $j = $i; $j < $length; $j++ ) {
90
  $word = substr( $pw_lower, $i, $j - $i + 1 );
91
 
92
- if ( isset( $dictionary->{$word} ) ) {
93
  $result[] = array(
94
  'begin' => $i,
95
  'end' => $j,
96
  'token' => substr( $password, $i, $j - $i + 1 ),
97
  'matched_word' => $word,
98
- 'rank' => $dictionary->{$word},
99
  );
100
  }
101
  }
@@ -110,7 +110,7 @@ class ITSEC_Zxcvbn_Dictionary_Match extends ITSEC_Zxcvbn_Match {
110
  * @return object
111
  */
112
  protected static function get_ranked_dictionary( $dictionary_name ) {
113
- return json_decode( file_get_contents( dirname( __FILE__ ) . sprintf( '/ranked_frequency_list-%s.json', $dictionary_name ) ) );
114
  }
115
 
116
  public function estimate_guesses() {
89
  for ( $j = $i; $j < $length; $j++ ) {
90
  $word = substr( $pw_lower, $i, $j - $i + 1 );
91
 
92
+ if ( isset( $dictionary[ $word ] ) ) {
93
  $result[] = array(
94
  'begin' => $i,
95
  'end' => $j,
96
  'token' => substr( $password, $i, $j - $i + 1 ),
97
  'matched_word' => $word,
98
+ 'rank' => $dictionary[ $word ],
99
  );
100
  }
101
  }
110
  * @return object
111
  */
112
  protected static function get_ranked_dictionary( $dictionary_name ) {
113
+ return json_decode( file_get_contents( dirname( __FILE__ ) . sprintf( '/ranked_frequency_list-%s.json', $dictionary_name ) ), true );
114
  }
115
 
116
  public function estimate_guesses() {
core/lib/lockout/abstract-context.php ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout;
4
+
5
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Source\Source;
6
+
7
+ abstract class Context implements Source {
8
+
9
+ /** @var string */
10
+ private $lockout_module;
11
+
12
+ /**
13
+ * ITSEC_Execute_Lockout_Context constructor.
14
+ *
15
+ * @param string $lockout_module
16
+ */
17
+ public function __construct( $lockout_module ) {
18
+ $this->lockout_module = $lockout_module;
19
+ }
20
+
21
+ /**
22
+ * Get the registered lockout module {@see 'itsec_lockout_modules'} that
23
+ *
24
+ * @return string
25
+ */
26
+ public function get_lockout_module() {
27
+ return $this->lockout_module;
28
+ }
29
+
30
+ /**
31
+ * @return mixed
32
+ */
33
+ public function get_source_slug() {
34
+ return $this->get_lockout_module();
35
+ }
36
+
37
+ /**
38
+ * Make an execute lock context from the lockout context.
39
+ *
40
+ * @return Execute_Lock\Context
41
+ */
42
+ abstract public function make_execute_lock_context();
43
+ }
44
+
45
+ require_once( __DIR__ . '/class-host-context.php' );
46
+ require_once( __DIR__ . '/class-user-context.php' );
47
+ require_once( __DIR__ . '/class-username-context.php' );
core/lib/lockout/class-host-context.php ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout;
4
+
5
+ use ITSEC_Lib;
6
+
7
+ final class Host_Context extends Context {
8
+
9
+ /** @var string */
10
+ private $host;
11
+
12
+ /** @var int */
13
+ private $login_user_id;
14
+
15
+ /** @var string */
16
+ private $login_username;
17
+
18
+ /** @var bool */
19
+ private $user_limit_triggered = false;
20
+
21
+ /**
22
+ * ITSEC_Host_Lockout_Context constructor.
23
+ *
24
+ * @param string $lockout_module
25
+ * @param string $host
26
+ */
27
+ public function __construct( $lockout_module, $host = '' ) {
28
+ parent::__construct( $lockout_module );
29
+ $this->host = $host ?: ITSEC_Lib::get_ip();
30
+ }
31
+
32
+ /**
33
+ * Get the host being locked out.
34
+ *
35
+ * @return string
36
+ */
37
+ public function get_host() {
38
+ return $this->host;
39
+ }
40
+
41
+ /**
42
+ * If this lockout occurred while trying to login as a user,
43
+ * this will return the user ID that was trying to be logged-in-to.
44
+ *
45
+ * @return int|null
46
+ */
47
+ public function get_login_user_id() {
48
+ return $this->login_user_id;
49
+ }
50
+
51
+ /**
52
+ * Set who is being logged in as.
53
+ *
54
+ * @param int $user_id
55
+ *
56
+ * @return $this
57
+ */
58
+ public function set_login_user_id( $user_id ) {
59
+ $this->login_user_id = $user_id;
60
+
61
+ return $this;
62
+ }
63
+
64
+ /**
65
+ * If this lockout occurred while trying to login to a non-existent user,
66
+ * this will return that username.
67
+ *
68
+ * @return string
69
+ */
70
+ public function get_login_username() {
71
+ return $this->login_username;
72
+ }
73
+
74
+ /**
75
+ * Set which username is being logged in as.
76
+ *
77
+ * @param string $login_username
78
+ *
79
+ * @return Host_Context
80
+ */
81
+ public function set_login_username( $login_username ) {
82
+ $this->login_username = $login_username;
83
+
84
+ return $this;
85
+ }
86
+
87
+ /**
88
+ * Was the user lockout limit was triggered.
89
+ *
90
+ * @return bool
91
+ */
92
+ public function is_user_limit_triggered() {
93
+ return $this->user_limit_triggered;
94
+ }
95
+
96
+ /**
97
+ * Set that the user lockout limit was triggered.
98
+ *
99
+ * @param bool $user_limit_triggered
100
+ *
101
+ * @return Host_Context
102
+ */
103
+ public function set_user_limit_triggered( $user_limit_triggered = true ) {
104
+ $this->user_limit_triggered = $user_limit_triggered;
105
+
106
+ return $this;
107
+ }
108
+
109
+ public function make_execute_lock_context() {
110
+ $context = new Execute_Lock\Host_Context( $this, $this->get_host() );
111
+
112
+ if ( $user_id = $this->get_login_user_id() ) {
113
+ $context->set_login_user_id( $user_id );
114
+ } elseif ( $username = $this->get_login_username() ) {
115
+ $context->set_login_username( $username );
116
+ }
117
+
118
+ return $context;
119
+ }
120
+ }
core/lib/lockout/class-lockout.php ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout;
4
+
5
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Source\Source;
6
+
7
+ final class Lockout implements Source {
8
+
9
+ /** @var int */
10
+ private $id;
11
+
12
+ /** @var string */
13
+ private $module;
14
+
15
+ /** @var \DateTime */
16
+ private $start;
17
+
18
+ /** @var \DateTime */
19
+ private $expire;
20
+
21
+ /** @var string */
22
+ private $host;
23
+
24
+ /** @var int */
25
+ private $user_id;
26
+
27
+ /** @var string */
28
+ private $username;
29
+
30
+ /** @var bool */
31
+ private $active;
32
+
33
+ /** @var Context */
34
+ private $context;
35
+
36
+ /**
37
+ * Lockout constructor.
38
+ *
39
+ * @param int $id
40
+ * @param string $module
41
+ * @param \DateTime $start
42
+ * @param \DateTime $expire
43
+ * @param string $host
44
+ * @param int $user_id
45
+ * @param string $username
46
+ * @param bool $active
47
+ * @param Context $context
48
+ */
49
+ public function __construct( $id, $module, \DateTime $start, \DateTime $expire, $host, $user_id, $username, $active, Context $context = null ) {
50
+ $this->id = $id;
51
+ $this->module = $module;
52
+ $this->start = $start;
53
+ $this->expire = $expire;
54
+ $this->host = $host;
55
+ $this->user_id = $user_id;
56
+ $this->username = $username;
57
+ $this->active = $active;
58
+ $this->context = $context;
59
+ }
60
+
61
+ /**
62
+ * Get the unique id for this lockout.
63
+ *
64
+ * @return int
65
+ */
66
+ public function get_id() {
67
+ return $this->id;
68
+ }
69
+
70
+ /**
71
+ * Get the lockout module responsible for this lockout.
72
+ *
73
+ * @return string
74
+ */
75
+ public function get_module() {
76
+ return $this->module;
77
+ }
78
+
79
+ /**
80
+ * Get the date & time when the lockout started. In UTC.
81
+ *
82
+ * @return \DateTime
83
+ */
84
+ public function get_start() {
85
+ return $this->start;
86
+ }
87
+
88
+ /**
89
+ * Get the date & time when the lockout has expired. In UTC.
90
+ *
91
+ * @return \DateTime
92
+ */
93
+ public function get_expire() {
94
+ return $this->expire;
95
+ }
96
+
97
+ /**
98
+ * Get the host that was locked out.
99
+ *
100
+ * @return string
101
+ */
102
+ public function get_host() {
103
+ return $this->host;
104
+ }
105
+
106
+ /**
107
+ * Get the user ID that was locked out.
108
+ *
109
+ * @return int
110
+ */
111
+ public function get_user_id() {
112
+ return $this->user_id;
113
+ }
114
+
115
+ /**
116
+ * Get the username that was locked out.
117
+ *
118
+ * @return string
119
+ */
120
+ public function get_username() {
121
+ return $this->username;
122
+ }
123
+
124
+ /**
125
+ * Is the lockout marked as active. This does not check that
126
+ * the lockout has not expired.
127
+ *
128
+ * @return bool
129
+ */
130
+ public function is_active() {
131
+ return $this->active;
132
+ }
133
+
134
+ /**
135
+ * @return Context|null
136
+ */
137
+ public function get_context() {
138
+ return $this->context;
139
+ }
140
+
141
+ /**
142
+ * Make an execute lock context for this lockout.
143
+ *
144
+ * @return Execute_Lock\Context
145
+ */
146
+ public function make_execute_lock_context() {
147
+ if ( $context = $this->get_context() ) {
148
+ return $context->make_execute_lock_context()->with_source( $this );
149
+ }
150
+
151
+ if ( $this->get_host() ) {
152
+ return new Execute_Lock\Host_Context( $this, $this->get_host() );
153
+ }
154
+
155
+ if ( $this->get_user_id() ) {
156
+ return new Execute_Lock\User_Context( $this, $this->get_user_id() );
157
+ }
158
+
159
+ if ( $this->get_username() ) {
160
+ return new Execute_Lock\Username_Context( $this, $this->get_username() );
161
+ }
162
+
163
+ throw new \UnexpectedValueException( __( 'Unable to generate context for lockout.', 'better-wp-security' ) );
164
+ }
165
+
166
+ public function get_source_slug() {
167
+ return 'lockout-' . $this->id;
168
+ }
169
+ }
core/lib/lockout/class-user-context.php ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout;
4
+
5
+ final class User_Context extends Context {
6
+
7
+ /** @var int */
8
+ private $user_id;
9
+
10
+ /**
11
+ * ITSEC_User_Lockout_Context constructor.
12
+ *
13
+ * @param string $lockout_module
14
+ * @param int $user_id
15
+ */
16
+ public function __construct( $lockout_module, $user_id ) {
17
+ parent::__construct( $lockout_module );
18
+ $this->user_id = $user_id;
19
+ }
20
+
21
+ /**
22
+ * Get the user ID that was locked out.
23
+ *
24
+ * @return int
25
+ */
26
+ public function get_user_id() {
27
+ return $this->user_id;
28
+ }
29
+
30
+ public function make_execute_lock_context() {
31
+ return new Execute_Lock\User_Context( $this, $this->get_user_id() );
32
+ }
33
+ }
core/lib/lockout/class-username-context.php ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout;
4
+
5
+ final class Username_Context extends Context {
6
+
7
+ /** @var string */
8
+ private $username;
9
+
10
+ /**
11
+ * ITSEC_Username_Lockout_Context constructor.
12
+ *
13
+ * @param string $lockout_module
14
+ * @param string $username
15
+ */
16
+ public function __construct( $lockout_module, $username ) {
17
+ parent::__construct( $lockout_module );
18
+ $this->username = $username;
19
+ }
20
+
21
+ /**
22
+ * Get the username that was locked out.
23
+ *
24
+ * @return string
25
+ */
26
+ public function get_username() {
27
+ return $this->username;
28
+ }
29
+
30
+ public function make_execute_lock_context() {
31
+ return new Execute_Lock\Username_Context( $this, $this->get_username() );
32
+ }
33
+ }
core/lib/lockout/execute-lock/abstract-context.php ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout\Execute_Lock;
4
+
5
+ use ArrayAccess;
6
+ use ArrayIterator;
7
+ use IteratorAggregate;
8
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Source\Source;
9
+
10
+ abstract class Context implements ArrayAccess, IteratorAggregate {
11
+
12
+ /** @var Source */
13
+ private $source;
14
+
15
+ /**
16
+ * ITSEC_Execute_Lock_Context constructor.
17
+ *
18
+ * @param Source $source
19
+ */
20
+ public function __construct( Source $source ) { $this->source = $source; }
21
+
22
+ /**
23
+ * Get the source that caused the execute lock.
24
+ *
25
+ * @return Source
26
+ */
27
+ public function get_source() {
28
+ return $this->source;
29
+ }
30
+
31
+ /**
32
+ * Create a copy of this context with a different source.
33
+ *
34
+ * @param Source $source
35
+ *
36
+ * @return $this
37
+ */
38
+ public function with_source( Source $source ) {
39
+ $self = clone $this;
40
+ $self->source = $source;
41
+
42
+ return $self;
43
+ }
44
+
45
+ /**
46
+ * Return the context as an array.
47
+ *
48
+ * @return array
49
+ */
50
+ public function to_legacy() {
51
+ return array(
52
+ 'type' => $this->get_source()->get_source_slug(),
53
+ );
54
+ }
55
+
56
+ public function getIterator() {
57
+ return new ArrayIterator( $this->to_legacy() );
58
+ }
59
+
60
+ public function offsetExists( $offset ) {
61
+ $legacy = $this->to_legacy();
62
+
63
+ return isset( $legacy[ $offset ] );
64
+ }
65
+
66
+ public function offsetGet( $offset ) {
67
+ $legacy = $this->to_legacy();
68
+
69
+ return $legacy[ $offset ];
70
+ }
71
+
72
+ public function offsetSet( $offset, $value ) {
73
+ // no-op
74
+ }
75
+
76
+ public function offsetUnset( $offset ) {
77
+ // no-op
78
+ }
79
+ }
80
+
81
+ require_once( __DIR__ . '/class-host-context.php' );
82
+ require_once( __DIR__ . '/class-user-context.php' );
83
+ require_once( __DIR__ . '/class-username-context.php' );
core/lib/lockout/execute-lock/class-host-context.php ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout\Execute_Lock;
4
+
5
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Source\Source;
6
+
7
+ final class Host_Context extends Context {
8
+
9
+ /** @var string */
10
+ private $host;
11
+
12
+ /** @var bool */
13
+ private $network_brute_force;
14
+
15
+ /** @var int */
16
+ private $login_user_id;
17
+
18
+ /** @var string */
19
+ private $login_username;
20
+
21
+ /**
22
+ * ITSEC_Execute_Host_Lockout_Context constructor.
23
+ *
24
+ * @param Source $source
25
+ * @param string $host
26
+ */
27
+ public function __construct( Source $source, $host ) {
28
+ parent::__construct( $source );
29
+ $this->host = $host;
30
+ }
31
+
32
+ /**
33
+ * Get the host being locked out.
34
+ *
35
+ * @return string
36
+ */
37
+ public function get_host() {
38
+ return $this->host;
39
+ }
40
+
41
+ /**
42
+ * Get whether this is a Network Brute Force generated lockout.
43
+ *
44
+ * @return bool
45
+ */
46
+ public function is_network_brute_force() {
47
+ return $this->network_brute_force;
48
+ }
49
+
50
+ /**
51
+ * Set that this is a Network Brute Force generated lockout.
52
+ *
53
+ * @return $this
54
+ */
55
+ public function set_network_brute_force() {
56
+ $this->network_brute_force = true;
57
+
58
+ return $this;
59
+ }
60
+
61
+ /**
62
+ * If this lockout occurred while trying to login as a user,
63
+ * this will return the user ID that was trying to be logged-in-to.
64
+ *
65
+ * @return int|null
66
+ */
67
+ public function get_login_user_id() {
68
+ return $this->login_user_id;
69
+ }
70
+
71
+ /**
72
+ * Set who is being logging in as.
73
+ *
74
+ * @param int $user_id
75
+ *
76
+ * @return $this
77
+ */
78
+ public function set_login_user_id( $user_id ) {
79
+ $this->login_user_id = $user_id;
80
+
81
+ return $this;
82
+ }
83
+
84
+ /**
85
+ * If this lockout occurred while trying to login to a non-existent user,
86
+ * this will return that username.
87
+ *
88
+ * @return string
89
+ */
90
+ public function get_login_username() {
91
+ return $this->login_username;
92
+ }
93
+
94
+ /**
95
+ * Set which username is being logged in as.
96
+ *
97
+ * @param string $login_username
98
+ *
99
+ * @return Host_Context
100
+ */
101
+ public function set_login_username( $login_username ) {
102
+ $this->login_username = $login_username;
103
+
104
+ return $this;
105
+ }
106
+
107
+ public function to_legacy() {
108
+ $legacy = parent::to_legacy();
109
+
110
+ $legacy['host'] = $this->get_host();
111
+ $legacy['network_lock'] = $this->is_network_brute_force();
112
+
113
+ return $legacy;
114
+ }
115
+ }
core/lib/lockout/execute-lock/class-user-context.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout\Execute_Lock;
4
+
5
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Source\Source;
6
+
7
+ final class User_Context extends Context {
8
+
9
+ /** @var int */
10
+ private $user_id;
11
+
12
+ /**
13
+ * ITSEC_Execute_User_Lockout_Context constructor.
14
+ *
15
+ * @param Source $source
16
+ * @param int $user_id
17
+ */
18
+ public function __construct( Source $source, $user_id ) {
19
+ parent::__construct( $source );
20
+ $this->user_id = $user_id;
21
+ }
22
+
23
+ /**
24
+ * Get the user ID that was locked out.
25
+ *
26
+ * @return int
27
+ */
28
+ public function get_user_id() {
29
+ return $this->user_id;
30
+ }
31
+
32
+ public function to_legacy() {
33
+ $legacy = parent::to_legacy();
34
+
35
+ $legacy['user'] = $this->get_user_id();
36
+ $legacy['user_lock'] = true;
37
+
38
+ return $legacy;
39
+ }
40
+ }
core/lib/lockout/execute-lock/class-username-context.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout\Execute_Lock;
4
+
5
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Source\Source;
6
+
7
+ final class Username_Context extends Context {
8
+
9
+ /** @var string */
10
+ private $username;
11
+
12
+ /**
13
+ * ITSEC_Execute_Username_Lockout_Context constructor.
14
+ *
15
+ * @param Source $source
16
+ * @param string $username
17
+ */
18
+ public function __construct( Source $source, $username ) {
19
+ parent::__construct( $source );
20
+ $this->username = $username;
21
+ }
22
+
23
+ /**
24
+ * Get the username that was locked out.
25
+ *
26
+ * @return string
27
+ */
28
+ public function get_username() {
29
+ return $this->username;
30
+ }
31
+
32
+ public function to_legacy() {
33
+ $legacy = parent::to_legacy();
34
+
35
+ $legacy['username'] = $this->get_username();
36
+ $legacy['user_lock'] = true;
37
+
38
+ return $legacy;
39
+ }
40
+ }
core/lib/lockout/execute-lock/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/lib/lockout/execute-lock/source/class-configurable.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout\Execute_Lock\Source;
4
+
5
+ class Configurable implements Source {
6
+
7
+ /** @var string */
8
+ private $slug;
9
+
10
+ /**
11
+ * ITSEC_Execute_Lock_Source_Configurable constructor.
12
+ *
13
+ * @param string $slug
14
+ */
15
+ public function __construct( $slug ) { $this->slug = $slug; }
16
+
17
+ public function get_source_slug() {
18
+ return $this->slug;
19
+ }
20
+ }
core/lib/lockout/execute-lock/source/class-lockout-module.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout\Execute_Lock\Source;
4
+
5
+ class Lockout_Module implements Source {
6
+
7
+ /** @var string */
8
+ private $module_slug;
9
+
10
+ /**
11
+ * Lockout_Module constructor.
12
+ *
13
+ * @param string $module_slug
14
+ */
15
+ public function __construct( $module_slug ) { $this->module_slug = $module_slug; }
16
+
17
+ public function get_source_slug() {
18
+ return $this->module_slug;
19
+ }
20
+ }
core/lib/lockout/execute-lock/source/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/lib/lockout/execute-lock/source/interface-source.php ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace iThemesSecurity\Lib\Lockout\Execute_Lock\Source;
4
+
5
+ require_once( __DIR__ . '/class-lockout-module.php' );
6
+ require_once( __DIR__ . '/class-configurable.php' );
7
+
8
+ interface Source {
9
+
10
+ /**
11
+ * Get a unique slug for the lock source.
12
+ *
13
+ * @return string
14
+ */
15
+ public function get_source_slug();
16
+ }
core/lib/lockout/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/lib/login-interstitial/abstract-itsec-login-interstitial.php CHANGED
@@ -34,7 +34,7 @@ abstract class ITSEC_Login_Interstitial {
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
 
@@ -70,8 +70,14 @@ abstract class ITSEC_Login_Interstitial {
70
  *
71
  * @param ITSEC_Login_Interstitial_Session $session
72
  * @param array $data
 
 
73
  */
74
- public function submit( ITSEC_Login_Interstitial_Session $session, array $data ) { }
 
 
 
 
75
 
76
  /**
77
  * Does the interstitial have async GET actions.
34
  * @param ITSEC_Login_Interstitial_Session $session
35
  * @param array $args
36
  *
37
+ * @return void
38
  */
39
  abstract public function render( ITSEC_Login_Interstitial_Session $session, array $args );
40
 
70
  *
71
  * @param ITSEC_Login_Interstitial_Session $session
72
  * @param array $data
73
+ *
74
+ * @return WP_Error|null
75
  */
76
+ public function submit( ITSEC_Login_Interstitial_Session $session, array $data ) {
77
+ _doing_it_wrong( __METHOD__, 'Must override ::submit if has submit handler.', '5.3.0' );
78
+
79
+ return new WP_Error( 'internal_server_error', 'Internal Server Error' );
80
+ }
81
 
82
  /**
83
  * Does the interstitial have async GET actions.
core/lib/login-interstitial/class-itsec-login-interstitial-session.php CHANGED
@@ -386,12 +386,41 @@ class ITSEC_Login_Interstitial_Session {
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
 
@@ -409,9 +438,28 @@ class ITSEC_Login_Interstitial_Session {
409
  }
410
  }
411
 
 
 
 
 
412
  return $deleted;
413
  }
414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  /**
416
  * Create a new state session.
417
  *
@@ -421,6 +469,10 @@ class ITSEC_Login_Interstitial_Session {
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(),
@@ -432,13 +484,20 @@ class ITSEC_Login_Interstitial_Session {
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
  /**
386
  return $this->user;
387
  }
388
 
389
+ /**
390
+ * Build up the interstitial session configuration from the global state.
391
+ *
392
+ * This does not set the show afters because this is also used by the show after code.
393
+ */
394
+ public function initialize_from_global_state() {
395
+ if ( isset( $_REQUEST['interim-login'] ) ) {
396
+ $this->set_interim_login();
397
+ }
398
+
399
+ if ( ! empty( $_REQUEST['redirect_to'] ) ) {
400
+ $this->set_redirect_to( $_REQUEST['redirect_to'] );
401
+ } elseif ( ! did_action( 'login_init' ) && ( $ref = wp_get_referer() ) ) {
402
+ $this->set_redirect_to( $ref );
403
+ } elseif ( ! did_action( 'login_init' ) ) {
404
+ $this->set_redirect_to( $_SERVER['REQUEST_URI'] );
405
+ }
406
+
407
+ if ( ! empty( $_REQUEST['rememberme'] ) ) {
408
+ $this->set_remember_me();
409
+ }
410
+ }
411
+
412
  /**
413
  * Save the session.
414
  *
415
  * @return bool
416
  */
417
  public function save() {
418
+ $this->log( 'save', [
419
+ 'current' => $this->get_current_interstitial(),
420
+ 'completed' => $this->get_completed_interstitials(),
421
+ 'show_after' => $this->get_show_after(),
422
+ ] );
423
+
424
  return update_metadata_by_mid( 'user', $this->get_id(), $this->data, self::META_KEY );
425
  }
426
 
438
  }
439
  }
440
 
441
+ if ( ! empty( $this->data['log'] ) ) {
442
+ ITSEC_Log::add_process_stop( $this->data['log'] );
443
+ }
444
+
445
  return $deleted;
446
  }
447
 
448
+ /**
449
+ * Log an update to this interstitial.
450
+ *
451
+ * @param string $code
452
+ * @param mixed|false $data
453
+ * @param array|false $overrides
454
+ */
455
+ protected function log( $code, $data = false, $overrides = array() ) {
456
+ if ( ! empty( $this->data['log'] ) ) {
457
+ $reference = $this->data['log'];
458
+ $reference['code'] = $code;
459
+ ITSEC_Log::add_process_update( $reference, $data, $overrides );
460
+ }
461
+ }
462
+
463
  /**
464
  * Create a new state session.
465
  *
469
  * @return ITSEC_Login_Interstitial_Session|WP_Error
470
  */
471
  public static function create( WP_User $user, $current = '' ) {
472
+ $log = ITSEC_Log::add_process_start( 'login-interstitial', 'create', [
473
+ 'current' => $current,
474
+ '_server' => $_SERVER,
475
+ ], [ 'user_id' => $user->ID ] );
476
 
477
  $data = array(
478
  'uuid' => wp_generate_uuid4(),
484
  'remember_me' => false,
485
  'interim_login' => false,
486
  'state' => array(),
487
+ 'log' => $log,
488
  );
489
 
490
  if ( ! $mid = add_user_meta( $user->ID, self::META_KEY, $data ) ) {
491
+ $error = new WP_Error( 'itsec-lib-login-interstitial-save-failed', esc_html__( 'Failed to create interstitial state.', 'better-wp-security' ) );
492
+ ITSEC_Log::add_process_stop( $log, $error );
493
+
494
+ return $error;
495
  }
496
 
497
+ $session = new self( $user, $mid, $data );
498
+ $session->log( 'created', $mid );
499
+
500
+ return $session;
501
  }
502
 
503
  /**
core/lib/mail-templates/magic-link.html ADDED
@@ -0,0 +1,991 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <style type="text/css">
2
+ p{
3
+ margin:10px 0;
4
+ padding:0;
5
+ }
6
+ table{
7
+ border-collapse:collapse;
8
+ }
9
+ h1,h2,h3,h4,h5,h6{
10
+ display:block;
11
+ margin:0;
12
+ padding:0;
13
+ }
14
+ img,a img{
15
+ border:0;
16
+ height:auto;
17
+ outline:none;
18
+ text-decoration:none;
19
+ }
20
+ body,#bodyTable,#bodyCell{
21
+ height:100%;
22
+ margin:0;
23
+ padding:0;
24
+ width:100%;
25
+ }
26
+ .mcnPreviewText{
27
+ display:none !important;
28
+ }
29
+ #outlook a{
30
+ padding:0;
31
+ }
32
+ img{
33
+ -ms-interpolation-mode:bicubic;
34
+ }
35
+ table{
36
+ mso-table-lspace:0pt;
37
+ mso-table-rspace:0pt;
38
+ }
39
+ .ReadMsgBody{
40
+ width:100%;
41
+ }
42
+ .ExternalClass{
43
+ width:100%;
44
+ }
45
+ p,a,li,td,blockquote{
46
+ mso-line-height-rule:exactly;
47
+ }
48
+ a[href^=tel],a[href^=sms]{
49
+ color:inherit;
50
+ cursor:default;
51
+ text-decoration:none;
52
+ }
53
+ p,a,li,td,body,table,blockquote{
54
+ -ms-text-size-adjust:100%;
55
+ -webkit-text-size-adjust:100%;
56
+ }
57
+ .ExternalClass,.ExternalClass p,.ExternalClass td,.ExternalClass div,.ExternalClass span,.ExternalClass font{
58
+ line-height:100%;
59
+ }
60
+ a[x-apple-data-detectors]{
61
+ color:inherit !important;
62
+ text-decoration:none !important;
63
+ font-size:inherit !important;
64
+ font-family:inherit !important;
65
+ font-weight:inherit !important;
66
+ line-height:inherit !important;
67
+ }
68
+ .templateContainer{
69
+ max-width:600px !important;
70
+ }
71
+ a.mcnButton{
72
+ display:block;
73
+ }
74
+ .mcnImage,.mcnRetinaImage{
75
+ vertical-align:bottom;
76
+ }
77
+ .mcnTextContent{
78
+ word-break:break-word;
79
+ }
80
+ .mcnTextContent img{
81
+ height:auto !important;
82
+ }
83
+ .mcnDividerBlock{
84
+ table-layout:fixed !important;
85
+ }
86
+ /*
87
+ @tab Page
88
+ @section Background Style
89
+ @tip Set the background color and top border for your email. You may want to choose colors that match your company's branding.
90
+ */
91
+ body,#bodyTable{
92
+ /*@editable*/background-color:#FAFAFA;
93
+ }
94
+ /*
95
+ @tab Page
96
+ @section Background Style
97
+ @tip Set the background color and top border for your email. You may want to choose colors that match your company's branding.
98
+ */
99
+ #bodyCell{
100
+ /*@editable*/border-top:0;
101
+ }
102
+ /*
103
+ @tab Page
104
+ @section Heading 1
105
+ @tip Set the styling for all first-level headings in your emails. These should be the largest of your headings.
106
+ @style heading 1
107
+ */
108
+ h1{
109
+ /*@editable*/color:#303946;
110
+ /*@editable*/font-family:Helvetica;
111
+ /*@editable*/font-size:33px;
112
+ /*@editable*/font-style:normal;
113
+ /*@editable*/font-weight:bold;
114
+ /*@editable*/line-height:125%;
115
+ /*@editable*/letter-spacing:normal;
116
+ /*@editable*/text-align:center;
117
+ }
118
+ /*
119
+ @tab Page
120
+ @section Heading 2
121
+ @tip Set the styling for all second-level headings in your emails.
122
+ @style heading 2
123
+ */
124
+ h2{
125
+ /*@editable*/color:#333e4c;
126
+ /*@editable*/font-family:Helvetica;
127
+ /*@editable*/font-size:24px;
128
+ /*@editable*/font-style:normal;
129
+ /*@editable*/font-weight:bold;
130
+ /*@editable*/line-height:125%;
131
+ /*@editable*/letter-spacing:normal;
132
+ /*@editable*/text-align:center;
133
+ }
134
+ /*
135
+ @tab Page
136
+ @section Heading 3
137
+ @tip Set the styling for all third-level headings in your emails.
138
+ @style heading 3
139
+ */
140
+ h3{
141
+ /*@editable*/color:#ffffff;
142
+ /*@editable*/font-family:Helvetica;
143
+ /*@editable*/font-size:30px;
144
+ /*@editable*/font-style:normal;
145
+ /*@editable*/font-weight:bold;
146
+ /*@editable*/line-height:150%;
147
+ /*@editable*/letter-spacing:normal;
148
+ /*@editable*/text-align:center;
149
+ }
150
+ /*
151
+ @tab Page
152
+ @section Heading 4
153
+ @tip Set the styling for all fourth-level headings in your emails. These should be the smallest of your headings.
154
+ @style heading 4
155
+ */
156
+ h4{
157
+ /*@editable*/color:#008cce;
158
+ /*@editable*/font-family:Helvetica;
159
+ /*@editable*/font-size:18px;
160
+ /*@editable*/font-style:normal;
161
+ /*@editable*/font-weight:bold;
162
+ /*@editable*/line-height:150%;
163
+ /*@editable*/letter-spacing:0px;
164
+ /*@editable*/text-align:center;
165
+ }
166
+ /*
167
+ @tab Preheader
168
+ @section Preheader Style
169
+ @tip Set the background color and borders for your email's preheader area.
170
+ */
171
+ #templatePreheader{
172
+ /*@editable*/background-color:#f8d23a;
173
+ /*@editable*/background-image:none;
174
+ /*@editable*/background-repeat:no-repeat;
175
+ /*@editable*/background-position:center;
176
+ /*@editable*/background-size:cover;
177
+ /*@editable*/border-top:0;
178
+ /*@editable*/border-bottom:0;
179
+ /*@editable*/padding-top:3px;
180
+ /*@editable*/padding-bottom:3px;
181
+ }
182
+ /*
183
+ @tab Preheader
184
+ @section Preheader Text
185
+ @tip Set the styling for your email's preheader text. Choose a size and color that is easy to read.
186
+ */
187
+ #templatePreheader .mcnTextContent,#templatePreheader .mcnTextContent p{
188
+ /*@editable*/color:#514d3c;
189
+ /*@editable*/font-family:Helvetica;
190
+ /*@editable*/font-size:13px;
191
+ /*@editable*/line-height:150%;
192
+ /*@editable*/text-align:left;
193
+ }
194
+ /*
195
+ @tab Preheader
196
+ @section Preheader Link
197
+ @tip Set the styling for your email's preheader links. Choose a color that helps them stand out from your text.
198
+ */
199
+ #templatePreheader .mcnTextContent a,#templatePreheader .mcnTextContent p a{
200
+ /*@editable*/color:#514d3c;
201
+ /*@editable*/font-weight:bold;
202
+ /*@editable*/text-decoration:underline;
203
+ }
204
+ /*
205
+ @tab Header
206
+ @section Header Style
207
+ @tip Set the background color and borders for your email's header area.
208
+ */
209
+ #templateHeader{
210
+ /*@editable*/background-color:#ffffff;
211
+ /*@editable*/background-image:none;
212
+ /*@editable*/background-repeat:no-repeat;
213
+ /*@editable*/background-position:center;
214
+ /*@editable*/background-size:cover;
215
+ /*@editable*/border-top:0;
216
+ /*@editable*/border-bottom:0;
217
+ /*@editable*/padding-top:30px;
218
+ /*@editable*/padding-bottom:50px;
219
+ }
220
+ /*
221
+ @tab Header
222
+ @section Header Text
223
+ @tip Set the styling for your email's header text. Choose a size and color that is easy to read.
224
+ */
225
+ #templateHeader .mcnTextContent,#templateHeader .mcnTextContent p{
226
+ /*@editable*/color:#505050;
227
+ /*@editable*/font-family:Helvetica;
228
+ /*@editable*/font-size:16px;
229
+ /*@editable*/line-height:150%;
230
+ /*@editable*/text-align:center;
231
+ }
232
+ /*
233
+ @tab Header
234
+ @section Header Link
235
+ @tip Set the styling for your email's header links. Choose a color that helps them stand out from your text.
236
+ */
237
+ #templateHeader .mcnTextContent a,#templateHeader .mcnTextContent p a{
238
+ /*@editable*/color:#008cce;
239
+ /*@editable*/font-weight:bold;
240
+ /*@editable*/text-decoration:none;
241
+ }
242
+ /*
243
+ @tab Upper Body
244
+ @section Upper Body Style
245
+ @tip Set the background color and borders for your email's upper body area.
246
+ */
247
+ #templateUpperBody{
248
+ /*@editable*/background-color:#ffffff;
249
+ /*@editable*/background-image:none;
250
+ /*@editable*/background-repeat:no-repeat;
251
+ /*@editable*/background-position:center;
252
+ /*@editable*/background-size:cover;
253
+ /*@editable*/border-top:0;
254
+ /*@editable*/border-bottom:0;
255
+ /*@editable*/padding-top:30px;
256
+ /*@editable*/padding-bottom:10px;
257
+ }
258
+ /*
259
+ @tab Upper Body
260
+ @section Upper Body Text
261
+ @tip Set the styling for your email's upper body text. Choose a size and color that is easy to read.
262
+ */
263
+ #templateUpperBody .mcnTextContent,#templateUpperBody .mcnTextContent p{
264
+ /*@editable*/color:#202020;
265
+ /*@editable*/font-family:Helvetica;
266
+ /*@editable*/font-size:16px;
267
+ /*@editable*/line-height:150%;
268
+ /*@editable*/text-align:center;
269
+ }
270
+ /*
271
+ @tab Upper Body
272
+ @section Upper Body Link
273
+ @tip Set the styling for your email's upper body links. Choose a color that helps them stand out from your text.
274
+ */
275
+ #templateUpperBody .mcnTextContent a,#templateUpperBody .mcnTextContent p a{
276
+ /*@editable*/color:#008cce;
277
+ /*@editable*/font-weight:bold;
278
+ /*@editable*/text-decoration:none;
279
+ }
280
+ /*
281
+ @tab Columns
282
+ @section Column Style
283
+ @tip Set the background color and borders for your email's columns.
284
+ */
285
+ #templateColumns{
286
+ /*@editable*/background-color:#ffffff;
287
+ /*@editable*/background-image:none;
288
+ /*@editable*/background-repeat:no-repeat;
289
+ /*@editable*/background-position:center;
290
+ /*@editable*/background-size:cover;
291
+ /*@editable*/border-top:0;
292
+ /*@editable*/border-bottom:0;
293
+ /*@editable*/padding-top:30px;
294
+ /*@editable*/padding-bottom:30px;
295
+ }
296
+ /*
297
+ @tab Columns
298
+ @section Column Text
299
+ @tip Set the styling for your email's column text. Choose a size and color that is easy to read.
300
+ */
301
+ #templateColumns .columnContainer .mcnTextContent,#templateColumns .columnContainer .mcnTextContent p{
302
+ /*@editable*/color:#1c5057;
303
+ /*@editable*/font-family:Helvetica;
304
+ /*@editable*/font-size:16px;
305
+ /*@editable*/line-height:150%;
306
+ /*@editable*/text-align:left;
307
+ }
308
+ /*
309
+ @tab Columns
310
+ @section Column Link
311
+ @tip Set the styling for your email's column links. Choose a color that helps them stand out from your text.
312
+ */
313
+ #templateColumns .columnContainer .mcnTextContent a,#templateColumns .columnContainer .mcnTextContent p a{
314
+ /*@editable*/color:#008cce;
315
+ /*@editable*/font-weight:bold;
316
+ /*@editable*/text-decoration:none;
317
+ }
318
+ /*
319
+ @tab Lower Body
320
+ @section Lower Body Style
321
+ @tip Set the background color and borders for your email's lower body area.
322
+ */
323
+ #templateLowerBody{
324
+ /*@editable*/background-color:#ffffff;
325
+ /*@editable*/background-image:none;
326
+ /*@editable*/background-repeat:no-repeat;
327
+ /*@editable*/background-position:center;
328
+ /*@editable*/background-size:cover;
329
+ /*@editable*/border-top:0;
330
+ /*@editable*/border-bottom:0;
331
+ /*@editable*/padding-top:50px;
332
+ /*@editable*/padding-bottom:20px;
333
+ }
334
+ /*
335
+ @tab Lower Body
336
+ @section Lower Body Text
337
+ @tip Set the styling for your email's lower body text. Choose a size and color that is easy to read.
338
+ */
339
+ #templateLowerBody .mcnTextContent,#templateLowerBody .mcnTextContent p{
340
+ /*@editable*/color:#505050;
341
+ /*@editable*/font-family:Helvetica;
342
+ /*@editable*/font-size:16px;
343
+ /*@editable*/line-height:150%;
344
+ /*@editable*/text-align:center;
345
+ }
346
+ /*
347
+ @tab Lower Body
348
+ @section Lower Body Link
349
+ @tip Set the styling for your email's lower body links. Choose a color that helps them stand out from your text.
350
+ */
351
+ #templateLowerBody .mcnTextContent a,#templateLowperBody .mcnTextContent p a{
352
+ /*@editable*/color:#3fb4eb;
353
+ /*@editable*/font-weight:bold;
354
+ /*@editable*/text-decoration:none;
355
+ }
356
+ /*
357
+ @tab Footer
358
+ @section Footer Style
359
+ @tip Set the background color and borders for your email's footer area.
360
+ */
361
+ #templateFooter{
362
+ /*@editable*/background-color:#ffffff;
363
+ /*@editable*/background-image:none;
364
+ /*@editable*/background-repeat:no-repeat;
365
+ /*@editable*/background-position:center;
366
+ /*@editable*/background-size:cover;
367
+ /*@editable*/border-top:0;
368
+ /*@editable*/border-bottom:0;
369
+ /*@editable*/padding-top:40px;
370
+ /*@editable*/padding-bottom:9px;
371
+ }
372
+ /*
373
+ @tab Footer
374
+ @section Footer Text
375
+ @tip Set the styling for your email's footer text. Choose a size and color that is easy to read.
376
+ */
377
+ #templateFooter .mcnTextContent,#templateFooter .mcnTextContent p{
378
+ /*@editable*/color:#505050;
379
+ /*@editable*/font-family:Helvetica;
380
+ /*@editable*/font-size:16px;
381
+ /*@editable*/line-height:150%;
382
+ /*@editable*/text-align:center;
383
+ }
384
+ /*
385
+ @tab Footer
386
+ @section Footer Link
387
+ @tip Set the styling for your email's footer links. Choose a color that helps them stand out from your text.
388
+ */
389
+ #templateFooter .mcnTextContent a,#templateFooter .mcnTextContent p a{
390
+ /*@editable*/color:#c3fdf8;
391
+ /*@editable*/font-weight:bold;
392
+ /*@editable*/text-decoration:none;
393
+ }
394
+ @media only screen and (min-width:768px){
395
+ .templateContainer{
396
+ width:600px !important;
397
+ }
398
+
399
+ } @media only screen and (max-width: 480px){
400
+ body,table,td,p,a,li,blockquote{
401
+ -webkit-text-size-adjust:none !important;
402
+ }
403
+
404
+ } @media only screen and (max-width: 480px){
405
+ body{
406
+ width:100% !important;
407
+ min-width:100% !important;
408
+ }
409
+
410
+ } @media only screen and (max-width: 480px){
411
+ #bodyCell{
412
+ padding-top:10px !important;
413
+ }
414
+
415
+ } @media only screen and (max-width: 480px){
416
+ .columnWrapper{
417
+ max-width:100% !important;
418
+ width:100% !important;
419
+ }
420
+
421
+ } @media only screen and (max-width: 480px){
422
+ .mcnRetinaImage{
423
+ max-width:100% !important;
424
+ }
425
+
426
+ } @media only screen and (max-width: 480px){
427
+ .mcnImage{
428
+ width:100% !important;
429
+ }
430
+
431
+ } @media only screen and (max-width: 480px){
432
+ .mcnCartContainer,.mcnCaptionTopContent,.mcnRecContentContainer,.mcnCaptionBottomContent,.mcnTextContentContainer,.mcnBoxedTextContentContainer,.mcnImageGroupContentContainer,.mcnCaptionLeftTextContentContainer,.mcnCaptionRightTextContentContainer,.mcnCaptionLeftImageContentContainer,.mcnCaptionRightImageContentContainer,.mcnImageCardLeftTextContentContainer,.mcnImageCardRightTextContentContainer,.mcnImageCardLeftImageContentContainer,.mcnImageCardRightImageContentContainer{
433
+ max-width:100% !important;
434
+ width:100% !important;
435
+ }
436
+
437
+ } @media only screen and (max-width: 480px){
438
+ .mcnBoxedTextContentContainer{
439
+ min-width:100% !important;
440
+ }
441
+
442
+ } @media only screen and (max-width: 480px){
443
+ .mcnImageGroupContent{
444
+ padding:9px !important;
445
+ }
446
+
447
+ } @media only screen and (max-width: 480px){
448
+ .mcnCaptionLeftContentOuter .mcnTextContent,.mcnCaptionRightContentOuter .mcnTextContent{
449
+ padding-top:9px !important;
450
+ }
451
+
452
+ } @media only screen and (max-width: 480px){
453
+ .mcnImageCardTopImageContent,.mcnCaptionBottomContent:last-child .mcnCaptionBottomImageContent,.mcnCaptionBlockInner .mcnCaptionTopContent:last-child .mcnTextContent{
454
+ padding-top:18px !important;
455
+ }
456
+
457
+ } @media only screen and (max-width: 480px){
458
+ .mcnImageCardBottomImageContent{
459
+ padding-bottom:9px !important;
460
+ }
461
+
462
+ } @media only screen and (max-width: 480px){
463
+ .mcnImageGroupBlockInner{
464
+ padding-top:0 !important;
465
+ padding-bottom:0 !important;
466
+ }
467
+
468
+ } @media only screen and (max-width: 480px){
469
+ .mcnImageGroupBlockOuter{
470
+ padding-top:9px !important;
471
+ padding-bottom:9px !important;
472
+ }
473
+
474
+ } @media only screen and (max-width: 480px){
475
+ .mcnTextContent,.mcnBoxedTextContentColumn{
476
+ padding-right:18px !important;
477
+ padding-left:18px !important;
478
+ }
479
+
480
+ } @media only screen and (max-width: 480px){
481
+ .mcnImageCardLeftImageContent,.mcnImageCardRightImageContent{
482
+ padding-right:18px !important;
483
+ padding-bottom:0 !important;
484
+ padding-left:18px !important;
485
+ }
486
+
487
+ } @media only screen and (max-width: 480px){
488
+ .mcpreview-image-uploader{
489
+ display:none !important;
490
+ width:100% !important;
491
+ }
492
+
493
+ } @media only screen and (max-width: 480px){
494
+ /*
495
+ @tab Mobile Styles
496
+ @section Heading 1
497
+ @tip Make the first-level headings larger in size for better readability on small screens.
498
+ */
499
+ h1{
500
+ /*@editable*/font-size:36px !important;
501
+ /*@editable*/line-height:150% !important;
502
+ }
503
+
504
+ } @media only screen and (max-width: 480px){
505
+ /*
506
+ @tab Mobile Styles
507
+ @section Heading 2
508
+ @tip Make the second-level headings larger in size for better readability on small screens.
509
+ */
510
+ h2{
511
+ /*@editable*/font-size:30px !important;
512
+ /*@editable*/line-height:150% !important;
513
+ }
514
+
515
+ } @media only screen and (max-width: 480px){
516
+ /*
517
+ @tab Mobile Styles
518
+ @section Heading 3
519
+ @tip Make the third-level headings larger in size for better readability on small screens.
520
+ */
521
+ h3{
522
+ /*@editable*/font-size:24px !important;
523
+ /*@editable*/line-height:150% !important;
524
+ }
525
+
526
+ } @media only screen and (max-width: 480px){
527
+ /*
528
+ @tab Mobile Styles
529
+ @section Heading 4
530
+ @tip Make the fourth-level headings larger in size for better readability on small screens.
531
+ */
532
+ h4{
533
+ /*@editable*/font-size:16px !important;
534
+ /*@editable*/line-height:150% !important;
535
+ }
536
+
537
+ } @media only screen and (max-width: 480px){
538
+ /*
539
+ @tab Mobile Styles
540
+ @section Boxed Text
541
+ @tip Make the boxed text larger in size for better readability on small screens. We recommend a font size of at least 16px.
542
+ */
543
+ .mcnBoxedTextContentContainer .mcnTextContent,.mcnBoxedTextContentContainer .mcnTextContent p{
544
+ /*@editable*/font-size:12px !important;
545
+ /*@editable*/line-height:150% !important;
546
+ }
547
+
548
+ } @media only screen and (max-width: 480px){
549
+ /*
550
+ @tab Mobile Styles
551
+ @section Preheader Visibility
552
+ @tip Set the visibility of the email's preheader on small screens. You can hide it to save space.
553
+ */
554
+ #templatePreheader{
555
+ /*@editable*/display:none !important;
556
+ }
557
+
558
+ } @media only screen and (max-width: 480px){
559
+ /*
560
+ @tab Mobile Styles
561
+ @section Preheader Text
562
+ @tip Make the preheader text larger in size for better readability on small screens.
563
+ */
564
+ #templatePreheader .mcnTextContent,#templatePreheader .mcnTextContent p{
565
+ /*@editable*/font-size:14px !important;
566
+ /*@editable*/line-height:150% !important;
567
+ }
568
+
569
+ } @media only screen and (max-width: 480px){
570
+ /*
571
+ @tab Mobile Styles
572
+ @section Header Text
573
+ @tip Make the header text larger in size for better readability on small screens.
574
+ */
575
+ #templateHeader .mcnTextContent,#templateHeader .mcnTextContent p{
576
+ /*@editable*/font-size:16px !important;
577
+ /*@editable*/line-height:150% !important;
578
+ }
579
+
580
+ } @media only screen and (max-width: 480px){
581
+ /*
582
+ @tab Mobile Styles
583
+ @section Upper Body Text
584
+ @tip Make the upper body text larger in size for better readability on small screens. We recommend a font size of at least 16px.
585
+ */
586
+ #templateUpperBody .mcnTextContent,#templateUpperBody .mcnTextContent p{
587
+ /*@editable*/font-size:16px !important;
588
+ /*@editable*/line-height:150% !important;
589
+ }
590
+
591
+ } @media only screen and (max-width: 480px){
592
+ /*
593
+ @tab Mobile Styles
594
+ @section Column Text
595
+ @tip Make the column text larger in size for better readability on small screens. We recommend a font size of at least 16px.
596
+ */
597
+ #templateColumns .columnContainer .mcnTextContent,#templateColumns .columnContainer .mcnTextContent p{
598
+ /*@editable*/font-size:16px !important;
599
+ /*@editable*/line-height:150% !important;
600
+ }
601
+
602
+ } @media only screen and (max-width: 480px){
603
+ /*
604
+ @tab Mobile Styles
605
+ @section Lower Body Text
606
+ @tip Make the lower body text larger in size for better readability on small screens. We recommend a font size of at least 16px.
607
+ */
608
+ #templateLowerBody .mcnTextContent,#templateLowerBody .mcnTextContent p{
609
+ /*@editable*/font-size:16px !important;
610
+ /*@editable*/line-height:150% !important;
611
+ }
612
+
613
+ } @media only screen and (max-width: 480px){
614
+ /*
615
+ @tab Mobile Styles
616
+ @section Footer Text
617
+ @tip Make the footer content text larger in size for better readability on small screens.
618
+ */
619
+ #templateFooter .mcnTextContent,#templateFooter .mcnTextContent p{
620
+ /*@editable*/font-size:14px !important;
621
+ /*@editable*/line-height:150% !important;
622
+ }
623
+
624
+ }</style></head>
625
+ <body>
626
+ <!--*|IF:MC_PREVIEW_TEXT|*-->
627
+ <!--[if !gte mso 9]><!----><span class="mcnPreviewText" style="display:none; font-size:0px; line-height:0px; max-height:0px; max-width:0px; opacity:0; overflow:hidden; visibility:hidden; mso-hide:all;">*|MC_PREVIEW_TEXT|*</span><!--<![endif]-->
628
+ <!--*|END:IF|*-->
629
+ <center>
630
+ <table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
631
+ <tr>
632
+ <td align="center" valign="top" id="bodyCell">
633
+ <!-- BEGIN TEMPLATE // -->
634
+ <table border="0" cellpadding="0" cellspacing="0" width="100%">
635
+ <tr>
636
+ <td align="center" valign="top" id="templatePreheader">
637
+ <!--[if (gte mso 9)|(IE)]>
638
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;">
639
+ <tr>
640
+ <td align="center" valign="top" width="600" style="width:600px;">
641
+ <![endif]-->
642
+ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" class="templateContainer">
643
+ <tr>
644
+ <td valign="top" class="preheaderContainer"><table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnTextBlock" style="min-width:100%;">
645
+ <tbody class="mcnTextBlockOuter">
646
+ <tr>
647
+ <td valign="top" class="mcnTextBlockInner" style="padding-top:9px;">
648
+ <!--[if mso]>
649
+ <table align="left" border="0" cellspacing="0" cellpadding="0" width="100%" style="width:100%;">
650
+ <tr>
651
+ <![endif]-->
652
+
653
+ <!--[if mso]>
654
+ <td valign="top" width="600" style="width:600px;">
655
+ <![endif]-->
656
+ <table align="left" border="0" cellpadding="0" cellspacing="0" style="max-width:100%; min-width:100%;" width="100%" class="mcnTextContentContainer">
657
+ <tbody><tr>
658
+
659
+ <td valign="top" class="mcnTextContent" style="padding: 0px 18px 9px; text-align: center;">
660
+
661
+
662
+ </td>
663
+ </tr>
664
+ </tbody></table>
665
+ <!--[if mso]>
666
+ </td>
667
+ <![endif]-->
668
+
669
+ <!--[if mso]>
670
+ </tr>
671
+ </table>
672
+ <![endif]-->
673
+ </td>
674
+ </tr>
675
+ </tbody>
676
+ </table></td>
677
+ </tr>
678
+ </table>
679
+ <!--[if (gte mso 9)|(IE)]>
680
+ </td>
681
+ </tr>
682
+ </table>
683
+ <![endif]-->
684
+ </td>
685
+ </tr>
686
+ <tr>
687
+ <td align="center" valign="top" id="templateHeader">
688
+ <!--[if (gte mso 9)|(IE)]>
689
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;">
690
+ <tr>
691
+ <td align="center" valign="top" width="600" style="width:600px;">
692
+ <![endif]-->
693
+ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" class="templateContainer">
694
+ <tr>
695
+ <td valign="top" class="headerContainer"><table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnDividerBlock" style="min-width:100%;">
696
+ <tbody class="mcnDividerBlockOuter">
697
+ <tr>
698
+ <td class="mcnDividerBlockInner" style="min-width: 100%; padding: 10px 18px 18px;">
699
+ <table class="mcnDividerContent" border="0" cellpadding="0" cellspacing="0" width="100%" style="min-width:100%;">
700
+ <tbody><tr>
701
+ <td>
702
+ <span></span>
703
+ </td>
704
+ </tr>
705
+ </tbody></table>
706
+ <!--
707
+ <td class="mcnDividerBlockInner" style="padding: 18px;">
708
+ <hr class="mcnDividerContent" style="border-bottom-color:none; border-left-color:none; border-right-color:none; border-bottom-width:0; border-left-width:0; border-right-width:0; margin-top:0; margin-right:0; margin-bottom:0; margin-left:0;" />
709
+ -->
710
+ </td>
711
+ </tr>
712
+ </tbody>
713
+ </table><table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnImageBlock" style="min-width:100%;">
714
+ <tbody class="mcnImageBlockOuter">
715
+ <tr>
716
+ <td valign="top" style="padding:9px" class="mcnImageBlockInner">
717
+ <table align="left" width="100%" border="0" cellpadding="0" cellspacing="0" class="mcnImageContentContainer" style="min-width:100%;">
718
+ <tbody><tr>
719
+ <td class="mcnImageContent" valign="top" style="padding-right: 9px; padding-left: 9px; padding-top: 0; padding-bottom: 0; text-align:center;">
720
+
721
+
722
+ <img align="center" alt="" src="https://gallery.mailchimp.com/7acf83c7a47b32c740ad94a4e/images/35010455-0fe7-4f14-a9c4-253f6579937f.png" width="163" style="max-width:163px; padding-bottom: 0; display: inline !important; vertical-align: bottom;" class="mcnImage">
723
+
724
+
725
+ </td>
726
+ </tr>
727
+ </tbody></table>
728
+ </td>
729
+ </tr>
730
+ </tbody>
731
+ </table><table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnDividerBlock" style="min-width:100%;">
732
+ <tbody class="mcnDividerBlockOuter">
733
+ <tr>
734
+ <td class="mcnDividerBlockInner" style="min-width: 100%; padding: 10px 18px;">
735
+ <table class="mcnDividerContent" border="0" cellpadding="0" cellspacing="0" width="100%" style="min-width:100%;">
736
+ <tbody><tr>
737
+ <td>
738
+ <span></span>
739
+ </td>
740
+ </tr>
741
+ </tbody></table>
742
+ <!--
743
+ <td class="mcnDividerBlockInner" style="padding: 18px;">
744
+ <hr class="mcnDividerContent" style="border-bottom-color:none; border-left-color:none; border-right-color:none; border-bottom-width:0; border-left-width:0; border-right-width:0; margin-top:0; margin-right:0; margin-bottom:0; margin-left:0;" />
745
+ -->
746
+ </td>
747
+ </tr>
748
+ </tbody>
749
+ </table><table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnTextBlock" style="min-width:100%;">
750
+ <tbody class="mcnTextBlockOuter">
751
+ <tr>
752
+ <td valign="top" class="mcnTextBlockInner" style="padding-top:9px;">
753
+ <!--[if mso]>
754
+ <table align="left" border="0" cellspacing="0" cellpadding="0" width="100%" style="width:100%;">
755
+ <tr>
756
+ <![endif]-->
757
+
758
+ <!--[if mso]>
759
+ <td valign="top" width="600" style="width:600px;">
760
+ <![endif]-->
761
+ <table align="left" border="0" cellpadding="0" cellspacing="0" style="max-width:100%; min-width:100%;" width="100%" class="mcnTextContentContainer">
762
+ <tbody><tr>
763
+
764
+ <td valign="top" class="mcnTextContent" style="padding-top:0; padding-right:18px; padding-bottom:9px; padding-left:18px;">
765
+
766
+ <h1 class="null">Your Magic Link is Here!</h1>
767
+
768
+ </td>
769
+ </tr>
770
+ </tbody></table>
771
+ <!--[if mso]>
772
+ </td>
773
+ <![endif]-->
774
+
775
+ <!--[if mso]>
776
+ </tr>
777
+ </table>
778
+ <![endif]-->
779
+ </td>
780
+ </tr>
781
+ </tbody>
782
+ </table><table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnTextBlock" style="min-width:100%;">
783
+ <tbody class="mcnTextBlockOuter">
784
+ <tr>
785
+ <td valign="top" class="mcnTextBlockInner" style="padding-top:9px;">
786
+ <!--[if mso]>
787
+ <table align="left" border="0" cellspacing="0" cellpadding="0" width="100%" style="width:100%;">
788
+ <tr>
789
+ <![endif]-->
790
+
791
+ <!--[if mso]>
792
+ <td valign="top" width="600" style="width:600px;">
793
+ <![endif]-->
794
+ <table align="left" border="0" cellpadding="0" cellspacing="0" style="max-width:100%; min-width:100%;" width="100%" class="mcnTextContentContainer">
795
+ <tbody><tr>
796
+
797
+ <td valign="top" class="mcnTextContent" style="padding-top:0; padding-right:18px; padding-bottom:9px; padding-left:18px;">
798
+
799
+ <h4 class="null">Hi Saylor,<br>
800
+ Click the button to continue logging in.</h4>
801
+
802
+ </td>
803
+ </tr>
804
+ </tbody></table>
805
+ <!--[if mso]>
806
+ </td>
807
+ <![endif]-->
808
+
809
+ <!--[if mso]>
810
+ </tr>
811
+ </table>
812
+ <![endif]-->
813
+ </td>
814
+ </tr>
815
+ </tbody>
816
+ </table></td>
817
+ </tr>
818
+ </table>
819
+ <!--[if (gte mso 9)|(IE)]>
820
+ </td>
821
+ </tr>
822
+ </table>
823
+ <![endif]-->
824
+ </td>
825
+ </tr>
826
+ <tr>
827
+ <td align="center" valign="top" id="templateUpperBody">
828
+ <!--[if (gte mso 9)|(IE)]>
829
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;">
830
+ <tr>
831
+ <td align="center" valign="top" width="600" style="width:600px;">
832
+ <![endif]-->
833
+ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" class="templateContainer">
834
+ <tr>
835
+ <td valign="top" class="bodyContainer"><table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnButtonBlock" style="min-width:100%;">
836
+ <tbody class="mcnButtonBlockOuter">
837
+ <tr>
838
+ <td style="padding-top:0; padding-right:18px; padding-bottom:18px; padding-left:18px;" valign="top" align="center" class="mcnButtonBlockInner">
839
+ <table border="0" cellpadding="0" cellspacing="0" class="mcnButtonContentContainer" style="border-collapse: separate !important;border-radius: 3px;background-color: #F8D23A;">
840
+ <tbody>
841
+ <tr>
842
+ <td align="center" valign="middle" class="mcnButtonContent" style="font-family: Arial; font-size: 24px; padding: 22px;">
843
+ <a class="mcnButton " title="Login Now →" href="http://www.ithemes.com" target="_blank" style="font-weight: bold;letter-spacing: normal;line-height: 100%;text-align: center;text-decoration: none;color: #3C3A31;">Login Now →</a>
844
+ </td>
845
+ </tr>
846
+ </tbody>
847
+ </table>
848
+ </td>
849
+ </tr>
850
+ </tbody>
851
+ </table></td>
852
+ </tr>
853
+ </table>
854
+ <!--[if (gte mso 9)|(IE)]>
855
+ </td>
856
+ </tr>
857
+ </table>
858
+ <![endif]-->
859
+ </td>
860
+ </tr>
861
+ <tr>
862
+ <td align="center" valign="top" id="templateColumns">
863
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" class="templateContainer">
864
+ <tr>
865
+ <td valign="top">
866
+ <!--[if (gte mso 9)|(IE)]>
867
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;">
868
+ <tr>
869
+ <td align="center" valign="top" width="300" style="width:300px;">
870
+ <![endif]-->
871
+ <table align="left" border="0" cellpadding="0" cellspacing="0" width="300" class="columnWrapper">
872
+ <tr>
873
+ <td valign="top" class="columnContainer"></td>
874
+ </tr>
875
+ </table>
876
+ <!--[if (gte mso 9)|(IE)]>
877
+ </td>
878
+ <td align="center" valign="top" width="300" style="width:300px;">
879
+ <![endif]-->
880
+ <table align="left" border="0" cellpadding="0" cellspacing="0" width="300" class="columnWrapper">
881
+ <tr>
882
+ <td valign="top" class="columnContainer"></td>
883
+ </tr>
884
+ </table>
885
+ <!--[if (gte mso 9)|(IE)]>
886
+ </td>
887
+ </tr>
888
+ </table>
889
+ <![endif]-->
890
+ </td>
891
+ </tr>
892
+ </table>
893
+ </td>
894
+ </tr>
895
+ <tr>
896
+ <td align="center" valign="top" id="templateLowerBody">
897
+ <!--[if (gte mso 9)|(IE)]>
898
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;">
899
+ <tr>
900
+ <td align="center" valign="top" width="600" style="width:600px;">
901
+ <![endif]-->
902
+ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" class="templateContainer">
903
+ <tr>
904
+ <td valign="top" class="bodyContainer"></td>
905
+ </tr>
906
+ </table>
907
+ <!--[if (gte mso 9)|(IE)]>
908
+ </td>
909
+ </tr>
910
+ </table>
911
+ <![endif]-->
912
+ </td>
913
+ </tr>
914
+ <tr>
915
+ <td align="center" valign="top" id="templateFooter">
916
+ <!--[if (gte mso 9)|(IE)]>
917
+ <table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;">
918
+ <tr>
919
+ <td align="center" valign="top" width="600" style="width:600px;">
920
+ <![endif]-->
921
+ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" class="templateContainer">
922
+ <tr>
923
+ <td valign="top" class="footerContainer"><table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnImageBlock" style="min-width:100%;">
924
+ <tbody class="mcnImageBlockOuter">
925
+ <tr>
926
+ <td valign="top" style="padding:9px" class="mcnImageBlockInner">
927
+ <table align="left" width="100%" border="0" cellpadding="0" cellspacing="0" class="mcnImageContentContainer" style="min-width:100%;">
928
+ <tbody><tr>
929
+ <td class="mcnImageContent" valign="top" style="padding-right: 9px; padding-left: 9px; padding-top: 0; padding-bottom: 0; text-align:center;">
930
+
931
+
932
+ <img align="center" alt="" src="https://gallery.mailchimp.com/7acf83c7a47b32c740ad94a4e/images/39da2582-1fd8-4190-a085-eb45316d4cd9.png" width="50" style="max-width:50px; padding-bottom: 0; display: inline !important; vertical-align: bottom;" class="mcnImage">
933
+
934
+
935
+ </td>
936
+ </tr>
937
+ </tbody></table>
938
+ </td>
939
+ </tr>
940
+ </tbody>
941
+ </table><table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnTextBlock" style="min-width:100%;">
942
+ <tbody class="mcnTextBlockOuter">
943
+ <tr>
944
+ <td valign="top" class="mcnTextBlockInner" style="padding-top:9px;">
945
+ <!--[if mso]>
946
+ <table align="left" border="0" cellspacing="0" cellpadding="0" width="100%" style="width:100%;">
947
+ <tr>
948
+ <![endif]-->
949
+
950
+ <!--[if mso]>
951
+ <td valign="top" width="600" style="width:600px;">
952
+ <![endif]-->
953
+ <table align="left" border="0" cellpadding="0" cellspacing="0" style="max-width:100%; min-width:100%;" width="100%" class="mcnTextContentContainer">
954
+ <tbody><tr>
955
+
956
+ <td valign="top" class="mcnTextContent" style="padding-top:0; padding-right:18px; padding-bottom:9px; padding-left:18px;">
957
+
958
+ <span style="font-size:11px">This email was generated by the iThemes Security plugin on behalf of [website].<br>
959
+ To unsubscribe from these notifications, please&nbsp;<a data-saferedirecturl="https://www.google.com/url?q=https://saylor.dev.ithemes.com&amp;source=gmail&amp;ust=1564773724048000&amp;usg=AFQjCNG0ZQUquLozba102rtW848UVn3YYQ" href="https://saylor.dev.ithemes.com/" target="_blank"><span style="color:#3366ff">contact the site administrator</span></a><span style="color:#3366ff">.</span></span>
960
+ </td>
961
+ </tr>
962
+ </tbody></table>
963
+ <!--[if mso]>
964
+ </td>
965
+ <![endif]-->
966
+
967
+ <!--[if mso]>
968
+ </tr>
969
+ </table>
970
+ <![endif]-->
971
+ </td>
972
+ </tr>
973
+ </tbody>
974
+ </table></td>
975
+ </tr>
976
+ </table>
977
+ <!--[if (gte mso 9)|(IE)]>
978
+ </td>
979
+ </tr>
980
+ </table>
981
+ <![endif]-->
982
+ </td>
983
+ </tr>
984
+ </table>
985
+ <!-- // END TEMPLATE -->
986
+ </td>
987
+ </tr>
988
+ </table>
989
+ </center>
990
+ </body>
991
+ </html>
core/lib/schema.php CHANGED
@@ -51,6 +51,7 @@ CREATE TABLE {$wpdb->base_prefix}itsec_lockouts (
51
  lockout_user bigint(20) UNSIGNED,
52
  lockout_username varchar(60),
53
  lockout_active int(1) NOT NULL DEFAULT 1,
 
54
  PRIMARY KEY (lockout_id),
55
  KEY lockout_expire_gmt (lockout_expire_gmt),
56
  KEY lockout_host (lockout_host),
@@ -114,6 +115,16 @@ CREATE TABLE {$wpdb->base_prefix}itsec_fingerprints (
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' );
51
  lockout_user bigint(20) UNSIGNED,
52
  lockout_username varchar(60),
53
  lockout_active int(1) NOT NULL DEFAULT 1,
54
+ lockout_context TEXT,
55
  PRIMARY KEY (lockout_id),
56
  KEY lockout_expire_gmt (lockout_expire_gmt),
57
  KEY lockout_host (lockout_host),
115
  UNIQUE KEY fingerprint_user__hash (fingerprint_user,fingerprint_hash),
116
  UNIQUE KEY fingerprint_uuid (fingerprint_uuid)
117
  ) $charset_collate;
118
+
119
+ CREATE TABLE {$wpdb->base_prefix}itsec_opaque_tokens (
120
+ token_id char(64) NOT NULL,
121
+ token_hashed char(64) NOT NULL,
122
+ token_type VARCHAR(32) NOT NULL,
123
+ token_data TEXT NOT NULL,
124
+ token_created_at DATETIME NOT NULL,
125
+ PRIMARY KEY (token_id),
126
+ UNIQUE KEY token_hashed (token_hashed)
127
+ ) $charset_collate;
128
  ";
129
 
130
  require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
core/lib/validator.php CHANGED
@@ -112,7 +112,7 @@ abstract class ITSEC_Validator {
112
  }
113
  }
114
 
115
- final protected function sanitize_setting( $type, $var, $name, $prevent_save_on_error = true, $trim_value = true ) {
116
  $id = $this->get_id();
117
 
118
  if ( ! isset( $this->settings[$var] ) ) {
@@ -470,6 +470,10 @@ abstract class ITSEC_Validator {
470
  }
471
 
472
  if ( false !== $error ) {
 
 
 
 
473
  $this->add_error( $this->generate_error( $id, $var, $type, $error ) );
474
  $this->vars_to_skip_validate_matching_types[] = $var;
475
 
112
  }
113
  }
114
 
115
+ final protected function sanitize_setting( $type, $var, $name, $prevent_save_on_error = true, $trim_value = true, $custom_error = '' ) {
116
  $id = $this->get_id();
117
 
118
  if ( ! isset( $this->settings[$var] ) ) {
470
  }
471
 
472
  if ( false !== $error ) {
473
+ if ( $custom_error ) {
474
+ $error = $custom_error;
475
+ }
476
+
477
  $this->add_error( $this->generate_error( $id, $var, $type, $error ) );
478
  $this->vars_to_skip_validate_matching_types[] = $var;
479
 
core/lockout.php CHANGED
@@ -2,10 +2,20 @@
2
  /**
3
  * Handles lockouts for modules and core
4
  *
5
- * @package iThemes-Security
6
  * @since 4.0
 
7
  */
8
 
 
 
 
 
 
 
 
 
 
 
9
  /**
10
  * Class ITSEC_Lockout
11
  *
@@ -83,9 +93,6 @@ final class ITSEC_Lockout {
83
  //Register all plugin modules
84
  add_action( 'plugins_loaded', array( $this, 'register_modules' ) );
85
 
86
- //Set an error message on improper logout
87
- add_action( 'login_head', array( $this, 'set_lockout_error' ) );
88
-
89
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
90
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
91
 
@@ -135,8 +142,12 @@ final class ITSEC_Lockout {
135
 
136
  $host = ITSEC_Lib::get_ip();
137
 
138
- if ( $this->is_host_locked_out( $host ) || ITSEC_Lib::is_ip_blacklisted() ) {
139
- $this->execute_lock();
 
 
 
 
140
  }
141
  }
142
 
@@ -145,88 +156,92 @@ final class ITSEC_Lockout {
145
  *
146
  * @since 4.0
147
  *
148
- * @param mixed $user WordPress user object or false.
149
- * @param mixed $username The username to check.
150
- * @param string $type Lockout type asking for the check.
151
  *
152
  * @return void
153
  */
154
  public function check_lockout( $user = false, $username = false, $type = '' ) {
155
- global $wpdb;
 
156
 
157
- $wpdb->hide_errors(); //Hide database errors in case the tables aren't there
158
-
159
- $host = ITSEC_Lib::get_ip();
160
- $username = sanitize_text_field( trim( $username ) );
161
- $username_check = false;
162
- $user_check = false;
163
- $host_check = false;
164
-
165
- if ( is_object( $user ) && is_a( $user, 'WP_User' ) ) {
166
 
 
167
  $user_id = $user->ID;
168
-
169
- } else if ( ! empty( $user ) ) {
170
-
171
- $user = get_userdata( intval( $user ) );
172
- $user_id = $user->ID;
173
-
174
  } else {
175
-
176
  $user = wp_get_current_user();
177
  $user_id = $user->ID;
178
-
179
- if ( $username !== false && $username != '' ) {
180
- $username_check = $wpdb->get_results( $wpdb->prepare(
181
- "SELECT `lockout_username`, `lockout_type` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_username`= %s;",
182
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $username
183
- ) );
184
- }
185
-
186
- $host_check = $wpdb->get_results( $wpdb->prepare(
187
- "SELECT `lockout_host`, `lockout_type` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_host`= %s;",
188
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $host
189
- ) );
190
-
191
  }
192
 
193
- if ( $user_id !== 0 && $user_id !== null ) {
194
-
195
- $user_check = $wpdb->get_results( $wpdb->prepare(
196
- "SELECT `lockout_user`, `lockout_type` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_user`= %d;",
197
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $user_id
198
- ) );
199
  }
200
 
201
- $error = $wpdb->last_error;
202
-
203
- if ( strlen( trim( $error ) ) > 0 ) {
204
- ITSEC_Lib::create_database_tables();
205
  }
206
 
207
- if ( $host_check ) {
 
 
208
 
209
- $type = $type ? $type : $host_check[0]->lockout_type;
210
- $this->execute_lock( array( 'type' => $type ) );
211
 
212
- } elseif ( $user_check || $username_check ) {
 
 
213
 
214
- if ( ! $type ) {
215
- $type = $user_check ? $user_check[0]->lockout_type : $username_check[0]->lockout_type;
216
- }
217
 
218
- $lock_context = array( 'user_lock' => true, 'type' => $type );
 
 
 
 
 
 
 
 
 
 
219
 
220
- if ( $user ) {
221
- $lock_context['user'] = $user;
222
- } elseif ( $username ) {
223
- $lock_context['username'] = $username;
224
- }
 
 
 
 
 
 
 
 
225
 
226
- $this->execute_lock( $lock_context );
 
 
 
 
227
 
 
 
228
  }
229
 
 
 
 
 
 
230
  }
231
 
232
  /**
@@ -292,158 +307,468 @@ final class ITSEC_Lockout {
292
  *
293
  * @since 4.0
294
  *
295
- * @param string $module string name of the calling module
296
- * @param string $username username of user
297
  *
298
  * @return void
299
  */
300
- public function do_lockout( $module, $username = false ) {
301
- global $wpdb;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
- if ( ! isset( $this->lockout_modules[$module] ) ) {
304
  return;
305
  }
306
 
 
 
 
307
 
308
- // TODO: Ensure that this is not needed and remove it.
309
- $wpdb->hide_errors(); //Hide database errors in case the tables aren't there
310
-
311
 
312
- $lock_host = false;
313
- $lock_user_id = false;
314
- $lock_username = false;
315
- $options = $this->lockout_modules[$module];
316
 
317
- $lockout_event_data = array(
318
  'temp_type' => $options['type'],
319
  'temp_date' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() ),
320
  'temp_date_gmt' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
321
  );
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
- if ( isset( $options['host'] ) && $options['host'] > 0 ) {
325
- $host = ITSEC_Lib::get_ip();
 
 
326
 
327
- $host_lockout_event_data = $lockout_event_data;
328
- $host_lockout_event_data['temp_host'] = $host;
329
 
330
- $wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $host_lockout_event_data );
331
 
332
- $host_count = $wpdb->get_var(
333
- $wpdb->prepare(
334
- "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_host` = %s",
335
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
336
- $host
337
- )
338
- );
339
 
340
- if ( $host_count >= $options['host'] ) {
341
- $lock_host = $host;
342
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  }
344
 
345
- if ( false !== $username && isset( $options['user'] ) && $options['user'] > 0 ) {
346
- $username = sanitize_text_field( $username );
347
- $user_id = username_exists( $username );
 
348
 
349
- if ( false !== $user_id ) {
350
- $user_lockout_event_data = $lockout_event_data;
351
- $user_lockout_event_data['temp_user'] = $user_id;
352
- $user_lockout_event_data['temp_username'] = $username;
353
 
354
- $wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $user_lockout_event_data );
 
 
355
 
356
- $user_count = $wpdb->get_var(
357
- $wpdb->prepare(
358
- "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND (`temp_username` = %s OR `temp_user` = %d)",
359
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
360
- $username,
361
- $user_id
362
- )
363
- );
364
 
365
- if ( $user_count >= $options['user'] ) {
366
- $lock_user_id = $user_id;
367
- }
368
- } else {
369
- $user_lockout_event_data = $lockout_event_data;
370
- $user_lockout_event_data['temp_username'] = $username;
371
 
372
- $wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $user_lockout_event_data );
373
 
374
- $user_count = $wpdb->get_var(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  $wpdb->prepare(
376
- "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_username` = %s",
377
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
378
- $username
379
  )
380
  );
381
 
382
- if ( $user_count >= $options['user'] ) {
383
- $lock_username = $username;
 
 
 
 
 
 
 
384
  }
385
  }
386
  }
387
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
389
- $error = $wpdb->last_error;
 
 
 
390
 
391
- // TODO: Confirm that this scenario can never happen and remove this
392
- if ( strlen( trim( $error ) ) > 0 ) {
393
- ITSEC_Lib::create_database_tables();
394
  }
395
 
 
 
 
 
396
 
397
- if ( false !== $lock_host || false !== $lock_user_id || false !== $lock_username ) {
398
- $this->lockout( $module, $lock_host, $lock_user_id, $lock_username );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  }
401
 
402
  /**
403
  * Executes lockout (locks user out)
404
  *
405
- * @param array $context
406
- * @param bool $deprecated Deprecated argument. Previously whether this is a network lock.
407
  *
408
  * @return void
409
  */
410
- public function execute_lock( $context = array(), $deprecated = false ) {
 
 
411
 
412
- if ( func_num_args() > 1 ) {
413
- _deprecated_argument( __METHOD__, '6.5.0', 'A network lockout should be specified in the $context parameter.' );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  }
415
 
416
- if ( is_array( $context ) ) {
417
- $context = wp_parse_args( $context, array( 'user_lock' => false, 'network_lock' => false, 'type' => '' ) );
418
- $user = $context['user_lock'];
419
- $network = $context['network_lock'];
420
- } else {
421
- $user = $context;
422
- $network = $deprecated;
423
  }
424
 
425
  if ( ITSEC_Lib::is_ip_whitelisted( ITSEC_Lib::get_ip() ) ) {
426
  return;
427
  }
428
 
429
- if ( $network === true ) { //lockout triggered by iThemes Network
 
 
 
 
430
 
431
  $message = ITSEC_Modules::get_setting( 'global', 'community_lockout_message' );
432
 
433
  if ( ! $message ) {
434
  $message = __( 'Your IP address has been flagged as a threat by the iThemes Security network.', 'better-wp-security' );
435
  }
436
-
437
- } elseif ( $user === true ) { //lockout the user
438
 
439
  $message = ITSEC_Modules::get_setting( 'global', 'user_lockout_message' );
440
 
441
  if ( ! $message ) {
442
- $message = __( 'You have been locked out due to too many invalid login attempts.', 'better-wp-security' );
443
  }
444
-
445
- } else { //just lockout the host
446
-
447
  $message = ITSEC_Modules::get_setting( 'global', 'lockout_message' );
448
 
449
  if ( ! $message ) {
@@ -451,26 +776,23 @@ final class ITSEC_Lockout {
451
  }
452
  }
453
 
454
- $formatted = false;
455
 
456
- if ( $context['type'] ) {
457
- /**
458
- * Filter the lockout message displayed to the user.
459
- *
460
- * @param string $message
461
- * @param string $type
462
- * @param array $context
463
- */
464
- $message = apply_filters( "itsec_{$context['type']}_lockout_message", $message, $context );
465
 
 
466
  /**
467
- * Filter whether to print the lockout error message with formatting or not.
468
  *
469
- * @param bool $formatted
470
- * @param string $type
471
- * @param array $context
472
  */
473
- $formatted = apply_filters( "itsec_{$context['type']}_lockout_format_message", false, $context );
474
  }
475
 
476
  $current_user = wp_get_current_user();
@@ -479,28 +801,20 @@ final class ITSEC_Lockout {
479
  wp_logout();
480
  }
481
 
482
- if ( $formatted ) {
483
- wp_die( $message, '', array( 'response' => 403 ) );
484
- } else {
485
- @header( 'HTTP/1.0 403 Forbidden' );
486
- @header( 'Cache-Control: no-cache, must-revalidate, max-age=0' );
487
- @header( 'Expires: Thu, 22 Jun 1978 00:28:00 GMT' );
488
- @header( 'Pragma: no-cache' );
489
 
490
- add_filter( 'wp_die_handler', array( $this, 'apply_wp_die_handler' ) );
491
- add_filter( 'wp_die_ajax_handler', array( $this, 'apply_wp_die_handler' ) );
492
- add_filter( 'wp_die_xmlrpc_handler', array( $this, 'apply_wp_die_handler' ) );
493
- wp_die( $message, '', array( 'response' => 403 ) );
494
- }
495
- }
496
 
497
- /**
498
- * Apply the Scalar wp die handler to print a message to the screen.
499
- *
500
- * @return string
501
- */
502
- public function apply_wp_die_handler() {
503
- return '_scalar_wp_die_handler';
 
 
504
  }
505
 
506
  /**
@@ -517,7 +831,7 @@ final class ITSEC_Lockout {
517
  $global_settings_url = add_query_arg( array( 'module_type', 'all' ), $global_settings_url );
518
  }
519
 
520
- $description = '<h4>' . __( 'About Lockouts', 'better-wp-security' ) . '</h4>';
521
  $description .= '<p>';
522
  $description .= sprintf( __( 'Your lockout settings can be configured in <a href="%s" data-module-link="global">Global Settings</a>.', 'better-wp-security' ), esc_url( $global_settings_url ) );
523
  $description .= '<br />';
@@ -545,8 +859,8 @@ final class ITSEC_Lockout {
545
  *
546
  * @since 4.0
547
  *
548
- * @param string $type 'all', 'host', 'user' or 'username'.
549
- * @param array $args Additional arguments.
550
  *
551
  * @return array all lockouts in the system
552
  */
@@ -667,7 +981,7 @@ final class ITSEC_Lockout {
667
 
668
  if ( ! is_array( $whitelist ) ) {
669
  $whitelist = array();
670
- } else if ( isset( $whitelist['ip'] ) ) {
671
  // Update old format
672
  $whitelist = array(
673
  $whitelist['ip'] => $whitelist['exp'] - ITSEC_Core::get_time_offset(),
@@ -702,11 +1016,11 @@ final class ITSEC_Lockout {
702
  * @param string $ip
703
  */
704
  public function add_to_temp_whitelist( $ip ) {
705
- $whitelist = $this->get_temp_whitelist();
706
- $expiration = ITSEC_Core::get_current_time_gmt() + DAY_IN_SECONDS;
707
  $refresh_expiration = $expiration - HOUR_IN_SECONDS;
708
 
709
- if ( isset( $whitelist[$ip] ) && $whitelist[$ip] > $refresh_expiration ) {
710
  // An update is not needed yet.
711
  return;
712
  }
@@ -714,11 +1028,11 @@ final class ITSEC_Lockout {
714
  // Remove expired entries.
715
  foreach ( $whitelist as $cached_ip => $cached_expiration ) {
716
  if ( $cached_expiration < ITSEC_Core::get_current_time_gmt() ) {
717
- unset( $whitelist[$cached_ip] );
718
  }
719
  }
720
 
721
- $whitelist[$ip] = $expiration;
722
 
723
  update_site_option( 'itsec_temp_whitelist_ip', $whitelist );
724
  }
@@ -731,11 +1045,11 @@ final class ITSEC_Lockout {
731
  public function remove_from_temp_whitelist( $ip ) {
732
  $whitelist = $this->get_temp_whitelist();
733
 
734
- if ( ! isset( $whitelist[$ip] ) ) {
735
  return;
736
  }
737
 
738
- unset( $whitelist[$ip] );
739
 
740
  update_site_option( 'itsec_temp_whitelist_ip', $whitelist );
741
  }
@@ -759,219 +1073,15 @@ final class ITSEC_Lockout {
759
  }
760
 
761
  $whitelist = $this->get_temp_whitelist();
762
- $ip = ITSEC_Lib::get_ip();
763
 
764
- if ( isset( $whitelist[$ip] ) && $whitelist[$ip] > ITSEC_Core::get_current_time() ) {
765
  return true;
766
  }
767
 
768
  return false;
769
  }
770
 
771
- /**
772
- * Create a lockout.
773
- *
774
- * @param array $args
775
- *
776
- * @return array
777
- */
778
- public function create_lockout( $args = array() ) {
779
- global $wpdb;
780
-
781
- $args = wp_parse_args( $args, array(
782
- 'module' => '',
783
- 'host' => false,
784
- 'user_id' => false,
785
- 'username' => false,
786
- ) );
787
-
788
- $module = $args['module'];
789
- $host = $args['host'];
790
- $user_id = $args['user_id'];
791
- $username = $args['username'];
792
-
793
- $module_details = $this->lockout_modules[ $module ];
794
-
795
- $whitelisted = ITSEC_Lib::is_ip_whitelisted( $host );
796
- $blacklisted = false;
797
-
798
- $log_data = array(
799
- 'module' => $module,
800
- 'host' => $host,
801
- 'user_id' => $user_id,
802
- 'username' => $username,
803
- 'module_details' => $module_details,
804
- 'whitelisted' => $whitelisted,
805
- 'blacklisted' => false,
806
- );
807
-
808
-
809
- // Do a permanent ban if enabled and settings criteria are met.
810
- if ( ITSEC_Modules::get_setting( 'global', 'blacklist' ) && false !== $host ) {
811
- $blacklist_count = ITSEC_Modules::get_setting( 'global', 'blacklist_count' );
812
- $blacklist_period = ITSEC_Modules::get_setting( 'global', 'blacklist_period', 7 );
813
- $blacklist_seconds = $blacklist_period * DAY_IN_SECONDS;
814
-
815
- $host_count = 1 + $wpdb->get_var(
816
- $wpdb->prepare(
817
- "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_expire_gmt` > %s AND `lockout_host`= %s",
818
- date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - $blacklist_seconds ),
819
- $host
820
- )
821
- );
822
-
823
- if ( $host_count >= $blacklist_count ) {
824
- $blacklisted = true;
825
- $log_data['blacklisted'] = true;
826
-
827
- if ( $whitelisted ) {
828
- ITSEC_Log::add_notice( 'lockout', 'whitelisted-host-triggered-blacklist', array_merge( $log_data, compact( 'blacklist_period', 'blacklist_count', 'host_count' ) ) );
829
- } else {
830
- $this->blacklist_ip( $host );
831
- ITSEC_Log::add_action( 'lockout', 'host-triggered-blacklist', array_merge( $log_data, compact( 'blacklist_period', 'blacklist_count', 'host_count' ) ) );
832
- }
833
- }
834
- }
835
-
836
-
837
- $host_expiration = false;
838
- $user_expiration = false;
839
- $id = false;
840
-
841
- $lockouts_data = array(
842
- 'lockout_type' => $module_details['type'],
843
- 'lockout_start' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() ),
844
- 'lockout_start_gmt' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
845
- );
846
-
847
- if ( $whitelisted ) {
848
- $lockouts_data['lockout_expire'] = date( 'Y-m-d H:i:s', 1 );
849
- $lockouts_data['lockout_expire_gmt'] = date( 'Y-m-d H:i:s', 1 );
850
- } else {
851
- $exp_seconds = ITSEC_Modules::get_setting( 'global', 'lockout_period' ) * MINUTE_IN_SECONDS;
852
-
853
- $lockouts_data['lockout_expire'] = date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() + $exp_seconds );
854
- $lockouts_data['lockout_expire_gmt'] = date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() + $exp_seconds );
855
- }
856
-
857
- if ( false !== $host && ! $blacklisted ) {
858
- $host_expiration = $lockouts_data['lockout_expire'];
859
- $id = $this->add_lockout_to_db( 'host', $host, $whitelisted, $lockouts_data, $log_data );
860
- }
861
-
862
- if ( false !== $user_id ) {
863
- $user_expiration = $lockouts_data['lockout_expire'];
864
- $id = $this->add_lockout_to_db( 'user', $user_id, $whitelisted, $lockouts_data, $log_data );
865
- }
866
-
867
- if ( false !== $username ) {
868
- $user_expiration = $lockouts_data['lockout_expire'];
869
- $id = $this->add_lockout_to_db( 'username', $username, $whitelisted, $lockouts_data, $log_data );
870
- }
871
-
872
- return compact( 'id', 'host_expiration', 'user_expiration', 'whitelisted', 'blacklisted', 'module_details' );
873
- }
874
-
875
- /**
876
- * Store a record of the locked out user/host or permanently ban the host.
877
- *
878
- * Permanently banned hosts will be forwarded to the ban-users module via the itsec-new-blacklisted-ip hook and
879
- * not persisted to the database.
880
- *
881
- * If configured, notifies the configured email addresses of the lockout.
882
- *
883
- * @since 4.0
884
- *
885
- * @param string $module The module triggering the lockout.
886
- * @param string|bool $host Host to lock out or false if the host should not be locked out.
887
- * @param int|bool $user_id User ID to lockout or false if the host should not be locked out.
888
- * @param string|bool $username Username to lockout or false if the host should not be locked out.
889
- *
890
- * @return void
891
- */
892
- private function lockout( $module, $host, $user_id, $username ) {
893
- global $wpdb;
894
-
895
-
896
- $lock = "lockout_$host$user_id$username";
897
-
898
- // Acquire a lock to prevent a lockout being created more than once by a particularly fast attacker.
899
- if ( ! ITSEC_Lib::get_lock( $lock, 180 ) ) {
900
- return;
901
- }
902
-
903
- $details = $this->create_lockout( compact( 'module', 'host', 'user_id', 'username' ) );
904
-
905
- if ( $details['whitelisted'] ) {
906
- // No need to send an email notice when the host is whitelisted.
907
- ITSEC_Lib::release_lock( $lock );
908
-
909
- return;
910
- }
911
-
912
- $this->send_lockout_email(
913
- $host,
914
- $user_id,
915
- $username,
916
- $details['host_expiration'],
917
- $details['user_expiration'],
918
- $details['module_details']['reason']
919
- );
920
-
921
- $lock_context = array(
922
- 'type' => $details['module_details']['type'],
923
- );
924
-
925
- if ( false !== $user_id ) {
926
- $lock_context['user'] = get_userdata( $user_id );
927
- } elseif ( false !== $username ) {
928
- $lock_context['username'] = $username;
929
- }
930
-
931
- if ( false === $host ) {
932
- $lock_context['user_lock'] = true;
933
- }
934
-
935
- ITSEC_Lib::release_lock( $lock );
936
- $this->execute_lock( $lock_context );
937
- }
938
-
939
- /**
940
- * Adds a record of a lockout event to the database and log the event.
941
- *
942
- * @param string $type The type of lockout: "host", "user", "username".
943
- * @param string|int $id The value for the type: host's IP, user's ID, username.
944
- * @param bool $whitelisted Whether or not the host triggering the event is whitelisted.
945
- * @param array $lockout_data Array of base data to be inserted.
946
- * @param array $log_data Array of data to be logged for the event.
947
- *
948
- * @return int|false
949
- */
950
- private function add_lockout_to_db( $type, $id, $whitelisted, $lockout_data, $log_data ) {
951
- global $wpdb;
952
-
953
- $lockout_data["lockout_$type"] = $id;
954
-
955
- $result = $wpdb->insert( "{$wpdb->base_prefix}itsec_lockouts", $lockout_data );
956
- $insert_id = $result ? $wpdb->insert_id : false;
957
-
958
- if ( $whitelisted ) {
959
- ITSEC_Log::add_notice( 'lockout', "whitelisted-host-triggered-$type-lockout", array_merge( $log_data, $lockout_data ) );
960
- } else {
961
- if ( 'host' === $type ) {
962
- $code = "host-lockout::{$log_data['host']}";
963
- } else if ( 'user' === $type ) {
964
- $code = "user-lockout::{$log_data['user_id']}";
965
- } else if ( 'username' === $type ) {
966
- $code = "username-lockout::{$log_data['username']}";
967
- }
968
-
969
- ITSEC_Log::add_action( 'lockout', $code, array_merge( $log_data, $lockout_data ) );
970
- }
971
-
972
- return $insert_id;
973
- }
974
-
975
  /**
976
  * Inserts an IP address into the htaccess ban list.
977
  *
@@ -1041,7 +1151,7 @@ final class ITSEC_Lockout {
1041
  *
1042
  * @since 3.6.0
1043
  *
1044
- * @param array $verbs of verbs.
1045
  *
1046
  * @return array Array of verbs.
1047
  */
@@ -1085,11 +1195,13 @@ final class ITSEC_Lockout {
1085
  /**
1086
  * Get lockout details.
1087
  *
1088
- * @param int $id
 
1089
  *
1090
- * @return array|false
 
1091
  */
1092
- public function get_lockout( $id ) {
1093
  global $wpdb;
1094
 
1095
  $results = $wpdb->get_results( $wpdb->prepare(
@@ -1101,7 +1213,13 @@ final class ITSEC_Lockout {
1101
  return false;
1102
  }
1103
 
1104
- return $results[0];
 
 
 
 
 
 
1105
  }
1106
 
1107
  /**
@@ -1113,32 +1231,22 @@ final class ITSEC_Lockout {
1113
  *
1114
  * @return bool true on success or false
1115
  */
1116
- public function release_lockout( $id = null ) {
1117
-
1118
  global $wpdb;
1119
 
1120
- if ( $id !== null && trim( $id ) !== '' ) {
1121
-
1122
- $lockout = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE lockout_id = %d;", $id ), ARRAY_A );
1123
-
1124
- if ( is_array( $lockout ) && count( $lockout ) >= 1 ) {
1125
-
1126
- $success = $wpdb->update(
1127
- $wpdb->base_prefix . 'itsec_lockouts',
1128
- array(
1129
- 'lockout_active' => 0,
1130
- ),
1131
- array(
1132
- 'lockout_id' => (int) $id,
1133
- )
1134
- );
1135
-
1136
- return (bool) $success;
1137
-
1138
- }
1139
  }
1140
 
1141
- return false;
 
 
 
 
 
 
 
 
1142
  }
1143
 
1144
  /**
@@ -1177,16 +1285,14 @@ final class ITSEC_Lockout {
1177
  *
1178
  * @since 4.0
1179
  *
1180
- * @param string $host the host to lockout
1181
- * @param int $user_id the user id to lockout
1182
- * @param string $username the username to lockout
1183
- * @param string $host_expiration when the host login expires
1184
- * @param string $user_expiration when the user lockout expires
1185
- * @param string $reason the reason for the lockout to show to the user
1186
  *
1187
  * @return void
1188
  */
1189
- private function send_lockout_email( $host, $user_id, $username, $host_expiration, $user_expiration, $reason ) {
1190
 
1191
  $nc = ITSEC_Core::get_notification_center();
1192
 
@@ -1194,29 +1300,39 @@ final class ITSEC_Lockout {
1194
  return;
1195
  }
1196
 
1197
- $lockouts = array();
1198
- $show_remove_ip_ban_message = false;
1199
  $show_remove_lockout_message = false;
1200
 
1201
- if ( false !== $user_id ) {
1202
- $user = get_userdata( $user_id );
1203
- $username = $user->user_login;
1204
- }
1205
-
1206
- if ( false !== $username ) {
1207
  $show_remove_lockout_message = true;
1208
 
1209
  $lockouts[] = array(
1210
  'type' => 'user',
 
 
 
 
 
 
 
 
 
 
 
 
1211
  'id' => $username,
1212
  'until' => $user_expiration,
1213
  'reason' => $reason,
1214
  );
1215
  }
1216
 
1217
- if ( false !== $host ) {
1218
  if ( false === $host_expiration ) {
1219
- $host_expiration = __( 'Permanently', 'better-wp-security' );
1220
  $show_remove_ip_ban_message = true;
1221
  } else {
1222
  $show_remove_lockout_message = true;
@@ -1224,13 +1340,12 @@ final class ITSEC_Lockout {
1224
 
1225
  $lockouts[] = array(
1226
  'type' => 'host',
1227
- 'id' => '<a href="' . esc_url( ITSEC_Lib::get_trace_ip_link( $host ) ) . '">' . $host . '</a>',
1228
  'until' => $host_expiration,
1229
  'reason' => $reason,
1230
  );
1231
  }
1232
 
1233
-
1234
  $mail = $nc->mail();
1235
 
1236
  $mail->add_header( esc_html__( 'Site Lockout Notification', 'better-wp-security' ), esc_html__( 'Site Lockout Notification', 'better-wp-security' ) );
@@ -1248,7 +1363,6 @@ final class ITSEC_Lockout {
1248
 
1249
  $mail->add_footer();
1250
 
1251
-
1252
  $subject = $mail->prepend_site_url_to_subject( $nc->get_subject( 'lockout' ) );
1253
  $subject = apply_filters( 'itsec_lockout_email_subject', $subject );
1254
  $mail->set_subject( $subject, false );
@@ -1256,38 +1370,24 @@ final class ITSEC_Lockout {
1256
  $nc->send( 'lockout', $mail );
1257
  }
1258
 
1259
- /**
1260
- * Sets an error message when a user has been forcibly logged out due to lockout
1261
- *
1262
- * @return string
1263
- */
1264
- public function set_lockout_error() {
1265
-
1266
- //check to see if it's the logout screen
1267
- if ( isset( $_GET['itsec'] ) && $_GET['itsec'] == true ) {
1268
- return '<div id="login_error">' . ITSEC_Modules::get_setting( 'global', 'user_lockout_message' ) . '</div>' . PHP_EOL;
1269
- }
1270
-
1271
- }
1272
-
1273
  public function filter_entry_for_list_display( $entry, $code, $data ) {
1274
  $entry['module_display'] = esc_html__( 'Lockout', 'better-wp-security' );
1275
 
1276
  if ( 'whitelisted-host-triggered-blacklist' === $code ) {
1277
  $entry['description'] = esc_html__( 'Whitelisted Host Triggered Blacklist', 'better-wp-security' );
1278
- } else if ( 'host-triggered-blacklist' === $code ) {
1279
  $entry['description'] = esc_html__( 'Host Triggered Blacklist', 'better-wp-security' );
1280
- } else if ( 'whitelisted-host-triggered-host-lockout' === $code ) {
1281
  $entry['description'] = esc_html__( 'Whitelisted Host Triggered Host Lockout', 'better-wp-security' );
1282
- } else if ( 'host-lockout' === $code ) {
1283
  if ( isset( $data[0] ) ) {
1284
  $entry['description'] = sprintf( wp_kses( __( 'Host Lockout: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), $data[0] );
1285
  } else {
1286
  $entry['description'] = esc_html__( 'Host Lockout', 'better-wp-security' );
1287
  }
1288
- } else if ( 'whitelisted-host-triggered-user-lockout' === $code ) {
1289
  $entry['description'] = esc_html__( 'Whitelisted Host Triggered User Lockout', 'better-wp-security' );
1290
- } else if ( 'user-lockout' === $code ) {
1291
  if ( isset( $data[0] ) ) {
1292
  $user = get_user_by( 'id', $data[0] );
1293
  }
@@ -1297,9 +1397,9 @@ final class ITSEC_Lockout {
1297
  } else {
1298
  $entry['description'] = esc_html__( 'User Lockout', 'better-wp-security' );
1299
  }
1300
- } else if ( 'whitelisted-host-triggered-username-lockout' === $code ) {
1301
  $entry['description'] = esc_html__( 'Whitelisted Host Triggered Username Lockout', 'better-wp-security' );
1302
- } else if ( 'username-lockout' === $code ) {
1303
  if ( isset( $data[0] ) ) {
1304
  $entry['description'] = sprintf( wp_kses( __( 'Username Lockout: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), $data[0] );
1305
  } else {
2
  /**
3
  * Handles lockouts for modules and core
4
  *
 
5
  * @since 4.0
6
+ * @package iThemes-Security
7
  */
8
 
9
+ use iThemesSecurity\Lib\Lockout;
10
+ use iThemesSecurity\Lib\Lockout\Execute_Lock;
11
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Source\Configurable;
12
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Source\Lockout_Module;
13
+
14
+ require_once( __DIR__ . '/lib/lockout/execute-lock/source/interface-source.php' );
15
+ require_once( __DIR__ . '/lib/lockout/execute-lock/abstract-context.php' );
16
+ require_once( __DIR__ . '/lib/lockout/class-lockout.php' );
17
+ require_once( __DIR__ . '/lib/lockout/abstract-context.php' );
18
+
19
  /**
20
  * Class ITSEC_Lockout
21
  *
93
  //Register all plugin modules
94
  add_action( 'plugins_loaded', array( $this, 'register_modules' ) );
95
 
 
 
 
96
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
97
  add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
98
 
142
 
143
  $host = ITSEC_Lib::get_ip();
144
 
145
+ if ( ITSEC_Lib::is_ip_blacklisted() ) {
146
+ $this->execute_lock( new Execute_Lock\Host_Context( new Configurable( 'blacklist' ), $host ) );
147
+ }
148
+
149
+ if ( $lockout = $this->find_lockout( 'host', $host ) ) {
150
+ $this->execute_lock( $lockout->make_execute_lock_context() );
151
  }
152
  }
153
 
156
  *
157
  * @since 4.0
158
  *
159
+ * @param WP_User|int|false $user WordPress user object or false.
160
+ * @param string|false $username The username to check.
161
+ * @param string $type Lockout type asking for the check.
162
  *
163
  * @return void
164
  */
165
  public function check_lockout( $user = false, $username = false, $type = '' ) {
166
+ $host = ITSEC_Lib::get_ip();
167
+ $username = sanitize_text_field( trim( $username ) );
168
 
169
+ $lockout = null;
 
 
 
 
 
 
 
 
170
 
171
+ if ( $user instanceof WP_User ) {
172
  $user_id = $user->ID;
173
+ } elseif ( $user ) {
174
+ $user = get_userdata( (int) $user );
175
+ $user_id = $user ? $user->ID : 0;
 
 
 
176
  } else {
 
177
  $user = wp_get_current_user();
178
  $user_id = $user->ID;
179
+ $lockout = $this->find_lockout( 'host', $host );
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
 
182
+ if ( ! $lockout && $user_id ) {
183
+ $lockout = $this->find_lockout( 'user', $user_id );
 
 
 
 
184
  }
185
 
186
+ // Only check for a username lockout if no user ID was passed.
187
+ if ( ! $lockout && ! $user_id && $username ) {
188
+ $lockout = $this->find_lockout( 'username', $username );
 
189
  }
190
 
191
+ if ( ! $lockout ) {
192
+ return;
193
+ }
194
 
195
+ $context = $lockout->make_execute_lock_context();
 
196
 
197
+ if ( $type && $context->get_source()->get_source_slug() !== $type ) {
198
+ $context = $context->with_source( new Lockout_Module( $type ) );
199
+ }
200
 
201
+ $this->execute_lock( $context );
202
+ }
 
203
 
204
+ /**
205
+ * Find the lockout for a given type and identifier.
206
+ *
207
+ * @param string $type
208
+ * @param string $identifier
209
+ *
210
+ * @return Lockout\Lockout|null
211
+ */
212
+ private function find_lockout( $type, $identifier ) {
213
+ /** @var wpdb $wpdb */
214
+ global $wpdb;
215
 
216
+ switch ( $type ) {
217
+ case 'username':
218
+ $field = 'lockout_username';
219
+ break;
220
+ case 'user':
221
+ $field = 'lockout_user';
222
+ break;
223
+ case 'host':
224
+ $field = 'lockout_host';
225
+ break;
226
+ default:
227
+ return null;
228
+ }
229
 
230
+ $data = $wpdb->get_row( $wpdb->prepare(
231
+ "SELECT * FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `{$field}` = %s ORDER BY `lockout_start` DESC LIMIT 1;",
232
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
233
+ $identifier
234
+ ), ARRAY_A );
235
 
236
+ if ( ! $data ) {
237
+ return null;
238
  }
239
 
240
+ try {
241
+ return $this->hydrate_lockout_entity( $data['lockout_id'], $data );
242
+ } catch ( Exception $e ) {
243
+ return null;
244
+ }
245
  }
246
 
247
  /**
307
  *
308
  * @since 4.0
309
  *
310
+ * @param Lockout\Context $context
 
311
  *
312
  * @return void
313
  */
314
+ public function do_lockout( $context ) {
315
+ if ( is_string( $context ) ) {
316
+ _deprecated_argument( __METHOD__, '5.3.0', '$context should be an iThemesSecurity\Lib\Lockout\Context object.' );
317
+
318
+ if ( func_num_args() > 1 && $username = func_get_arg( 1 ) ) {
319
+ $user_id = username_exists( $username );
320
+
321
+ if ( ! empty( $this->lockout_modules[ $context ]['host'] ) ) {
322
+ $context = new Lockout\Host_Context( $context, ITSEC_Lib::get_ip() );
323
+
324
+ if ( $user_id ) {
325
+ $context->set_login_user_id( $user_id );
326
+ } else {
327
+ $context->set_login_username( $username );
328
+ }
329
+ } elseif ( $user_id ) {
330
+ $context = new Lockout\User_Context( $context, $user_id );
331
+ } else {
332
+ $context = new Lockout\Username_Context( $context, $username );
333
+ }
334
+ } else {
335
+ $context = new Lockout\Host_Context( $context, ITSEC_Lib::get_ip() );
336
+ }
337
+ }
338
+
339
+ if ( ! $context instanceof Lockout\Context ) {
340
+ _doing_it_wrong( __METHOD__, '$context must be an iThemesSecurity\Lib\Lockout\Context object.', '5.3.0' );
341
 
 
342
  return;
343
  }
344
 
345
+ if ( ! isset( $this->lockout_modules[ $context->get_lockout_module() ] ) ) {
346
+ return;
347
+ }
348
 
349
+ if ( ! apply_filters( 'itsec_do_lockout', true, $context ) ) {
350
+ return;
351
+ }
352
 
353
+ $lockout = false;
354
+ $options = $this->lockout_modules[ $context->get_lockout_module() ];
 
 
355
 
356
+ $event_data = array(
357
  'temp_type' => $options['type'],
358
  'temp_date' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() ),
359
  'temp_date_gmt' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
360
  );
361
 
362
+ if ( $context instanceof Lockout\Host_Context && ! empty( $options['host'] ) ) {
363
+ $lockout = $this->create_host_temp_event( $context->get_host(), $event_data, $options );
364
+
365
+ if ( ! empty( $options['user'] ) ) {
366
+ if ( $context->get_login_user_id() && $this->create_user_temp_event( $context->get_login_user_id(), $event_data, $options ) ) {
367
+ if ( $lockout ) {
368
+ $context->set_user_limit_triggered();
369
+ } else {
370
+ $context = new Lockout\User_Context( $context->get_lockout_module(), $context->get_login_user_id() );
371
+ }
372
+
373
+ $lockout = true;
374
+ } elseif ( $context->get_login_username() && $this->create_username_temp_event( $context->get_login_username(), $event_data, $options ) ) {
375
+ if ( $lockout ) {
376
+ $context->set_user_limit_triggered();
377
+ } else {
378
+ $context = new Lockout\Username_Context( $context->get_lockout_module(), $context->get_login_username() );
379
+ }
380
+
381
+ $lockout = true;
382
+ }
383
+ }
384
+ } elseif ( $context instanceof Lockout\User_Context && ! empty( $options['user'] ) ) {
385
+ $lockout = $this->create_user_temp_event( $context->get_user_id(), $event_data, $options );
386
+ } elseif ( $context instanceof Lockout\Username_Context && ! empty( $options['user'] ) ) {
387
+ $lockout = $this->create_username_temp_event( $context->get_username(), $event_data, $options );
388
+ }
389
 
390
+ if ( $lockout ) {
391
+ $this->lockout( $context );
392
+ }
393
+ }
394
 
395
+ private function create_host_temp_event( $host, $event_data, $options ) {
396
+ global $wpdb;
397
 
398
+ $event_data['temp_host'] = $host;
399
 
400
+ $wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $event_data );
 
 
 
 
 
 
401
 
402
+ $host_count = $wpdb->get_var(
403
+ $wpdb->prepare(
404
+ "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_host` = %s",
405
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
406
+ $host
407
+ )
408
+ );
409
+
410
+ return $host_count >= $options['host'];
411
+ }
412
+
413
+ private function create_user_temp_event( $user_id, $event_data, $options ) {
414
+ global $wpdb;
415
+
416
+ $event_data['temp_user'] = $user_id;
417
+ $event_data['temp_username'] = sanitize_text_field( get_userdata( $user_id )->user_login );
418
+
419
+ $wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $event_data );
420
+
421
+ $user_count = $wpdb->get_var(
422
+ $wpdb->prepare(
423
+ "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND (`temp_username` = %s OR `temp_user` = %d)",
424
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
425
+ $event_data['temp_username'],
426
+ $event_data['temp_user']
427
+ )
428
+ );
429
+
430
+ return $user_count >= $options['user'];
431
+ }
432
+
433
+ private function create_username_temp_event( $username, $event_data, $options ) {
434
+ global $wpdb;
435
+
436
+ $event_data['temp_username'] = sanitize_text_field( $username );
437
+
438
+ $wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $event_data );
439
+
440
+ $user_count = $wpdb->get_var(
441
+ $wpdb->prepare(
442
+ "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_username` = %s",
443
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
444
+ $event_data['temp_username']
445
+ )
446
+ );
447
+
448
+ return $user_count >= $options['user'];
449
+ }
450
+
451
+ /**
452
+ * Store a record of the locked out user/host or permanently ban the host.
453
+ *
454
+ * Permanently banned hosts will be forwarded to the ban-users module via the itsec-new-blacklisted-ip hook and
455
+ * not persisted to the database.
456
+ *
457
+ * If configured, notifies the configured email addresses of the lockout.
458
+ *
459
+ * @since 4.0
460
+ *
461
+ * @param Lockout\Context $context
462
+ *
463
+ * @return void
464
+ */
465
+ private function lockout( Lockout\Context $context ) {
466
+ $lock = $context->get_lockout_module() . '_';
467
+
468
+ if ( $context instanceof Lockout\Host_Context ) {
469
+ $lock .= $context->get_host();
470
+ } elseif ( $context instanceof Lockout\User_Context ) {
471
+ $lock .= $context->get_user_id();
472
+ } elseif ( $context instanceof Lockout\Username_Context ) {
473
+ $lock .= $context->get_username();
474
  }
475
 
476
+ // Acquire a lock to prevent a lockout being created more than once by a particularly fast attacker.
477
+ if ( ! ITSEC_Lib::get_lock( $lock, 180 ) ) {
478
+ return;
479
+ }
480
 
481
+ $details = $this->create_lockout( $context );
 
 
 
482
 
483
+ if ( $details['whitelisted'] ) {
484
+ // No need to send an email notice when the host is whitelisted.
485
+ ITSEC_Lib::release_lock( $lock );
486
 
487
+ return;
488
+ }
 
 
 
 
 
 
489
 
490
+ $this->send_lockout_email(
491
+ $context,
492
+ $details['host_expiration'],
493
+ $details['user_expiration'],
494
+ $details['module_details']['reason']
495
+ );
496
 
497
+ ITSEC_Lib::release_lock( $lock );
498
 
499
+ if ( $details['blacklisted'] ) {
500
+ $this->execute_lock( new Execute_Lock\Host_Context( new Configurable( 'blacklist' ), $context->get_host() ) );
501
+ } elseif ( $details['lockout'] instanceof Lockout\Lockout ) {
502
+ $this->execute_lock( $details['lockout']->make_execute_lock_context() );
503
+ } else {
504
+ $this->execute_lock( $context->make_execute_lock_context() );
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Create a lockout.
510
+ *
511
+ * @param Lockout\Context|array $args_or_context
512
+ *
513
+ * @return array
514
+ */
515
+ public function create_lockout( $args_or_context = array() ) {
516
+ global $wpdb;
517
+
518
+ $host = $user_id = $username = false;
519
+
520
+ if ( $args_or_context instanceof Lockout\Context ) {
521
+ $context = $args_or_context;
522
+ $module = $context->get_lockout_module();
523
+
524
+ switch ( true ) {
525
+ case $context instanceof Lockout\Host_Context:
526
+ $host = $context->get_host();
527
+
528
+ if ( $context->is_user_limit_triggered() ) {
529
+ $user_id = $context->get_login_user_id() ?: $user_id;
530
+ $username = $context->get_login_username() ?: $username;
531
+ }
532
+ break;
533
+ case $context instanceof Lockout\User_Context:
534
+ $user_id = $context->get_user_id();
535
+ break;
536
+ case $context instanceof Lockout\Username_Context:
537
+ $username = $context->get_username();
538
+ break;
539
+ }
540
+ } else {
541
+ $args = $args_or_context;
542
+ $module = $args['module'];
543
+ $host = isset( $args['host'] ) ? $args['host'] : false;
544
+ $user_id = isset( $args['user_id'] ) ? $args['user_id'] : false;
545
+ $username = isset( $args['username'] ) ? $args['username'] : false;
546
+ $context = null;
547
+ }
548
+
549
+ $module_details = $this->lockout_modules[ $module ];
550
+
551
+ $whitelisted = ITSEC_Lib::is_ip_whitelisted( $host );
552
+ $blacklisted = false;
553
+
554
+ $log_data = array(
555
+ 'module' => $module,
556
+ 'host' => $host,
557
+ 'user_id' => $user_id,
558
+ 'username' => $username,
559
+ 'module_details' => $module_details,
560
+ 'whitelisted' => $whitelisted,
561
+ 'blacklisted' => false,
562
+ );
563
+
564
+ // Do a permanent ban if enabled and settings criteria are met.
565
+ if ( false !== $host && ITSEC_Modules::get_setting( 'global', 'blacklist' ) ) {
566
+ $blacklist_count = ITSEC_Modules::get_setting( 'global', 'blacklist_count' );
567
+ $blacklist_period = ITSEC_Modules::get_setting( 'global', 'blacklist_period', 7 );
568
+ $blacklist_seconds = $blacklist_period * DAY_IN_SECONDS;
569
+
570
+ $host_count = 1 + $wpdb->get_var(
571
  $wpdb->prepare(
572
+ "SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_expire_gmt` > %s AND `lockout_host`= %s",
573
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - $blacklist_seconds ),
574
+ $host
575
  )
576
  );
577
 
578
+ if ( $host_count >= $blacklist_count ) {
579
+ $blacklisted = true;
580
+ $log_data['blacklisted'] = true;
581
+
582
+ if ( $whitelisted ) {
583
+ ITSEC_Log::add_notice( 'lockout', 'whitelisted-host-triggered-blacklist', array_merge( $log_data, compact( 'blacklist_period', 'blacklist_count', 'host_count' ) ) );
584
+ } else {
585
+ $this->blacklist_ip( $host );
586
+ ITSEC_Log::add_action( 'lockout', 'host-triggered-blacklist', array_merge( $log_data, compact( 'blacklist_period', 'blacklist_count', 'host_count' ) ) );
587
  }
588
  }
589
  }
590
 
591
+ $host_expiration = false;
592
+ $user_expiration = false;
593
+ $lockout = null;
594
+
595
+ $lockouts_data = array(
596
+ 'lockout_type' => $module_details['type'],
597
+ 'lockout_start' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() ),
598
+ 'lockout_start_gmt' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
599
+ 'lockout_context' => $context ? serialize( $context ) : null,
600
+ );
601
+
602
+ if ( $whitelisted ) {
603
+ $lockouts_data['lockout_expire'] = date( 'Y-m-d H:i:s', 1 );
604
+ $lockouts_data['lockout_expire_gmt'] = date( 'Y-m-d H:i:s', 1 );
605
+ } else {
606
+ $exp_seconds = ITSEC_Modules::get_setting( 'global', 'lockout_period' ) * MINUTE_IN_SECONDS;
607
+
608
+ $lockouts_data['lockout_expire'] = date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() + $exp_seconds );
609
+ $lockouts_data['lockout_expire_gmt'] = date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() + $exp_seconds );
610
+ }
611
+
612
+ if ( false !== $host && ! $blacklisted ) {
613
+ $host_expiration = $lockouts_data['lockout_expire'];
614
+ $lockout = $this->add_lockout_to_db( 'host', $host, $whitelisted, $lockouts_data, $log_data );
615
+ }
616
 
617
+ if ( false !== $user_id ) {
618
+ $user_expiration = $lockouts_data['lockout_expire'];
619
+ $lockout = $this->add_lockout_to_db( 'user', $user_id, $whitelisted, $lockouts_data, $log_data );
620
+ }
621
 
622
+ if ( false !== $username ) {
623
+ $user_expiration = $lockouts_data['lockout_expire'];
624
+ $lockout = $this->add_lockout_to_db( 'username', $username, $whitelisted, $lockouts_data, $log_data );
625
  }
626
 
627
+ $id = $lockout ? $lockout->get_id() : false;
628
+
629
+ return compact( 'id', 'host_expiration', 'user_expiration', 'whitelisted', 'blacklisted', 'module_details', 'lockout' );
630
+ }
631
 
632
+ /**
633
+ * Adds a record of a lockout event to the database and log the event.
634
+ *
635
+ * @param string $type The type of lockout: "host", "user", "username".
636
+ * @param string|int $id The value for the type: host's IP, user's ID, username.
637
+ * @param bool $whitelisted Whether or not the host triggering the event is whitelisted.
638
+ * @param array $lockout_data Array of base data to be inserted.
639
+ * @param array $log_data Array of data to be logged for the event.
640
+ *
641
+ * @return Lockout\Lockout|null
642
+ */
643
+ private function add_lockout_to_db( $type, $id, $whitelisted, $lockout_data, $log_data ) {
644
+ global $wpdb;
645
+
646
+ $lockout_data["lockout_$type"] = $id;
647
+
648
+ $result = $wpdb->insert( "{$wpdb->base_prefix}itsec_lockouts", $lockout_data );
649
+ $insert_id = $result ? $wpdb->insert_id : false;
650
+
651
+ if ( $whitelisted ) {
652
+ ITSEC_Log::add_notice( 'lockout', "whitelisted-host-triggered-$type-lockout", array_merge( $log_data, $lockout_data ) );
653
+ } else {
654
+ if ( 'host' === $type ) {
655
+ $code = "host-lockout::{$log_data['host']}";
656
+ } elseif ( 'user' === $type ) {
657
+ $code = "user-lockout::{$log_data['user_id']}";
658
+ } elseif ( 'username' === $type ) {
659
+ $code = "username-lockout::{$log_data['username']}";
660
+ }
661
+
662
+ ITSEC_Log::add_action( 'lockout', $code, array_merge( $log_data, $lockout_data ) );
663
  }
664
+
665
+ if ( ! $insert_id ) {
666
+ return null;
667
+ }
668
+
669
+ try {
670
+ return $this->hydrate_lockout_entity( $insert_id, $lockout_data );
671
+ } catch ( Exception $e ) {
672
+ return null;
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Hydrate a lockout entity from its DB data.
678
+ *
679
+ * @param int $id
680
+ * @param array $data
681
+ *
682
+ * @return Lockout\Lockout
683
+ * @throws Exception
684
+ */
685
+ private function hydrate_lockout_entity( $id, array $data ) {
686
+ $context = null;
687
+
688
+ if ( $data['lockout_context'] && ! ( $context = unserialize( $data['lockout_context'] ) ) instanceof Lockout\Context ) {
689
+ $context = null;
690
+ }
691
+
692
+ return new Lockout\Lockout(
693
+ $id,
694
+ $data['lockout_type'],
695
+ new DateTime( $data['lockout_start_gmt'], new DateTimeZone( 'UTC' ) ),
696
+ new DateTime( $data['lockout_expire_gmt'], new DateTimeZone( 'UTC' ) ),
697
+ isset( $data['lockout_host'] ) ? $data['lockout_host'] : '',
698
+ isset( $data['lockout_user'] ) ? $data['lockout_user'] : 0,
699
+ isset( $data['lockout_username'] ) ? $data['lockout_username'] : '',
700
+ ! empty( $data['lockout_active'] ),
701
+ $context
702
+ );
703
  }
704
 
705
  /**
706
  * Executes lockout (locks user out)
707
  *
708
+ * @param Execute_Lock\Context|array $context
 
709
  *
710
  * @return void
711
  */
712
+ public function execute_lock( $context = array() ) {
713
+ if ( is_array( $context ) ) {
714
+ _deprecated_argument( __METHOD__, '5.3.0', '$context should be a \iThemesSecurity\Lib\Lockout\Execute_Lock\Context object.' );
715
 
716
+ $legacy = wp_parse_args( $context, array( 'user_lock' => false, 'network_lock' => false, 'type' => '' ) );
717
+ $source = $legacy['type'] ? new Lockout_Module( $legacy['type'] ) : new Configurable( 'legacy' );
718
+
719
+ if ( ! empty( $legacy['host'] ) ) {
720
+ $context = new Execute_Lock\Host_Context( $source, $legacy['host'] );
721
+
722
+ if ( $legacy['network_lock'] ) {
723
+ $context->set_network_brute_force();
724
+ }
725
+
726
+ if ( ! empty( $legacy['user'] ) ) {
727
+ $context->set_login_user_id( $legacy['user'] );
728
+ }
729
+ } elseif ( ! empty( $legacy['user'] ) ) {
730
+ $context = new Execute_Lock\User_Context( $source, $legacy['user'] );
731
+ } elseif ( ! empty( $legacy['username'] ) ) {
732
+ $context = new Execute_Lock\Username_Context( $source, $legacy['username'] );
733
+ } elseif ( $legacy['user_lock'] ) {
734
+ $context = new Execute_Lock\Username_Context( $source, '' );
735
+ } else {
736
+ $context = new Execute_Lock\Host_Context( $source, ITSEC_Lib::get_ip() );
737
+
738
+ if ( $legacy['network_lock'] ) {
739
+ $context->set_network_brute_force();
740
+ }
741
+ }
742
  }
743
 
744
+ if ( ! $context instanceof Execute_Lock\Context ) {
745
+ _deprecated_argument( __METHOD__, '5.3.0', '$context should be a \iThemesSecurity\Lib\Lockout\Execute_Lock\Context object.' );
746
+ $context = new Execute_Lock\Host_Context( new Configurable( 'legacy' ), ITSEC_Lib::get_ip() );
 
 
 
 
747
  }
748
 
749
  if ( ITSEC_Lib::is_ip_whitelisted( ITSEC_Lib::get_ip() ) ) {
750
  return;
751
  }
752
 
753
+ if ( ! apply_filters( 'itsec_execute_lock', true, $context ) ) {
754
+ return;
755
+ }
756
+
757
+ if ( $context instanceof Execute_Lock\Host_Context && $context->is_network_brute_force() ) {
758
 
759
  $message = ITSEC_Modules::get_setting( 'global', 'community_lockout_message' );
760
 
761
  if ( ! $message ) {
762
  $message = __( 'Your IP address has been flagged as a threat by the iThemes Security network.', 'better-wp-security' );
763
  }
764
+ } elseif ( $context instanceof Execute_Lock\User_Context || $context instanceof Execute_Lock\Username_Context ) {
 
765
 
766
  $message = ITSEC_Modules::get_setting( 'global', 'user_lockout_message' );
767
 
768
  if ( ! $message ) {
769
+ $message = __( 'You have been locked out due to too many invalid login attempts.', 'better-wp-security' );
770
  }
771
+ } else {
 
 
772
  $message = ITSEC_Modules::get_setting( 'global', 'lockout_message' );
773
 
774
  if ( ! $message ) {
776
  }
777
  }
778
 
779
+ $source = $context->get_source();
780
 
781
+ if ( $source instanceof Lockout\Lockout ) {
782
+ $slug = $source->get_module();
783
+ } else {
784
+ $slug = $source->get_source_slug();
785
+ }
 
 
 
 
786
 
787
+ if ( $slug ) {
788
  /**
789
+ * Filter the lockout message displayed to the user.
790
  *
791
+ * @param string $message
792
+ * @param string $type
793
+ * @param Execute_Lock\Context $context
794
  */
795
+ $message = apply_filters( "itsec_{$slug}_lockout_message", $message, $context );
796
  }
797
 
798
  $current_user = wp_get_current_user();
801
  wp_logout();
802
  }
803
 
804
+ @header( 'HTTP/1.0 403 Forbidden' );
805
+ ITSEC_Lib::no_cache();
 
 
 
 
 
806
 
807
+ $actions = apply_filters( 'itsec_lockout_action_links', array(), $context );
 
 
 
 
 
808
 
809
+ ob_start();
810
+ call_user_func( function () use ( $context, $message, $actions ) {
811
+ require( dirname( __FILE__ ) . '/templates/lockout/lockout.php' );
812
+ } );
813
+
814
+ add_filter( 'wp_die_handler', function () {
815
+ return '_scalar_wp_die_handler';
816
+ } );
817
+ wp_die( ob_get_clean() );
818
  }
819
 
820
  /**
831
  $global_settings_url = add_query_arg( array( 'module_type', 'all' ), $global_settings_url );
832
  }
833
 
834
+ $description = '<h4>' . __( 'About Lockouts', 'better-wp-security' ) . '</h4>';
835
  $description .= '<p>';
836
  $description .= sprintf( __( 'Your lockout settings can be configured in <a href="%s" data-module-link="global">Global Settings</a>.', 'better-wp-security' ), esc_url( $global_settings_url ) );
837
  $description .= '<br />';
859
  *
860
  * @since 4.0
861
  *
862
+ * @param string $type 'all', 'host', 'user' or 'username'.
863
+ * @param array $args Additional arguments.
864
  *
865
  * @return array all lockouts in the system
866
  */
981
 
982
  if ( ! is_array( $whitelist ) ) {
983
  $whitelist = array();
984
+ } elseif ( isset( $whitelist['ip'] ) ) {
985
  // Update old format
986
  $whitelist = array(
987
  $whitelist['ip'] => $whitelist['exp'] - ITSEC_Core::get_time_offset(),
1016
  * @param string $ip
1017
  */
1018
  public function add_to_temp_whitelist( $ip ) {
1019
+ $whitelist = $this->get_temp_whitelist();
1020
+ $expiration = ITSEC_Core::get_current_time_gmt() + DAY_IN_SECONDS;
1021
  $refresh_expiration = $expiration - HOUR_IN_SECONDS;
1022
 
1023
+ if ( isset( $whitelist[ $ip ] ) && $whitelist[ $ip ] > $refresh_expiration ) {
1024
  // An update is not needed yet.
1025
  return;
1026
  }
1028
  // Remove expired entries.
1029
  foreach ( $whitelist as $cached_ip => $cached_expiration ) {
1030
  if ( $cached_expiration < ITSEC_Core::get_current_time_gmt() ) {
1031
+ unset( $whitelist[ $cached_ip ] );
1032
  }
1033
  }
1034
 
1035
+ $whitelist[ $ip ] = $expiration;
1036
 
1037
  update_site_option( 'itsec_temp_whitelist_ip', $whitelist );
1038
  }
1045
  public function remove_from_temp_whitelist( $ip ) {
1046
  $whitelist = $this->get_temp_whitelist();
1047
 
1048
+ if ( ! isset( $whitelist[ $ip ] ) ) {
1049
  return;
1050
  }
1051
 
1052
+ unset( $whitelist[ $ip ] );
1053
 
1054
  update_site_option( 'itsec_temp_whitelist_ip', $whitelist );
1055
  }
1073
  }
1074
 
1075
  $whitelist = $this->get_temp_whitelist();
1076
+ $ip = ITSEC_Lib::get_ip();
1077
 
1078
+ if ( isset( $whitelist[ $ip ] ) && $whitelist[ $ip ] > ITSEC_Core::get_current_time() ) {
1079
  return true;
1080
  }
1081
 
1082
  return false;
1083
  }
1084
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1085
  /**
1086
  * Inserts an IP address into the htaccess ban list.
1087
  *
1151
  *
1152
  * @since 3.6.0
1153
  *
1154
+ * @param array $verbs of verbs.
1155
  *
1156
  * @return array Array of verbs.
1157
  */
1195
  /**
1196
  * Get lockout details.
1197
  *
1198
+ * @param int $id
1199
+ * @param string $return
1200
  *
1201
+ * @return Lockout\Lockout|array|false
1202
+ * @throws Exception
1203
  */
1204
+ public function get_lockout( $id, $return = ARRAY_A ) {
1205
  global $wpdb;
1206
 
1207
  $results = $wpdb->get_results( $wpdb->prepare(
1213
  return false;
1214
  }
1215
 
1216
+ $data = $results[0];
1217
+
1218
+ if ( $return === OBJECT ) {
1219
+ return $this->hydrate_lockout_entity( $id, $data );
1220
+ }
1221
+
1222
+ return $data;
1223
  }
1224
 
1225
  /**
1231
  *
1232
  * @return bool true on success or false
1233
  */
1234
+ public function release_lockout( $id = 0 ) {
 
1235
  global $wpdb;
1236
 
1237
+ if ( ! $id ) {
1238
+ return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1239
  }
1240
 
1241
+ return (bool) $wpdb->update(
1242
+ $wpdb->base_prefix . 'itsec_lockouts',
1243
+ array(
1244
+ 'lockout_active' => 0,
1245
+ ),
1246
+ array(
1247
+ 'lockout_id' => (int) $id,
1248
+ )
1249
+ );
1250
  }
1251
 
1252
  /**
1285
  *
1286
  * @since 4.0
1287
  *
1288
+ * @param Lockout\Context $context
1289
+ * @param string $host_expiration when the host login expires
1290
+ * @param string $user_expiration when the user lockout expires
1291
+ * @param string $reason the reason for the lockout to show to the user
 
 
1292
  *
1293
  * @return void
1294
  */
1295
+ private function send_lockout_email( Lockout\Context $context, $host_expiration, $user_expiration, $reason ) {
1296
 
1297
  $nc = ITSEC_Core::get_notification_center();
1298
 
1300
  return;
1301
  }
1302
 
1303
+ $lockouts = array();
1304
+ $show_remove_ip_ban_message = false;
1305
  $show_remove_lockout_message = false;
1306
 
1307
+ if (
1308
+ ( $context instanceof Lockout\User_Context && $user_id = $context->get_user_id() ) ||
1309
+ ( $context instanceof Lockout\Host_Context && $context->is_user_limit_triggered() && $user_id = $context->get_login_user_id() )
1310
+ ) {
 
 
1311
  $show_remove_lockout_message = true;
1312
 
1313
  $lockouts[] = array(
1314
  'type' => 'user',
1315
+ 'id' => get_userdata( $user_id )->user_login,
1316
+ 'until' => $user_expiration,
1317
+ 'reason' => $reason,
1318
+ );
1319
+ }
1320
+
1321
+ if (
1322
+ ( $context instanceof Lockout\Username_Context && $username = $context->get_username() ) ||
1323
+ ( $context instanceof Lockout\Host_Context && $context->is_user_limit_triggered() && $username = $context->get_login_username() )
1324
+ ) {
1325
+ $lockouts[] = array(
1326
+ 'type' => 'username',
1327
  'id' => $username,
1328
  'until' => $user_expiration,
1329
  'reason' => $reason,
1330
  );
1331
  }
1332
 
1333
+ if ( $context instanceof Lockout\Host_Context ) {
1334
  if ( false === $host_expiration ) {
1335
+ $host_expiration = __( 'Permanently', 'better-wp-security' );
1336
  $show_remove_ip_ban_message = true;
1337
  } else {
1338
  $show_remove_lockout_message = true;
1340
 
1341
  $lockouts[] = array(
1342
  'type' => 'host',
1343
+ 'id' => '<a href="' . esc_url( ITSEC_Lib::get_trace_ip_link( $context->get_host() ) ) . '">' . $context->get_host() . '</a>',
1344
  'until' => $host_expiration,
1345
  'reason' => $reason,
1346
  );
1347
  }
1348
 
 
1349
  $mail = $nc->mail();
1350
 
1351
  $mail->add_header( esc_html__( 'Site Lockout Notification', 'better-wp-security' ), esc_html__( 'Site Lockout Notification', 'better-wp-security' ) );
1363
 
1364
  $mail->add_footer();
1365
 
 
1366
  $subject = $mail->prepend_site_url_to_subject( $nc->get_subject( 'lockout' ) );
1367
  $subject = apply_filters( 'itsec_lockout_email_subject', $subject );
1368
  $mail->set_subject( $subject, false );
1370
  $nc->send( 'lockout', $mail );
1371
  }
1372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1373
  public function filter_entry_for_list_display( $entry, $code, $data ) {
1374
  $entry['module_display'] = esc_html__( 'Lockout', 'better-wp-security' );
1375
 
1376
  if ( 'whitelisted-host-triggered-blacklist' === $code ) {
1377
  $entry['description'] = esc_html__( 'Whitelisted Host Triggered Blacklist', 'better-wp-security' );
1378
+ } elseif ( 'host-triggered-blacklist' === $code ) {
1379
  $entry['description'] = esc_html__( 'Host Triggered Blacklist', 'better-wp-security' );
1380
+ } elseif ( 'whitelisted-host-triggered-host-lockout' === $code ) {
1381
  $entry['description'] = esc_html__( 'Whitelisted Host Triggered Host Lockout', 'better-wp-security' );
1382
+ } elseif ( 'host-lockout' === $code ) {
1383
  if ( isset( $data[0] ) ) {
1384
  $entry['description'] = sprintf( wp_kses( __( 'Host Lockout: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), $data[0] );
1385
  } else {
1386
  $entry['description'] = esc_html__( 'Host Lockout', 'better-wp-security' );
1387
  }
1388
+ } elseif ( 'whitelisted-host-triggered-user-lockout' === $code ) {
1389
  $entry['description'] = esc_html__( 'Whitelisted Host Triggered User Lockout', 'better-wp-security' );
1390
+ } elseif ( 'user-lockout' === $code ) {
1391
  if ( isset( $data[0] ) ) {
1392
  $user = get_user_by( 'id', $data[0] );
1393
  }
1397
  } else {
1398
  $entry['description'] = esc_html__( 'User Lockout', 'better-wp-security' );
1399
  }
1400
+ } elseif ( 'whitelisted-host-triggered-username-lockout' === $code ) {
1401
  $entry['description'] = esc_html__( 'Whitelisted Host Triggered Username Lockout', 'better-wp-security' );
1402
+ } elseif ( 'username-lockout' === $code ) {
1403
  if ( isset( $data[0] ) ) {
1404
  $entry['description'] = sprintf( wp_kses( __( 'Username Lockout: <code>%s</code>', 'better-wp-security' ), array( 'code' => array() ) ), $data[0] );
1405
  } else {
core/modules/404-detection/class-itsec-four-oh-four.php CHANGED
@@ -1,5 +1,7 @@
1
  <?php
2
 
 
 
3
  class ITSEC_Four_Oh_Four {
4
 
5
  private $settings;
@@ -34,7 +36,7 @@ class ITSEC_Four_Oh_Four {
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
  }
@@ -43,9 +45,9 @@ class ITSEC_Four_Oh_Four {
43
  /**
44
  * Register 404 detection for lockout
45
  *
46
- * @param array $lockout_modules array of lockout modules
47
  *
48
- * @return array array of lockout modules
49
  */
50
  public function register_lockout( $lockout_modules ) {
51
 
1
  <?php
2
 
3
+ use iThemesSecurity\Lib\Lockout\Host_Context;
4
+
5
  class ITSEC_Four_Oh_Four {
6
 
7
  private $settings;
36
  ! in_array( '.' . pathinfo( $uri[0], PATHINFO_EXTENSION ), $this->settings['types'], true )
37
  ) {
38
  ITSEC_Log::add_notice( 'four_oh_four', 'found_404', array( 'SERVER' => $_SERVER ) );
39
+ $itsec_lockout->do_lockout( new Host_Context( 'four_oh_four' ) );
40
  } else {
41
  do_action( 'itsec_four_oh_four_whitelisted', $uri );
42
  }
45
  /**
46
  * Register 404 detection for lockout
47
  *
48
+ * @param array $lockout_modules array of lockout modules
49
  *
50
+ * @return array
51
  */
52
  public function register_lockout( $lockout_modules ) {
53
 
core/modules/brute-force/class-itsec-brute-force.php CHANGED
@@ -1,16 +1,16 @@
1
  <?php
2
 
 
 
3
  class ITSEC_Brute_Force {
4
 
5
- private
6
- $settings,
7
- $username;
8
 
9
  function run() {
10
 
11
  $this->settings = ITSEC_Modules::get_settings( 'brute-force' );
12
 
13
- add_filter( 'authenticate', array( $this, 'authenticate' ), 10000, 3 ); // Set a very late priority so that we run after actual authentication takes place.
14
  add_filter( 'itsec_lockout_modules', array( $this, 'itsec_lockout_modules' ) );
15
  add_filter( 'jetpack_get_default_modules', array( $this, 'jetpack_get_default_modules' ) ); //disable jetpack protect via Geoge Stephanis
16
 
@@ -23,11 +23,10 @@ class ITSEC_Brute_Force {
23
  *
24
  * @param object $user user or wordpress error
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 */
32
  global $itsec_lockout;
33
 
@@ -40,37 +39,44 @@ class ITSEC_Brute_Force {
40
  // Failed authentication.
41
 
42
  $details = ITSEC_Lib::get_login_details();
43
- $SERVER = $_SERVER;
44
 
45
  if ( 'admin' === $username && $this->settings['auto_ban_admin'] ) {
46
  ITSEC_Log::add_notice( 'brute_force', 'auto-ban-admin-username', compact( 'details', 'user', 'username', 'SERVER' ) );
47
 
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' );
55
  } else {
56
- $user_id = username_exists( $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
  }
71
  } else {
72
  // Successful authentication. Check to ensure that they are not locked out.
73
- $itsec_lockout->check_lockout( $user, false, 'brute_force_host_lockout' );
74
  }
75
 
76
  return $user;
@@ -82,7 +88,7 @@ class ITSEC_Brute_Force {
82
  *
83
  * @since 4.0
84
  *
85
- * @param array $lockout_modules array of lockout modules
86
  *
87
  * @return array array of lockout modules
88
  */
1
  <?php
2
 
3
+ use iThemesSecurity\Lib\Lockout\Host_Context;
4
+
5
  class ITSEC_Brute_Force {
6
 
7
+ private $settings;
 
 
8
 
9
  function run() {
10
 
11
  $this->settings = ITSEC_Modules::get_settings( 'brute-force' );
12
 
13
+ add_filter( 'authenticate', array( $this, 'authenticate' ), 10000, 2 ); // Set a very late priority so that we run after actual authentication takes place.
14
  add_filter( 'itsec_lockout_modules', array( $this, 'itsec_lockout_modules' ) );
15
  add_filter( 'jetpack_get_default_modules', array( $this, 'jetpack_get_default_modules' ) ); //disable jetpack protect via Geoge Stephanis
16
 
23
  *
24
  * @param object $user user or wordpress error
25
  * @param string $username username attempted
 
26
  *
27
  * @return WP_User|WP_Error|null
28
  */
29
+ public function authenticate( $user, $username = '' ) {
30
  /** @var ITSEC_Lockout $itsec_lockout */
31
  global $itsec_lockout;
32
 
39
  // Failed authentication.
40
 
41
  $details = ITSEC_Lib::get_login_details();
42
+ $SERVER = $_SERVER;
43
 
44
  if ( 'admin' === $username && $this->settings['auto_ban_admin'] ) {
45
  ITSEC_Log::add_notice( 'brute_force', 'auto-ban-admin-username', compact( 'details', 'user', 'username', 'SERVER' ) );
46
 
47
+ $context = new Host_Context( 'brute_force_admin_user' );
48
+ $context->set_login_username( 'admin' );
49
+ $itsec_lockout->do_lockout( $context );
50
  } else {
51
+ $context = new Host_Context( 'brute_force' );
52
  $user_id = false;
53
+ $code = 'invalid-login';
54
 
55
  if ( empty( $username ) ) {
56
+ $itsec_lockout->check_lockout( false, false, 'brute_force' );
57
  } else {
58
+ ITSEC_Lib::load( 'login' );
59
+ $found_user = ITSEC_Lib_Login::get_user( $username );
60
+ $user_id = $found_user ? $found_user->ID : 0;
61
 
62
+ if ( ! $user_id ) {
63
+ $context->set_login_username( $username );
64
+ $itsec_lockout->check_lockout( false, $username, 'brute_force' );
65
  $code = "invalid-login::username-{$username}";
66
  } else {
67
+ $context->set_login_user_id( $user_id );
68
+ $itsec_lockout->check_lockout( $user_id, false, 'brute_force' );
69
  $code = "invalid-login::user-{$user_id}";
70
  }
71
  }
72
 
73
  ITSEC_Log::add_notice( 'brute_force', $code, compact( 'details', 'user', 'username', 'user_id', 'SERVER' ) );
74
 
75
+ $itsec_lockout->do_lockout( $context );
76
  }
77
  } else {
78
  // Successful authentication. Check to ensure that they are not locked out.
79
+ $itsec_lockout->check_lockout( $user, false, 'brute_force' );
80
  }
81
 
82
  return $user;
88
  *
89
  * @since 4.0
90
  *
91
+ * @param array $lockout_modules array of lockout modules
92
  *
93
  * @return array array of lockout modules
94
  */
core/modules/core/class-rest-core-admin-notices-controller.php CHANGED
@@ -9,7 +9,7 @@ class ITSEC_REST_Core_Admin_Notices_Controller extends WP_REST_Controller {
9
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
10
  ) );
11
 
12
- register_rest_route( 'ithemes-security/v1', 'admin-notices/(?P<notice>[\w\-]+)/(?P<action>[\w\-]+)', array(
13
  'methods' => WP_REST_Server::EDITABLE,
14
  'callback' => array( $this, 'update_item' ),
15
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
@@ -104,7 +104,7 @@ class ITSEC_REST_Core_Admin_Notices_Controller extends WP_REST_Controller {
104
  return $error;
105
  }
106
 
107
- return null;
108
  }
109
 
110
  public function update_item_permissions_check( $request ) {
9
  'permission_callback' => array( $this, 'get_items_permissions_check' ),
10
  ) );
11
 
12
+ register_rest_route( 'ithemes-security/v1', 'admin-notices/(?P<notice>[\w\-\.]+)/(?P<action>[\w\-]+)', array(
13
  'methods' => WP_REST_Server::EDITABLE,
14
  'callback' => array( $this, 'update_item' ),
15
  'permission_callback' => array( $this, 'update_item_permissions_check' ),
104
  return $error;
105
  }
106
 
107
+ return new WP_REST_Response( null, WP_Http::NO_CONTENT );
108
  }
109
 
110
  public function update_item_permissions_check( $request ) {
core/modules/core/img/security-ebook.png CHANGED
Binary file
core/modules/core/img/sync-logo.png CHANGED
Binary file
core/modules/core/sidebar-widget-mail-list-signup.php CHANGED
@@ -29,7 +29,7 @@ class ITSEC_Settings_Page_Sidebar_Widget_Mail_List_Signup extends ITSEC_Settings
29
  method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate"
30
  target="_blank" novalidate>
31
  <div style="text-align: center;">
32
- <img src="<?php echo plugins_url( 'img/security-ebook.png', __FILE__ ) ?>" width="145" height="187" alt="WordPress Security - A Pocket Guide">
33
  </div>
34
  <p><?php _e( 'Get tips for securing your site + the latest WordPress security updates, news and releases from iThemes.', 'better-wp-security' ); ?></p>
35
 
29
  method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate"
30
  target="_blank" novalidate>
31
  <div style="text-align: center;">
32
+ <img src="<?php echo plugins_url( 'img/security-ebook.png', __FILE__ ) ?>" style="max-width: 100%" alt="WordPress Security - A Pocket Guide">
33
  </div>
34
  <p><?php _e( 'Get tips for securing your site + the latest WordPress security updates, news and releases from iThemes.', 'better-wp-security' ); ?></p>
35
 
core/modules/core/sidebar-widget-sync-cross-promo.php CHANGED
@@ -12,15 +12,14 @@ class ITSEC_Settings_Page_Sidebar_Widget_Sync_Cross_Promo extends ITSEC_Settings
12
  public function render( $form ) {
13
  ?>
14
  <div style="text-align: center;">
15
- <img src="<?php echo plugins_url( 'img/sync-logo.png', __FILE__ ) ?>" width="173"
16
- height="65" alt="Manage Your Sites Remotely">
17
  </div>
18
  <?php
19
 
20
- echo '<p>' . __( 'Manage updates remotely for up to 10 WordPress sites today for free!', 'better-wp-security' ) . '</p>';
21
- echo '<p>' . __( 'Integrated with iThemes Security, so you can release lockouts and turn Away Mode on or off right from your Sync dashboard or your phone.', 'better-wp-security' ) . '</p>';
22
  echo '<div style="text-align: center;">';
23
- echo '<p><a class="button-primary" href="http://www.ithemes.com/sync" target="_blank" rel="noopener noreferrer">' . __( 'Try iThemes Sync for Free', 'better-wp-security' ) . '</a></p>';
24
  echo '</div>';
25
  }
26
 
12
  public function render( $form ) {
13
  ?>
14
  <div style="text-align: center;">
15
+ <img src="<?php echo plugins_url( 'img/sync-logo.png', __FILE__ ) ?>" style="max-width: 100%" alt="Manage Your Sites Remotely">
 
16
  </div>
17
  <?php
18
 
19
+ echo '<p>' . __( 'Manage updates (and much more!) for your WordPress websites all in one place. Save time logging in to multiple websites to perform WordPress admin tasks.', 'better-wp-security' ) . '</p>';
20
+ echo '<p>' . __( 'Integrated with iThemes Security, so you can release lockouts, whitelist IPs, and turn Away Mode on or off right from your Sync dashboard.', 'better-wp-security' ) . '</p>';
21
  echo '<div style="text-align: center;">';
22
+ echo '<p><a class="button-primary" href="https://ithemes.com/member/cart.php?action=add&id=523" target="_blank" rel="noopener noreferrer">' . __( 'Free 30 Day Trial', 'better-wp-security' ) . '</a></p>';
23
  echo '</div>';
24
  }
25
 
core/modules/email-confirmation/active.php ADDED
@@ -0,0 +1,4 @@
 
 
 
 
1
+ <?php
2
+ require_once __DIR__ . '/class-itsec-email-confirmation.php';
3
+ $module = new ITSEC_Email_Confirmation();
4
+ $module->run();
core/modules/email-confirmation/class-itsec-email-confirmation.php ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ITSEC_Email_Confirmation {
4
+
5
+ public function run() {
6
+ add_action( 'after_password_reset', array( $this, 'after_password_reset' ) );
7
+ add_action( 'profile_update', array( $this, 'on_user_update' ), 10, 2 );
8
+ }
9
+
10
+ /**
11
+ * When a user's password has been reset, mark the user's email address as confirmed.
12
+ *
13
+ * @param WP_User $user
14
+ */
15
+ public function after_password_reset( $user ) {
16
+ ITSEC_Lib::load( 'email-confirmation' );
17
+ ITSEC_Lib_Email_Confirmation::mark_email_as_confirmed( $user );
18
+ }
19
+
20
+ /**
21
+ * When a user's email is updated, mark their email confirmation as not confirmed.
22
+ *
23
+ * @param int $user_id User id.
24
+ * @param object $old_data Old data.
25
+ */
26
+ public function on_user_update( $user_id, $old_data ) {
27
+ $user = get_userdata( $user_id );
28
+
29
+ if ( $user->user_email === $old_data->user_email ) {
30
+ return;
31
+ }
32
+
33
+ ITSEC_Lib::load( 'email-confirmation' );
34
+ $change = get_user_meta( $user->ID, '_new_email', true );
35
+
36
+ if (
37
+ IS_PROFILE_PAGE &&
38
+ ! empty( $_GET['newuseremail'] ) &&
39
+ is_array( $change ) &&
40
+ isset( $change['hash'], $change['newemail'] ) &&
41
+ $change['newemail'] === $user->user_email &&
42
+ hash_equals( $change['hash'], $_GET['newuseremail'] )
43
+ ) {
44
+ ITSEC_Lib_Email_Confirmation::mark_email_as_confirmed( $user );
45
+
46
+ return;
47
+ }
48
+
49
+ ITSEC_Lib_Email_Confirmation::mark_email_as_confirmed( $user, false );
50
+ }
51
+ }
core/modules/email-confirmation/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/modules/global/settings-page.php CHANGED
@@ -265,7 +265,7 @@ final class ITSEC_Global_Settings_Page extends ITSEC_Module_Settings_Page {
265
  <td>
266
  <?php if ( $proxy_header ) : ?>
267
  <p class="description">
268
- <?php printf( esc_html__( 'Security Check Pro has automatically determined the correct header, %s.', 'better-wp-security' ), '<code>' . esc_attr( $proxy_header ) . '</code>' ); ?>
269
  </p>
270
  <?php else: ?>
271
  <?php $form->add_select( 'proxy', $proxy ); ?>
265
  <td>
266
  <?php if ( $proxy_header ) : ?>
267
  <p class="description">
268
+ <?php printf( esc_html__( 'Security Check Pro has automatically determined the correct header, %s.', 'better-wp-security' ), '<code>' . esc_html( is_array( $proxy_header ) ? implode( ', ', $proxy_header ) : $proxy_header ) . '</code>' ); ?>
269
  </p>
270
  <?php else: ?>
271
  <?php $form->add_select( 'proxy', $proxy ); ?>
core/modules/global/settings.php CHANGED
@@ -84,6 +84,12 @@ final class ITSEC_Global_Settings_New extends ITSEC_Settings {
84
 
85
  $new->uninstall();
86
 
 
 
 
 
 
 
87
  foreach ( $current->get_recurring_events() as $event ) {
88
  $new->schedule( $event['schedule'], $event['id'], $event['data'], array(
89
  'fire_at' => $event['fire_at'],
84
 
85
  $new->uninstall();
86
 
87
+ foreach ( $current->get_custom_schedules() as $slug => $interval ) {
88
+ $new->register_custom_schedule( $slug, $interval );
89
+ }
90
+
91
+ $new->run();
92
+
93
  foreach ( $current->get_recurring_events() as $event ) {
94
  $new->schedule( $event['schedule'], $event['id'], $event['data'], array(
95
  'fire_at' => $event['fire_at'],
core/modules/ipcheck/class-itsec-ipcheck.php CHANGED
@@ -1,5 +1,8 @@
1
  <?php
2
 
 
 
 
3
  /**
4
  * iThemes IPCheck API Wrapper.
5
  *
@@ -28,14 +31,23 @@ class ITSEC_IPCheck {
28
 
29
  $enable_ban = ITSEC_Modules::get_setting( 'network-brute-force', 'enable_ban' );
30
 
 
 
 
 
 
 
 
 
 
31
  if ( is_wp_error( $user ) || null == $user ) {
32
  if ( ITSEC_Network_Brute_Force_Utilities::report_ip() && $enable_ban ) {
33
  ITSEC_Log::add_notice( 'ipcheck', 'failed-login-by-blocked-ip', array( 'details' => ITSEC_Lib::get_login_details() ) );
34
- $itsec_lockout->execute_lock( array( 'network_lock' => true ) );
35
  }
36
  } elseif ( $enable_ban && ITSEC_Network_Brute_Force_Utilities::is_ip_banned() ) {
37
  ITSEC_Log::add_critical_issue( 'ipcheck', 'successful-login-by-blocked-ip', array( 'details' => ITSEC_Lib::get_login_details() ) );
38
- $itsec_lockout->execute_lock( array( 'network_lock' => true ) );
39
  }
40
 
41
  return $user;
1
  <?php
2
 
3
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Host_Context;
4
+ use iThemesSecurity\Lib\Lockout\Execute_Lock\Source\Configurable;
5
+
6
  /**
7
  * iThemes IPCheck API Wrapper.
8
  *
31
 
32
  $enable_ban = ITSEC_Modules::get_setting( 'network-brute-force', 'enable_ban' );
33
 
34
+ $context = new Host_Context( new Configurable( 'network-brute-force' ), ITSEC_Lib::get_ip() );
35
+ $context->set_network_brute_force();
36
+
37
+ if ( $user instanceof WP_User ) {
38
+ $context->set_login_user_id( $user->ID );
39
+ } elseif ( $exists = username_exists( $username ) ) {
40
+ $context->set_login_user_id( $exists );
41
+ }
42
+
43
  if ( is_wp_error( $user ) || null == $user ) {
44
  if ( ITSEC_Network_Brute_Force_Utilities::report_ip() && $enable_ban ) {
45
  ITSEC_Log::add_notice( 'ipcheck', 'failed-login-by-blocked-ip', array( 'details' => ITSEC_Lib::get_login_details() ) );
46
+ $itsec_lockout->execute_lock( $context );
47
  }
48
  } elseif ( $enable_ban && ITSEC_Network_Brute_Force_Utilities::is_ip_banned() ) {
49
  ITSEC_Log::add_critical_issue( 'ipcheck', 'successful-login-by-blocked-ip', array( 'details' => ITSEC_Lib::get_login_details() ) );
50
+ $itsec_lockout->execute_lock( $context );
51
  }
52
 
53
  return $user;
core/modules/pro/settings-page.php CHANGED
@@ -149,3 +149,18 @@ final class ITSEC_Version_Management_Settings_Page extends ITSEC_Module_Settings
149
  }
150
  }
151
  new ITSEC_Version_Management_Settings_Page();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
  }
151
  new ITSEC_Version_Management_Settings_Page();
152
+
153
+ final class ITSEC_Passwordless_Login_Settings_Page extends ITSEC_Module_Settings_Page {
154
+ public function __construct() {
155
+ $this->id = 'passwordless-login';
156
+ $this->title = __( 'Passwordless Login', 'better-wp-security' );
157
+ $this->description = __( 'Enable logging in without a password.', 'better-wp-security' );
158
+ $this->type = 'recommended';
159
+ $this->pro = true;
160
+ $this->upsell = true;
161
+ $this->upsell_url = 'https://ithemes.com/new-wordpress-passwordless-login-ithemes-security/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
162
+
163
+ parent::__construct();
164
+ }
165
+ }
166
+ new ITSEC_Passwordless_Login_Settings_Page();
core/modules/security-check/scanner.php CHANGED
@@ -18,6 +18,7 @@ final class ITSEC_Security_Check_Scanner {
18
  'magic-links' => __( 'Magic Links', 'better-wp-security' ),
19
  'malware-scheduling' => __( 'Malware Scan Scheduling', 'better-wp-security' ),
20
  'network-brute-force' => __( 'Network Brute Force Protection', 'better-wp-security' ),
 
21
  'strong-passwords' => __( 'Strong Passwords', 'better-wp-security' ),
22
  'two-factor' => __( 'Two-Factor Authentication', 'better-wp-security' ),
23
  'user-logging' => __( 'User Logging', 'better-wp-security' ),
@@ -78,6 +79,7 @@ final class ITSEC_Security_Check_Scanner {
78
 
79
  self::add_network_brute_force_signup();
80
 
 
81
  self::enforce_password_requirement_enabled( 'strength', __( 'Strong Password Enforcement', 'better-wp-security' ) );
82
  self::enforce_activation( 'two-factor', __( 'Two-Factor Authentication', 'better-wp-security' ) );
83
  self::enforce_setting( 'two-factor', 'available_methods', 'all', esc_html__( 'Changed the Authentication Methods Available to Users setting in Two-Factor Authentication to "All Methods".', 'better-wp-security' ) );
18
  'magic-links' => __( 'Magic Links', 'better-wp-security' ),
19
  'malware-scheduling' => __( 'Malware Scan Scheduling', 'better-wp-security' ),
20
  'network-brute-force' => __( 'Network Brute Force Protection', 'better-wp-security' ),
21
+ 'passwordless-login' => __( 'Passwordless Login', 'better-wp-security' ),
22
  'strong-passwords' => __( 'Strong Passwords', 'better-wp-security' ),
23
  'two-factor' => __( 'Two-Factor Authentication', 'better-wp-security' ),
24
  'user-logging' => __( 'User Logging', 'better-wp-security' ),
79
 
80
  self::add_network_brute_force_signup();
81
 
82
+ self::enforce_activation( 'passwordless-login', __( 'Passwordless Login', 'better-wp-security' ) );
83
  self::enforce_password_requirement_enabled( 'strength', __( 'Strong Password Enforcement', 'better-wp-security' ) );
84
  self::enforce_activation( 'two-factor', __( 'Two-Factor Authentication', 'better-wp-security' ) );
85
  self::enforce_setting( 'two-factor', 'available_methods', 'all', esc_html__( 'Changed the Authentication Methods Available to Users setting in Two-Factor Authentication to "All Methods".', 'better-wp-security' ) );
core/modules/system-tweaks/class-itsec-system-tweaks.php CHANGED
@@ -100,6 +100,10 @@ final class ITSEC_System_Tweaks {
100
  return;
101
  }
102
 
 
 
 
 
103
  @header( 'HTTP/1.1 414 Request-URI Too Long' );
104
  @header( 'Status: 414 Request-URI Too Long' );
105
  @header( 'Cache-Control: no-cache, must-revalidate' );
100
  return;
101
  }
102
 
103
+ if ( $_SERVER['REQUEST_METHOD'] === 'GET' && isset( $GLOBALS['pagenow'], $_GET['action'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) {
104
+ return;
105
+ }
106
+
107
  @header( 'HTTP/1.1 414 Request-URI Too Long' );
108
  @header( 'Status: 414 Request-URI Too Long' );
109
  @header( 'Cache-Control: no-cache, must-revalidate' );
core/package.json CHANGED
@@ -109,6 +109,8 @@
109
  "test-unit:coverage": "npm run test-unit -- --coverage",
110
  "test-unit:update": "npm run test-unit -- --updateSnapshot",
111
  "test-unit:watch": "npm run test-unit -- --watch",
112
- "watch": "./node_modules/.bin/webpack --watch"
 
 
113
  }
114
  }
109
  "test-unit:coverage": "npm run test-unit -- --coverage",
110
  "test-unit:update": "npm run test-unit -- --updateSnapshot",
111
  "test-unit:watch": "npm run test-unit -- --watch",
112
+ "watch": "./node_modules/.bin/webpack --watch",
113
+ "test-acceptance": "docker-compose exec -w /bitnami/wordpress/wp-content/plugins/ithemes-security-pro wordpress ./vendor/bin/codecept run acceptance",
114
+ "test-acceptance:build": "docker-compose exec -w /bitnami/wordpress/wp-content/plugins/ithemes-security-pro wordpress ./vendor/bin/codecept build"
115
  }
116
  }
core/templates/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/templates/lockout/icon.svg ADDED
@@ -0,0 +1 @@
 
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="547" height="305.817" viewBox="0 0 547 305.817"><defs><style>.a{fill:#0061a8;}.b{fill:#e1f2fc;}.b,.f,.g{stroke:#0083e3;}.c{fill:#0083e3;}.d{fill:#fff;}.e,.f,.g{fill:none;}.f,.g{stroke-linecap:round;}.f{stroke-width:2px;}.g{stroke-width:3px;}</style></defs><g transform="translate(-152 -220.183)"><ellipse class="a" cx="273.5" cy="56.5" rx="273.5" ry="56.5" transform="translate(152 413)"/><g transform="translate(226.992 220.683)"><g transform="translate(0 0)"><path class="b" d="M356.159,304.5A11.65,11.65,0,0,0,344.5,316.14V524.175a7.765,7.765,0,0,0,7.773,7.759h322.6a7.765,7.765,0,0,0,7.772-7.759V316.14a11.649,11.649,0,0,0-11.659-11.64Z" transform="translate(-307.906 -304.5)"/><path class="c" d="M371,529.473H687.369V334H371Z" transform="translate(-322.132 -320.336)"/><path class="d" d="M373,529.62H687.517V336H373Z" transform="translate(-323.205 -321.409)"/><path class="e" d="M281.281,803.654c-5.169,0-11.636-3.094-14.4-4.414-.394-.19-.672-.324-.88-.417V793H676.863v5.823c-.208.1-.486.227-.88.417-2.742,1.32-9.162,4.415-14.29,4.415H281.281Z" transform="translate(-265.768 -566.724)"/><path class="b" d="M677.753,792.5H265.5v7.008c.477-.009,9.273,5.035,16,5.035H661.861c6.684,0,15.415-5.049,15.874-5.049.014,0,.019-.042.019-.042Z" transform="translate(-265.5 -566.456)"/><path class="c" d="M266.633,805l-.463.926h410.4l.463-.926Z" transform="translate(-265.86 -573.166)"/><path class="c" d="M696.416,796.632h-46.9A4.548,4.548,0,0,1,645,792.047V792h56.048v.047a4.549,4.549,0,0,1-4.516,4.586" transform="translate(-469.214 -566.188)"/><path class="c" d="M705.412,319.951a1.621,1.621,0,1,1-1.621-1.621,1.623,1.623,0,0,1,1.621,1.621" transform="translate(-499.902 -311.924)"/><circle class="c" cx="4.725" cy="4.725" r="4.725" transform="translate(162.607 100.885)"/><circle class="c" cx="4.725" cy="4.725" r="4.725" transform="translate(234.106 99.885)"/><path class="c" d="M21.4,10.325,58.811,9.113c2.11,0,3.82-2.04,3.82-4.557S60.92,0,58.811,0L-9.755,8.147l10,.966Z" transform="matrix(-0.998, 0.07, -0.07, -0.998, 262.782, 83.763)"/><path class="c" d="M21.952-.07l15.42-5.575c2.357,0,33.408,8.876,33.408,10.98a4.059,4.059,0,0,1-4.268,3.81L38.491,1.465C35.036-.316-3.517,9.848-2.523,9.848S21.952-.07,21.952-.07Z" transform="translate(109.095 70.919) rotate(8)"/><line class="f" x2="50" transform="translate(127.508 100.817)"/><line class="f" x2="50" transform="translate(199.508 100.817)"/><line class="g" x2="32" transform="translate(199.508 150.817)"/></g></g></g></svg>
core/templates/lockout/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden.
core/templates/lockout/lamp-light.svg ADDED
@@ -0,0 +1 @@
 
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="544.535" height="653" viewBox="0 0 544.535 653"><defs><style>.a{fill:#f7fcfe;}.b{fill:#ffcd0c;}.c{fill:#d5d5d5;}.d{filter:url(#c);}.e{filter:url(#a);}</style><filter id="a" x="0" y="0" width="544.535" height="653" filterUnits="userSpaceOnUse"><feOffset dy="3" input="SourceAlpha"/><feGaussianBlur result="b"/><feFlood flood-opacity="0.161"/><feComposite operator="in" in2="b"/><feComposite in="SourceGraphic"/></filter><filter id="c" x="110" y="0" width="314.535" height="80.329" filterUnits="userSpaceOnUse"><feOffset dy="3" input="SourceAlpha"/><feGaussianBlur result="d"/><feFlood flood-opacity="0.161"/><feComposite operator="in" in2="d"/><feComposite in="SourceGraphic"/></filter></defs><g transform="translate(-684)"><g class="e" transform="matrix(1, 0, 0, 1, 684, 0)"><path class="a" d="M-3.694,0H274.229L412.535,650H-132Z" transform="translate(132)"/></g><g transform="translate(794)"><path class="b" d="M61,60.67a60.626,60.626,0,0,1-23.673-4.763,61,61,0,0,1-10.338-5.59,61.41,61.41,0,0,1-9.015-7.4,61.431,61.431,0,0,1-7.45-8.977A61.042,61.042,0,0,1,4.883,23.629,60.6,60.6,0,0,1,0,0H122a60.6,60.6,0,0,1-4.883,23.629,61.042,61.042,0,0,1-5.641,10.306,61.439,61.439,0,0,1-7.45,8.977,61.4,61.4,0,0,1-9.015,7.4,61,61,0,0,1-10.338,5.59A60.63,60.63,0,0,1,61,60.67Z" transform="translate(97 57.33)"/><g class="d" transform="matrix(1, 0, 0, 1, -110, 0)"><path class="c" d="M-3.694,0H274.229l18.306,77.329H-22Z" transform="translate(132)"/></g></g></g></svg>
core/templates/lockout/lockout.css ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ margin: 0;
3
+ }
4
+
5
+ #lock_out_screen {
6
+ position: relative;
7
+ background: #0083E3;
8
+ box-sizing: border-box;
9
+ height: 100%;
10
+ }
11
+
12
+ #lockout-text {
13
+ text-align: center;
14
+ z-index: 1;
15
+ position: relative;
16
+ background: url(./lamp-light.svg) no-repeat top left;
17
+ background-size: cover;
18
+ display: block;
19
+ }
20
+
21
+ #lockout-text h1 {
22
+ font-weight: 700;
23
+ text-transform: uppercase;
24
+ color: #0083e3;
25
+ max-width: 40%;
26
+ margin: 4em auto 1em;
27
+ font-family: 'Open Sans', Helvetica, Arial, sans-serif;
28
+ }
29
+
30
+ #lockout-text p {
31
+ max-width: 45%;
32
+ margin: 1em auto;
33
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
34
+ font-weight: 100;
35
+ line-height: 1.5em;
36
+ }
37
+
38
+ #lockout-text img {
39
+ position: absolute;
40
+ top: -1px;
41
+ left: 0;
42
+ right: 0;
43
+ margin: 0px auto;
44
+ }
45
+
46
+ #lockout-computer {
47
+ position: relative;
48
+ }
49
+
50
+ #lockout-computer img {
51
+ float: left;
52
+ width: 93%;
53
+ top: 30%;
54
+ position: absolute;
55
+ max-height: 300px;
56
+ }
57
+ #spacer {
58
+ background: linear-gradient(75deg, #ffffff 30%, rgba(0, 0, 0, 0) 30%), linear-gradient(65deg, #0083e3 60%, #0083e3 60%);
59
+ }
60
+ .container {
61
+ display: flex;
62
+ flex-wrap: wrap;
63
+ flex-direction: row;
64
+ margin: 0% 4%;
65
+ padding: 0;
66
+ justify-content: center;
67
+ background: #0083E3;
68
+ }
69
+ .box {
70
+ flex-grow: 1;
71
+ font-size: 18px;
72
+ text-align: center;
73
+ }
74
+ .box2 {
75
+ flex-basis: 50%;
76
+ }
77
+ .box3 {
78
+ flex-basis: 50%;
79
+ }
80
+ .box4 {
81
+ flex-basis: 25%;
82
+ }
83
+ #lock_out_screen .btn {
84
+ text-decoration: none;
85
+ margin: 1% 2% 10%;
86
+ display: inline-block;
87
+ padding: 2% 4%;
88
+ background: #0083e3;
89
+ color: #ffff;
90
+ border-radius: 4px;
91
+ border: solid 2px transparent;
92
+ cursor: pointer;
93
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
94
+ }
95
+ #lock_out_screen .secondary-btn {
96
+ border: solid 2px #0083e3;
97
+ background: transparent;
98
+ color: #0083e3;
99
+ }
100
+ #lock_out_screen .btn:hover {
101
+ background: #0061a8;
102
+ color: #fff;
103
+ border-color: transparent;
104
+ }
105
+
106
+ /* Media Queries */
107
+
108
+ @media only screen and (max-width: 1200px) {
109
+ #lockout-computer.flex.box3 {
110
+ flex-basis: 35%
111
+ }
112
+ #lockout-text.flex.box3 {
113
+ flex-basis: 65%
114
+ }
115
+ #lockout-text h1 {
116
+ margin: 6em auto 1em;
117
+ }
118
+ }
119
+ @media only screen and (max-width: 1000px) {
120
+ .container {
121
+ display: flex;
122
+ flex-wrap: nowrap;
123
+ flex-direction: column;
124
+ margin: 0% 4%;
125
+ padding: 0;
126
+ justify-content: center;
127
+ height: 100%;
128
+ }
129
+ #lockout-computer img {
130
+ float: unset;
131
+ width: 100%;
132
+ position: unset;
133
+ margin-top: 100px;
134
+ }
135
+ #lockout-text.flex.box3 {
136
+ flex-basis: 100%;
137
+ background: none;
138
+ background-size: unset;
139
+ z-index: unset;
140
+ position: unset;
141
+ }
142
+ #lockout-text h1 {
143
+ margin: 2em auto 1em;
144
+ max-width: 80%;
145
+ color: #fff;
146
+ }
147
+ #lockout-text p {
148
+ max-width: 65%;
149
+ color: #fdfdfd;
150
+ }
151
+ #lockout-text p {
152
+ max-width: 75%;
153
+ }
154
+ #lock_out_screen .btn {
155
+ background: #ffcd0c;
156
+ color: #1d1d1e;
157
+ }
158
+ }
159
+ @media only screen and (min-width: 1300px) {
160
+ #lockout-text h1 {
161
+ margin: 7em auto 1em;
162
+ }
163
+ }
core/templates/lockout/lockout.php ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @var \iThemesSecurity\Lib\Lockout\Context $context
4
+ * @var string $message
5
+ * @var array[] $actions
6
+ */
7
+ ?>
8
+ <!DOCTYPE html>
9
+ <html>
10
+ <head>
11
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
12
+ <meta name="viewport" content="width=device-width, initial-scale=1">
13
+ <link href="<?php echo plugin_dir_url( __FILE__ ) . 'lockout.css'; ?>" type="text/css" rel="stylesheet">
14
+ <?php wp_no_robots(); ?>
15
+ <title><?php esc_html_e( 'Forbidden', 'better-wp-security' ); ?></title>
16
+ </head>
17
+ <body id="error-page">
18
+ <div id="lock_out_screen">
19
+ <div class="container">
20
+ <div id="lockout-computer" class="flex box3">
21
+ <img src="<?php echo plugin_dir_url( __FILE__ ) . 'icon.svg'; ?>" alt=""/>
22
+ </div>
23
+ <div id="lockout-text" class="flex box3">
24
+ <h1><?php esc_html_e( 'You have been locked out.', 'better-wp-security' ) ?></h1>
25
+ <p style="font-weight:bold;"><?php echo $message; ?></p>
26
+ <?php do_action( 'itsec_lockout_template_before_actions', $context ); ?>
27
+ <?php foreach ( $actions as $action ): ?>
28
+ <a class="btn <?php echo empty( $action['secondary'] ) ? '' : 'secondary' ?>" href="<?php echo esc_url( $action['uri'] ); ?>">
29
+ <?php echo $action['label']; ?>
30
+ </a>
31
+ <?php endforeach; ?>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </body>
36
+ </html>
history.txt CHANGED
@@ -847,3 +847,15 @@
847
  Bug Fix: Hide Backend Bypass.
848
  Bug Fix: Strict Standards error during Sync request.
849
  Bug Fix: wp_die() if a login interstitial session fails to be created instead of throwing a fatal error.
 
 
 
 
 
 
 
 
 
 
 
 
847
  Bug Fix: Hide Backend Bypass.
848
  Bug Fix: Strict Standards error during Sync request.
849
  Bug Fix: wp_die() if a login interstitial session fails to be created instead of throwing a fatal error.
850
+ 7.5.0 - 2019-11-13 - Timothy Jacobs
851
+ Breaking Change: iThemes Security requires PHP 5.4 or later.
852
+ Enhancement: New Lockout Template screen.
853
+ Enhancement: Add confirmation button to Login Interstitial Async Actions when on a different device.
854
+ Enhancement: Add filter to "Lookup IP" link.
855
+ Developer Note: There were significant changes to the internals of the iThemes Security Lockout API in this release. If you are using the ITSEC_Lockout class directly, all the API functions will continue to work, but will emit deprecation notices when legacy behavior is being used. Please update any integrations.
856
+ Bug Fix: Brute Force module reporting invalid logins using an email address incorrectly.
857
+ Bug Fix: Improve lockout compatibility with caching plugins.
858
+ Bug Fix: Fix admin notice not being dismissed due to a REST API route that was more narrowly defined than necessary.
859
+ Bug Fix: Admin Notices list did not refresh after dismissing a notice.
860
+ Bug Fix: Strong Passwords zxcvbn Library was not evaluating penalty strings correctly.
861
+ Bug Fix: Fix PHP warning if there are multiple detected proxy headers.
package.json CHANGED
@@ -109,6 +109,8 @@
109
  "test-unit:coverage": "npm run test-unit -- --coverage",
110
  "test-unit:update": "npm run test-unit -- --updateSnapshot",
111
  "test-unit:watch": "npm run test-unit -- --watch",
112
- "watch": "./node_modules/.bin/webpack --watch"
 
 
113
  }
114
  }
109
  "test-unit:coverage": "npm run test-unit -- --coverage",
110
  "test-unit:update": "npm run test-unit -- --updateSnapshot",
111
  "test-unit:watch": "npm run test-unit -- --watch",
112
+ "watch": "./node_modules/.bin/webpack --watch",
113
+ "test-acceptance": "docker-compose exec -w /bitnami/wordpress/wp-content/plugins/ithemes-security-pro wordpress ./vendor/bin/codecept run acceptance",
114
+ "test-acceptance:build": "docker-compose exec -w /bitnami/wordpress/wp-content/plugins/ithemes-security-pro wordpress ./vendor/bin/codecept build"
115
  }
116
  }
readme.txt CHANGED
@@ -2,9 +2,9 @@
2
  Contributors: ithemes, chrisjean, 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.2.2
6
- Stable tag: 7.4.1
7
- Requires PHP: 5.2
8
  License: GPLv2 or later
9
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
10
 
@@ -189,6 +189,19 @@ Free support may be available with the help of the community in the <a href="htt
189
 
190
  == Changelog ==
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  = 7.4.1 =
193
  * Enhancement: New iThemes Sync Verb support for File Change.
194
  * Tweak: Add additional information about the login attempt when calling the Network Brute Force API.
@@ -547,5 +560,5 @@ Free support may be available with the help of the community in the <a href="htt
547
 
548
  == Upgrade Notice ==
549
 
550
- = 7.4.1 =
551
- Version 7.4.1 contains important improvements. It is recommended for all users.
2
  Contributors: ithemes, chrisjean, 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.3.0
6
+ Stable tag: 7.5.0
7
+ Requires PHP: 5.4
8
  License: GPLv2 or later
9
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
10
 
189
 
190
  == Changelog ==
191
 
192
+ = 7.5.0 =
193
+ * Breaking Change: iThemes Security requires PHP 5.4 or later.
194
+ * Enhancement: New Lockout Template screen.
195
+ * Enhancement: Add confirmation button to Login Interstitial Async Actions when on a different device.
196
+ * Enhancement: Add filter to "Lookup IP" link.
197
+ * Developer Note: There were significant changes to the internals of the iThemes Security Lockout API in this release. If you are using the ITSEC_Lockout class directly, all the API functions will continue to work, but will emit deprecation notices when legacy behavior is being used. Please update any integrations.
198
+ * Bug Fix: Brute Force module reporting invalid logins using an email address incorrectly.
199
+ * Bug Fix: Improve lockout compatibility with caching plugins.
200
+ * Bug Fix: Fix admin notice not being dismissed due to a REST API route that was more narrowly defined than necessary.
201
+ * Bug Fix: Admin Notices list did not refresh after dismissing a notice.
202
+ * Bug Fix: Strong Passwords zxcvbn Library was not evaluating penalty strings correctly.
203
+ * Bug Fix: Fix PHP warning if there are multiple detected proxy headers.
204
+
205
  = 7.4.1 =
206
  * Enhancement: New iThemes Sync Verb support for File Change.
207
  * Tweak: Add additional information about the login attempt when calling the Network Brute Force API.
560
 
561
  == Upgrade Notice ==
562
 
563
+ = 7.5.0 =
564
+ Version 7.5.0 contains new features and bug fixes. It is recommended for all users.