iThemes Security (formerly Better WP Security) - Version 6.6.0

Version Description

  • New Feature: Added a new setting in WordPress Tweaks: "Login with Email Address or Username".
    • Enhancement: Host email images from the plugin instead of relying on iThemes servers to help email clients marking messages as spam or blocking images.
    • Bug Fix: Error when searching for modules preventing modules from appearing.
    • Bug Fix: Use the wp_options table when acquiring locks in Multisite.
    • Bug Fix: Prevent duplicate daily digest emails on sites with high load.
    • Misc: Added Magic Links, a new Pro-only feature, to be activated by Security Check.
    • Misc: Rearranged modules to be listed alphabetically.
Download this release

Release Info

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

Code changes from version 6.5.1 to 6.6.0

better-wp-security.php CHANGED
@@ -6,7 +6,7 @@
6
  * Description: Take the guesswork out of WordPress security. iThemes Security offers 30+ ways to lock down WordPress in an easy-to-use WordPress security plugin.
7
  * Author: iThemes
8
  * Author URI: https://ithemes.com
9
- * Version: 6.5.1
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
6
  * Description: Take the guesswork out of WordPress security. iThemes Security offers 30+ ways to lock down WordPress in an easy-to-use WordPress security plugin.
7
  * Author: iThemes
8
  * Author URI: https://ithemes.com
9
+ * Version: 6.6.0
10
  * Text Domain: better-wp-security
11
  * Network: True
12
  * License: GPLv2
core/admin-pages/js/script.js CHANGED
@@ -571,7 +571,7 @@ var itsecSettingsPage = {
571
 
572
  var $window = jQuery( window ), height = $window.height(), width = $window.width(), offset = $el.offset();
573
 
574
- if ( ! $el ) {
575
  return false;
576
  }
577
 
571
 
572
  var $window = jQuery( window ), height = $window.height(), width = $window.width(), offset = $el.offset();
573
 
574
+ if ( ! $el || ! offset ) {
575
  return false;
576
  }
577
 
core/admin-pages/page-settings.php CHANGED
@@ -2,7 +2,7 @@
2
 
3
 
4
  final class ITSEC_Settings_Page {
5
- private $version = 1.7;
6
 
7
  private static $instance;
8
 
2
 
3
 
4
  final class ITSEC_Settings_Page {
5
+ private $version = 1.8;
6
 
7
  private static $instance;
8
 
core/core.php CHANGED
@@ -141,6 +141,12 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
141
  add_action( 'wp_login_failed', array( 'ITSEC_Lib', 'handle_wp_login_failed' ) );
142
 
143
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
 
 
 
 
 
 
144
  }
145
 
146
  /**
@@ -217,32 +223,34 @@ if ( ! class_exists( 'ITSEC_Core' ) ) {
217
  ITSEC_Modules::register_module( 'security-check', "$path/modules/security-check", 'always-active' );
218
  ITSEC_Modules::register_module( 'global', "$path/modules/global", 'always-active' );
219
  ITSEC_Modules::register_module( '404-detection', "$path/modules/404-detection" );
 
220
  ITSEC_Modules::register_module( 'away-mode', "$path/modules/away-mode" );
221
  ITSEC_Modules::register_module( 'ban-users', "$path/modules/ban-users", 'default-active' );
222
  include( "$path/modules/ban-users/init.php" ); // Provides the itsec_ban_users_handle_new_blacklisted_ip function which is always needed.
223
- ITSEC_Modules::register_module( 'brute-force', "$path/modules/brute-force", 'default-active' );
224
- ITSEC_Modules::register_module( 'core', "$path/modules/core", 'always-active' );
225
  ITSEC_Modules::register_module( 'backup', "$path/modules/backup", 'default-active' );
 
226
  ITSEC_Modules::register_module( 'file-change', "$path/modules/file-change" );
227
  ITSEC_Modules::register_module( 'file-permissions', "$path/modules/file-permissions", 'always-active' );
228
  ITSEC_Modules::register_module( 'hide-backend', "$path/modules/hide-backend", 'always-active' );
229
- ITSEC_Modules::register_module( 'network-brute-force', "$path/modules/ipcheck", 'default-active' );
230
- ITSEC_Modules::register_module( 'malware', "$path/modules/malware", 'always-active' );
231
- ITSEC_Modules::register_module( 'ssl', "$path/modules/ssl" );
232
- ITSEC_Modules::register_module( 'strong-passwords', "$path/modules/strong-passwords", 'default-active' );
233
- ITSEC_Modules::register_module( 'system-tweaks', "$path/modules/system-tweaks" );
234
- ITSEC_Modules::register_module( 'wordpress-tweaks', "$path/modules/wordpress-tweaks", 'default-active' );
235
 
236
  if ( is_multisite() ) {
237
  ITSEC_Modules::register_module( 'multisite-tweaks', "$path/modules/multisite-tweaks" );
238
  }
239
 
240
- ITSEC_Modules::register_module( 'admin-user', "$path/modules/admin-user", 'always-active' );
 
 
 
241
  ITSEC_Modules::register_module( 'wordpress-salts', "$path/modules/salts", 'always-active' );
242
- ITSEC_Modules::register_module( 'content-directory', "$path/modules/content-directory", 'always-active' );
243
- ITSEC_Modules::register_module( 'database-prefix', "$path/modules/database-prefix", 'always-active' );
244
  ITSEC_Modules::register_module( 'file-writing', "$path/modules/file-writing", 'always-active' );
245
 
 
 
246
  if ( ! ITSEC_Core::is_pro() ) {
247
  ITSEC_Modules::register_module( 'pro-module-upsells', "$path/modules/pro", 'always-active' );
248
  }
141
  add_action( 'wp_login_failed', array( 'ITSEC_Lib', 'handle_wp_login_failed' ) );
142
 
143
  add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
144
+
145
+ if ( ! wp_next_scheduled( 'itsec_clear_locks' ) ) {
146
+ wp_schedule_event( time(), 'daily', 'itsec_clear_locks' );
147
+ }
148
+
149
+ add_action( 'itsec_clear_locks', array( 'ITSEC_Lib', 'delete_expired_locks' ) );
150
  }
151
 
152
  /**
223
  ITSEC_Modules::register_module( 'security-check', "$path/modules/security-check", 'always-active' );
224
  ITSEC_Modules::register_module( 'global', "$path/modules/global", 'always-active' );
225
  ITSEC_Modules::register_module( '404-detection', "$path/modules/404-detection" );
226
+ ITSEC_Modules::register_module( 'admin-user', "$path/modules/admin-user", 'always-active' );
227
  ITSEC_Modules::register_module( 'away-mode', "$path/modules/away-mode" );
228
  ITSEC_Modules::register_module( 'ban-users', "$path/modules/ban-users", 'default-active' );
229
  include( "$path/modules/ban-users/init.php" ); // Provides the itsec_ban_users_handle_new_blacklisted_ip function which is always needed.
230
+ ITSEC_Modules::register_module( 'content-directory', "$path/modules/content-directory", 'always-active' );
231
+ ITSEC_Modules::register_module( 'database-prefix', "$path/modules/database-prefix", 'always-active' );
232
  ITSEC_Modules::register_module( 'backup', "$path/modules/backup", 'default-active' );
233
+ ITSEC_Modules::register_module( 'core', "$path/modules/core", 'always-active' );
234
  ITSEC_Modules::register_module( 'file-change', "$path/modules/file-change" );
235
  ITSEC_Modules::register_module( 'file-permissions', "$path/modules/file-permissions", 'always-active' );
236
  ITSEC_Modules::register_module( 'hide-backend', "$path/modules/hide-backend", 'always-active' );
237
+ ITSEC_Modules::register_module( 'brute-force', "$path/modules/brute-force", 'default-active' );
 
 
 
 
 
238
 
239
  if ( is_multisite() ) {
240
  ITSEC_Modules::register_module( 'multisite-tweaks', "$path/modules/multisite-tweaks" );
241
  }
242
 
243
+ ITSEC_Modules::register_module( 'network-brute-force', "$path/modules/ipcheck", 'default-active' );
244
+ ITSEC_Modules::register_module( 'ssl', "$path/modules/ssl" );
245
+ ITSEC_Modules::register_module( 'strong-passwords', "$path/modules/strong-passwords", 'default-active' );
246
+ ITSEC_Modules::register_module( 'system-tweaks', "$path/modules/system-tweaks" );
247
  ITSEC_Modules::register_module( 'wordpress-salts', "$path/modules/salts", 'always-active' );
248
+ ITSEC_Modules::register_module( 'wordpress-tweaks', "$path/modules/wordpress-tweaks", 'default-active' );
249
+
250
  ITSEC_Modules::register_module( 'file-writing', "$path/modules/file-writing", 'always-active' );
251
 
252
+ ITSEC_Modules::register_module( 'malware', "$path/modules/malware", 'always-active' );
253
+
254
  if ( ! ITSEC_Core::is_pro() ) {
255
  ITSEC_Modules::register_module( 'pro-module-upsells', "$path/modules/pro", 'always-active' );
256
  }
core/history.txt CHANGED
@@ -567,3 +567,11 @@
567
  3.7.1 - 2017-08-23 - Chris Jean & Timothy Jacobs
568
  Bug Fix: Fixed logical error that prevented backups from executing.
569
  Bug Fix: Fixed issue that could cause database locks to flood the database.
 
 
 
 
 
 
 
 
567
  3.7.1 - 2017-08-23 - Chris Jean & Timothy Jacobs
568
  Bug Fix: Fixed logical error that prevented backups from executing.
569
  Bug Fix: Fixed issue that could cause database locks to flood the database.
570
+ 3.8.0 - 2017-08-31 - Chris Jean & Timothy Jacobs
571
+ New Feature: Added a new setting in WordPress Tweaks: "Login with Email Address or Username".
572
+ Enhancement: Host email images from the plugin instead of relying on iThemes servers to help email clients marking messages as spam or blocking images.
573
+ Bug Fix: Error when searching for modules preventing modules from appearing.
574
+ Bug Fix: Use the wp_options table when acquiring locks in Multisite.
575
+ Bug Fix: Prevent duplicate daily digest emails on sites with high load.
576
+ Misc: Added Magic Links, a new Pro-only feature, to be activated by Security Check.
577
+ Misc: Rearranged modules to be listed alphabetically.
core/img/mail/article_icon.png ADDED
Binary file
core/img/mail/attachment_icon.png ADDED
Binary file
core/img/mail/documentation_icon.png ADDED
Binary file
core/img/mail/footer_logo.png ADDED
Binary file
core/img/mail/icon_folder.png ADDED
Binary file
core/img/mail/icon_lock.png ADDED
Binary file
core/img/mail/icon_message.png ADDED
Binary file
core/img/mail/index.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php // Silence is golden
core/img/mail/info_icon.png ADDED
Binary file
core/img/mail/logo.png ADDED
Binary file
core/img/mail/pro_logo.png ADDED
Binary file
core/img/mail/pro_logo_no_text.png ADDED
Binary file
core/img/mail/support_icon.png ADDED
Binary file
core/img/mail/video_icon.png ADDED
Binary file
core/img/mail/warning_icon_yellow.png ADDED
Binary file
core/img/mail/wp_security_book.png ADDED
Binary file
core/lib.php CHANGED
@@ -1033,21 +1033,26 @@ final class ITSEC_Lib {
1033
 
1034
  /** @var \wpdb $wpdb */
1035
  global $wpdb;
 
1036
 
1037
  $lock = "itsec-lock-{$name}";
1038
  $now = ITSEC_Core::get_current_time_gmt();
1039
  $release_at = $now + $expires_in;
1040
 
1041
- if ( empty( $wpdb->sitemeta ) ) {
1042
- $result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, 'no') /* LOCK */", $lock, $release_at ) );
1043
  } else {
1044
- $result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->sitemeta` (`site_id`, `meta_key`, `meta_value`) VALUES (%d, %s, %s) /* LOCK */", $wpdb->siteid, $lock, $release_at ) );
1045
  }
1046
 
1047
  // The lock exists. See if it has expired.
1048
  if ( ! $result ) {
1049
 
1050
- $locked_until = get_site_option( $lock );
 
 
 
 
1051
 
1052
  if ( ! $locked_until ) {
1053
  // Can't write or read the lock. Bail due to an unknown and hopefully temporary error.
@@ -1061,7 +1066,28 @@ final class ITSEC_Lib {
1061
  }
1062
 
1063
  // Ensure that the lock is set properly by triggering all the regular actions and filters.
1064
- update_site_option( $lock, $release_at );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1065
 
1066
  return true;
1067
  }
@@ -1074,7 +1100,95 @@ final class ITSEC_Lib {
1074
  * @param string $name The lock name.
1075
  */
1076
  public static function release_lock( $name ) {
1077
- delete_site_option( "itsec-lock-{$name}" );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1078
  }
1079
 
1080
  /**
1033
 
1034
  /** @var \wpdb $wpdb */
1035
  global $wpdb;
1036
+ $main_options = $wpdb->base_prefix . 'options';
1037
 
1038
  $lock = "itsec-lock-{$name}";
1039
  $now = ITSEC_Core::get_current_time_gmt();
1040
  $release_at = $now + $expires_in;
1041
 
1042
+ if ( is_multisite() ) {
1043
+ $result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `{$main_options}` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, 'no') /* LOCK */", $lock, $release_at ) );
1044
  } else {
1045
+ $result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, 'no') /* LOCK */", $lock, $release_at ) );
1046
  }
1047
 
1048
  // The lock exists. See if it has expired.
1049
  if ( ! $result ) {
1050
 
1051
+ if ( is_multisite() && get_current_blog_id() !== 1 ) {
1052
+ $locked_until = $wpdb->get_var( $wpdb->prepare( "SELECT `option_value` FROM {$main_options} WHERE `option_name` = %s", $main_options ) );
1053
+ } else {
1054
+ $locked_until = get_option( $lock );
1055
+ }
1056
 
1057
  if ( ! $locked_until ) {
1058
  // Can't write or read the lock. Bail due to an unknown and hopefully temporary error.
1066
  }
1067
 
1068
  // Ensure that the lock is set properly by triggering all the regular actions and filters.
1069
+ if ( ! is_multisite() || get_current_blog_id() === 1 ) {
1070
+ update_option( $lock, $release_at );
1071
+ } else {
1072
+ $wpdb->update( $main_options, array( 'option_value' => $release_at ), array( 'option_name' => $lock ) );
1073
+
1074
+ if ( function_exists( 'wp_cache_switch_to_blog' ) ) {
1075
+ // Update persistent object caches
1076
+ $current = get_current_blog_id();
1077
+ wp_cache_switch_to_blog( 1 );
1078
+
1079
+ $alloptions = wp_cache_get( 'alloptions' );
1080
+
1081
+ if ( is_array( $alloptions ) && isset( $alloptions[ $lock ] ) ) {
1082
+ $alloptions[ $lock ] = $release_at;
1083
+ wp_cache_set( 'alloptions', $alloptions, 'options' );
1084
+ } else {
1085
+ wp_cache_set( $lock, $release_at, 'options' );
1086
+ }
1087
+
1088
+ wp_cache_switch_to_blog( $current );
1089
+ }
1090
+ }
1091
 
1092
  return true;
1093
  }
1100
  * @param string $name The lock name.
1101
  */
1102
  public static function release_lock( $name ) {
1103
+
1104
+ $lock = "itsec-lock-{$name}";
1105
+
1106
+ if ( is_multisite() && get_current_blog_id() !== 1 ) {
1107
+
1108
+ /** @var \wpdb $wpdb */
1109
+ global $wpdb;
1110
+ $main_options = $wpdb->base_prefix . 'options';
1111
+
1112
+ $wpdb->delete( $main_options, array( 'option_name' => $lock ) );
1113
+
1114
+ if ( function_exists( 'wp_cache_switch_to_blog' ) ) {
1115
+ // Update persistent object caches
1116
+ $current = get_current_blog_id();
1117
+ wp_cache_switch_to_blog( 1 );
1118
+
1119
+ $alloptions = wp_cache_get( 'alloptions' );
1120
+
1121
+ if ( is_array( $alloptions ) && isset( $alloptions[ $lock ] ) ) {
1122
+ unset( $alloptions[$lock] );
1123
+ wp_cache_set( 'alloptions', $alloptions, 'options' );
1124
+ } else {
1125
+ wp_cache_delete( $lock, 'options' );
1126
+ }
1127
+
1128
+ wp_cache_switch_to_blog( $current );
1129
+ }
1130
+ } else {
1131
+ delete_option( $lock );
1132
+ }
1133
+ }
1134
+
1135
+ /**
1136
+ * Clear any expired locks.
1137
+ *
1138
+ * The vast majority of locks should be cleared by the same process that acquires them, however, this will clear locks that remain
1139
+ * due to a time out or fatal error.
1140
+ *
1141
+ * @since 3.8.0
1142
+ */
1143
+ public static function delete_expired_locks() {
1144
+
1145
+ /** @var \wpdb $wpdb */
1146
+ global $wpdb;
1147
+ $main_options = $wpdb->base_prefix . 'options';
1148
+
1149
+ $rows = $wpdb->get_results( $wpdb->prepare(
1150
+ "SELECT `option_name` FROM {$main_options} WHERE `option_name` LIKE %s AND `option_value` < %d",
1151
+ $wpdb->esc_like( 'itsec-lock-' ) . '%', ITSEC_Core::get_current_time_gmt()
1152
+ ) );
1153
+
1154
+ if ( $rows ) {
1155
+ if ( is_multisite() && get_current_blog_id() !== 1 ) {
1156
+ if ( function_exists( 'wp_cache_switch_to_blog' ) ) {
1157
+ // Update persistent object caches
1158
+ $current = get_current_blog_id();
1159
+ wp_cache_switch_to_blog( 1 );
1160
+
1161
+ $alloptions = wp_cache_get( 'alloptions' );
1162
+ $set_all = false;
1163
+
1164
+ foreach ( $rows as $row ) {
1165
+ $lock = $row->option_name;
1166
+
1167
+ if ( is_array( $alloptions ) && isset( $alloptions[ $lock ] ) ) {
1168
+ unset( $alloptions[$lock] );
1169
+ $set_all = true;
1170
+ } else {
1171
+ wp_cache_delete( $lock, 'options' );
1172
+ }
1173
+ }
1174
+
1175
+ if ( $set_all ) {
1176
+ wp_cache_set( 'alloptions', $alloptions );
1177
+ }
1178
+
1179
+ wp_cache_switch_to_blog( $current );
1180
+ }
1181
+
1182
+ $wpdb->query( $wpdb->prepare(
1183
+ "DELETE FROM {$main_options} WHERE `option_name` LIKE %s AND `option_value` < %d",
1184
+ $wpdb->esc_like( 'itsec-lock-' ) . '%', ITSEC_Core::get_current_time_gmt()
1185
+ ) );
1186
+ } else {
1187
+ foreach ( $rows as $row ) {
1188
+ delete_option( $row->option_name );
1189
+ }
1190
+ }
1191
+ }
1192
  }
1193
 
1194
  /**
core/lib/class-itsec-mail.php CHANGED
@@ -19,7 +19,7 @@ final class ITSEC_Mail {
19
  'charset' => esc_attr( get_bloginfo( 'charset' ) ),
20
  'title_tag' => $title,
21
  'banner_title' => $banner_title,
22
- 'logo' => ITSEC_Core::is_pro() ? 'https://ithemes.com/email_images/itsec-pro-logo-300x127.png' : 'https://ithemes.com/email_images/itsec-logo-300x127.png',
23
  'title' => $title,
24
  );
25
 
@@ -38,6 +38,18 @@ final class ITSEC_Mail {
38
  return $content;
39
  }
40
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  public function add_footer() {
42
  $footer = '';
43
 
@@ -100,7 +112,7 @@ final class ITSEC_Mail {
100
  }
101
 
102
  public function add_info_box( $content, $icon_type = 'info' ) {
103
- $icon_url = "http://ithemes.com/email_images/itsec-$icon_type-icon.png";
104
 
105
  $module = $this->get_template( 'info-box.html' );
106
  $module = $this->replace_all( $module, compact( 'content', 'icon_url' ) );
@@ -120,7 +132,7 @@ final class ITSEC_Mail {
120
  $heading = $this->get_template( 'section-heading.html' );
121
  $heading = $this->replace_all( $heading, compact( 'content' ) );
122
  } else {
123
- $icon_url = "https://ithemes.com/email_images/itsec-icon-$icon_type.png";
124
 
125
  $heading = $this->get_template( 'section-heading-with-icon.html' );
126
  $heading = $this->replace_all( $heading, compact( 'content', 'icon_url' ) );
@@ -233,7 +245,11 @@ final class ITSEC_Mail {
233
  }
234
 
235
  private function get_template( $template ) {
236
- return file_get_contents( $this->template_path . $template );
 
 
 
 
237
  }
238
 
239
  public static function filter_admin_page_url( $url ) {
19
  'charset' => esc_attr( get_bloginfo( 'charset' ) ),
20
  'title_tag' => $title,
21
  'banner_title' => $banner_title,
22
+ 'logo' => ITSEC_Core::is_pro() ? $this->get_image_url( 'pro_logo' ) : $this->get_image_url( 'logo' ),
23
  'title' => $title,
24
  );
25
 
38
  return $content;
39
  }
40
 
41
+ private function replace_images( $content ) {
42
+ return preg_replace_callback( '/{! \$([a-zA-Z_][\w]*) }}/', array( $this, 'replace_image_callback' ), $content );
43
+ }
44
+
45
+ private function replace_image_callback( $matches ) {
46
+ if ( empty( $matches ) || empty( $matches[1] ) ) {
47
+ return '';
48
+ }
49
+
50
+ return esc_url( $this->get_image_url( $matches[1] ) );
51
+ }
52
+
53
  public function add_footer() {
54
  $footer = '';
55
 
112
  }
113
 
114
  public function add_info_box( $content, $icon_type = 'info' ) {
115
+ $icon_url = $this->get_image_url( "{$icon_type}_icon" );
116
 
117
  $module = $this->get_template( 'info-box.html' );
118
  $module = $this->replace_all( $module, compact( 'content', 'icon_url' ) );
132
  $heading = $this->get_template( 'section-heading.html' );
133
  $heading = $this->replace_all( $heading, compact( 'content' ) );
134
  } else {
135
+ $icon_url = $this->get_image_url( "icon_{$icon_type}" );
136
 
137
  $heading = $this->get_template( 'section-heading-with-icon.html' );
138
  $heading = $this->replace_all( $heading, compact( 'content', 'icon_url' ) );
245
  }
246
 
247
  private function get_template( $template ) {
248
+ return $this->replace_images( file_get_contents( $this->template_path . $template ) );
249
+ }
250
+
251
+ private function get_image_url( $name ) {
252
+ return plugin_dir_url( ITSEC_Core::get_core_dir() . 'img/mail/index.php' ) . "{$name}.png";
253
  }
254
 
255
  public static function filter_admin_page_url( $url ) {
core/lib/mail-templates/footer.html CHANGED
@@ -32,7 +32,7 @@
32
  <table class="container" align="left" border="0" cellpadding="0" cellspacing="0" width="260" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
33
  <tr>
34
  <td class="container-cell" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
35
- <img class="preserve-ratio" src="http://ithemes.com/email_images/itsec-article-icon.png" style="max-width: 61px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="61" alt="" align="center">
36
  <br>
37
  <h4 style="color: #202020;font-family: Helvetica;font-size: 20px;font-weight: bold;line-height: 150%;margin: 0;padding: 0;text-align: center;">
38
  <a href="https://ithemes.com/category/wordpress-security/" target="_blank" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;text-decoration: none;">{{ $articles }}</a>
@@ -45,7 +45,7 @@
45
  <table class="container" align="right" border="0" cellpadding="0" cellspacing="0" width="260" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
46
  <tr>
47
  <td class="container-cell container-cell-bottom" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
48
- <img class="preserve-ratio" src="http://ithemes.com/email_images/itsec-video-icon.png" style="max-width: 61px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="61" alt="" align="center">
49
  <br>
50
  <h4 style="color: #202020;font-family: Helvetica;font-size: 20px;font-weight: bold;line-height: 150%;margin: 0;padding: 0;text-align: center;">
51
  <a href="https://ithemes.com/tutorial/category/ithemes-security/" target="_blank" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;text-decoration: none;">{{ $tutorials }}</a>
@@ -120,7 +120,7 @@
120
  <table class="container" align="left" border="0" cellpadding="0" cellspacing="0" width="260" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
121
  <tr>
122
  <td class="container-cell" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
123
- <img class="preserve-ratio" src="http://ithemes.com/email_images/itsec-documentation-icon.png" style="max-width: 62px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="62" alt="" align="center">
124
  <br>
125
  <h4 style="color: #202020;font-family: Helvetica;font-size: 20px;font-weight: bold;line-height: 150%;margin: 0;padding: 0;text-align: center;">
126
  <a href="http://ithemes.com/codex/page/IThemes_Security" target="_blank" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;text-decoration: none;">{{ $documentation }}</a>
@@ -133,7 +133,7 @@
133
  <table class="container" align="right" border="0" cellpadding="0" cellspacing="0" width="260" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
134
  <tr>
135
  <td class="container-cell container-cell-bottom" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
136
- <img class="preserve-ratio" src="http://ithemes.com/email_images/itsec-support-icon.png" style="max-width: 62px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="62" alt="" align="center">
137
  <br>
138
  <h4 style="color: #202020;font-family: Helvetica;font-size: 20px;font-weight: bold;line-height: 150%;margin: 0;padding: 0;text-align: center;">
139
  <a href="https://members.ithemes.com/panel/helpdesk.php" target="_blank" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;text-decoration: none;">{{ $support }}</a>
@@ -163,7 +163,7 @@
163
  <table class="container" align="left" border="0" cellpadding="0" cellspacing="0" width="104" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
164
  <tr>
165
  <td class="section-padding-bottom" align="left" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;padding-bottom: 20px;">
166
- <img class="preserve-ratio" src="https://ithemes.com/email_images/wp-security-book-cropped.png" style="max-width: 84px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="84" alt="" align="center">
167
  </td>
168
  </tr>
169
  </table>
@@ -194,7 +194,7 @@
194
  <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
195
  <tr>
196
  <td class="container-cell" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 200%;text-align: center;padding-bottom: 0;padding-top: 60px;">
197
- <img class="preserve-ratio" src="http://ithemes.com/email_images/itsec-footer-logo.png" style="max-width: 50px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="50" alt="" align="center"><br>
198
  <br>
199
  <a href="{{ $security_settings_link }}" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 11px;line-height: 200%;text-align: center;text-decoration: none;font-weight: bold;">{{ $unsubscribe_link_text }}</a>
200
  </td>
32
  <table class="container" align="left" border="0" cellpadding="0" cellspacing="0" width="260" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
33
  <tr>
34
  <td class="container-cell" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
35
+ <img class="preserve-ratio" src="{! $article_icon }}" style="max-width: 61px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="61" alt="" align="center">
36
  <br>
37
  <h4 style="color: #202020;font-family: Helvetica;font-size: 20px;font-weight: bold;line-height: 150%;margin: 0;padding: 0;text-align: center;">
38
  <a href="https://ithemes.com/category/wordpress-security/" target="_blank" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;text-decoration: none;">{{ $articles }}</a>
45
  <table class="container" align="right" border="0" cellpadding="0" cellspacing="0" width="260" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
46
  <tr>
47
  <td class="container-cell container-cell-bottom" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
48
+ <img class="preserve-ratio" src="{! $video_icon }}" style="max-width: 61px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="61" alt="" align="center">
49
  <br>
50
  <h4 style="color: #202020;font-family: Helvetica;font-size: 20px;font-weight: bold;line-height: 150%;margin: 0;padding: 0;text-align: center;">
51
  <a href="https://ithemes.com/tutorial/category/ithemes-security/" target="_blank" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;text-decoration: none;">{{ $tutorials }}</a>
120
  <table class="container" align="left" border="0" cellpadding="0" cellspacing="0" width="260" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
121
  <tr>
122
  <td class="container-cell" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
123
+ <img class="preserve-ratio" src="{! $documentation_icon }}" style="max-width: 62px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="62" alt="" align="center">
124
  <br>
125
  <h4 style="color: #202020;font-family: Helvetica;font-size: 20px;font-weight: bold;line-height: 150%;margin: 0;padding: 0;text-align: center;">
126
  <a href="http://ithemes.com/codex/page/IThemes_Security" target="_blank" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;text-decoration: none;">{{ $documentation }}</a>
133
  <table class="container" align="right" border="0" cellpadding="0" cellspacing="0" width="260" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
134
  <tr>
135
  <td class="container-cell container-cell-bottom" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
136
+ <img class="preserve-ratio" src="{! $support_icon }}" style="max-width: 62px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="62" alt="" align="center">
137
  <br>
138
  <h4 style="color: #202020;font-family: Helvetica;font-size: 20px;font-weight: bold;line-height: 150%;margin: 0;padding: 0;text-align: center;">
139
  <a href="https://members.ithemes.com/panel/helpdesk.php" target="_blank" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;text-decoration: none;">{{ $support }}</a>
163
  <table class="container" align="left" border="0" cellpadding="0" cellspacing="0" width="104" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
164
  <tr>
165
  <td class="section-padding-bottom" align="left" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;padding-bottom: 20px;">
166
+ <img class="preserve-ratio" src="{! $wp_security_book }}" style="max-width: 84px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="84" alt="" align="center">
167
  </td>
168
  </tr>
169
  </table>
194
  <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
195
  <tr>
196
  <td class="container-cell" valign="top" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 200%;text-align: center;padding-bottom: 0;padding-top: 60px;">
197
+ <img class="preserve-ratio" src="{! $footer_logo }}" style="max-width: 50px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="50" alt="" align="center"><br>
198
  <br>
199
  <a href="{{ $security_settings_link }}" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #0084CB;font-family: Helvetica;font-size: 11px;line-height: 200%;text-align: center;text-decoration: none;font-weight: bold;">{{ $unsubscribe_link_text }}</a>
200
  </td>
core/lib/mail-templates/pro-callout.html CHANGED
@@ -9,7 +9,7 @@
9
  <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
10
  <tr>
11
  <td valign="top" class="container-cell" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
12
- <img class="preserve-ratio" src="http://ithemes.com/email_images/itsec-pro-logo-no-text-100x118.png" style="max-width: 100px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="100" alt="" align="center">
13
  <p class="two-factor" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;font-family: Helvetica;font-size: 16px;line-height: 150%;margin-top: 20px;margin-right: 0;margin-bottom: 20px;margin-left: 0;padding: 0;text-align: center;color: #FFFFFF;">{{ $two_factor }}</p>
14
  <table width="100%" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
15
  <tr>
9
  <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
10
  <tr>
11
  <td valign="top" class="container-cell" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;color: #404040;font-family: Helvetica;font-size: 16px;line-height: 150%;text-align: center;padding-bottom: 20px;">
12
+ <img class="preserve-ratio" src="{! $pro_logo_no_text }}" style="max-width: 100px;border: 0;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;height: auto;" width="100" alt="" align="center">
13
  <p class="two-factor" style="-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;font-family: Helvetica;font-size: 16px;line-height: 150%;margin-top: 20px;margin-right: 0;margin-bottom: 20px;margin-left: 0;padding: 0;text-align: center;color: #FFFFFF;">{{ $two_factor }}</p>
14
  <table width="100%" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;">
15
  <tr>
core/lockout.php CHANGED
@@ -79,7 +79,7 @@ final class ITSEC_Lockout {
79
  add_action( 'itsec_purge_lockouts', array( $this, 'purge_lockouts' ) );
80
 
81
  //Check for host lockouts
82
- add_action( 'init', array( $this, 'check_lockout' ) );
83
 
84
  // Ensure that locked out users are prevented from checking logins.
85
  add_filter( 'authenticate', array( $this, 'check_authenticate_lockout' ), 30 );
@@ -128,17 +128,40 @@ final class ITSEC_Lockout {
128
  return $user;
129
  }
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  /**
132
  * Checks if the host or user is locked out and executes lockout
133
  *
134
  * @since 4.0
135
  *
136
- * @param mixed $user WordPress user object or false
137
- * @param mixed $username the username to check
 
138
  *
139
  * @return void
140
  */
141
- public function check_lockout( $user = false, $username = false ) {
142
  global $wpdb, $itsec_globals;
143
 
144
  $wpdb->hide_errors(); //Hide database errors in case the tables aren't there
@@ -160,16 +183,16 @@ final class ITSEC_Lockout {
160
  $user_id = $user->ID;
161
 
162
  if ( $username !== false && $username != '' ) {
163
- $username_check = $wpdb->get_var( "SELECT `lockout_username` FROM `" . $wpdb->base_prefix . "itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > '" . date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ) . "' AND `lockout_username`='" . $username . "';" );
164
  }
165
 
166
- $host_check = $wpdb->get_var( "SELECT `lockout_host` FROM `" . $wpdb->base_prefix . "itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > '" . date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ) . "' AND `lockout_host`='" . $host . "';" );
167
 
168
  }
169
 
170
  if ( $user_id !== 0 && $user_id !== null ) {
171
 
172
- $user_check = $wpdb->get_var( "SELECT `lockout_user` FROM `" . $wpdb->base_prefix . "itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > '" . date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ) . "' AND `lockout_user`=" . intval( $user_id ) . ";" );
173
 
174
  }
175
 
@@ -179,18 +202,67 @@ final class ITSEC_Lockout {
179
  ITSEC_Lib::create_database_tables();
180
  }
181
 
182
- if ( $host_check !== null && $host_check !== false ) {
183
 
184
- $this->execute_lock();
 
 
 
185
 
186
- } elseif ( ( $user_check !== false && $user_check !== null ) || ( $username_check !== false && $username_check !== null ) ) {
 
 
 
 
 
 
 
 
 
 
187
 
188
- $this->execute_lock( true );
189
 
190
  }
191
 
192
  }
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  /**
195
  * This persists a lockout to storage or performs a permanent ban if appropriate.
196
  *
@@ -335,68 +407,93 @@ final class ITSEC_Lockout {
335
  /**
336
  * Executes lockout (locks user out)
337
  *
338
- * @param bool $user Is a specific user being locked out.
339
- * @param bool $network Was the host flagged by the Security Network.
340
  *
341
  * @return void
342
  */
343
- public function execute_lock( $user = false, $network = false ) {
344
 
345
- if ( ITSEC_Lib::is_ip_whitelisted( ITSEC_Lib::get_ip() ) ) {
346
- return;
347
  }
348
 
349
- global $itsec_globals;
350
-
351
- $current_user = wp_get_current_user();
352
-
353
- if ( is_object( $current_user ) && isset( $current_user->ID ) ) {
354
- wp_logout();
 
355
  }
356
 
357
- @header( 'HTTP/1.0 403 Forbidden' );
358
- @header( 'Cache-Control: no-cache, must-revalidate, max-age=0' );
359
- @header( 'Expires: Thu, 22 Jun 1978 00:28:00 GMT' );
360
- @header( 'Pragma: no-cache' );
361
 
362
  if ( $network === true ) { //lockout triggered by iThemes Network
363
 
364
  $message = ITSEC_Modules::get_setting( 'global', 'community_lockout_message' );
365
 
366
- if ( ! empty( $message ) ) {
367
- die( $message );
368
- } else {
369
-
370
- die( __( "Your IP address has been flagged as a threat by the iThemes Security network.", 'better-wp-security' ) );
371
-
372
  }
373
 
374
  } elseif ( $user === true ) { //lockout the user
375
 
376
  $message = ITSEC_Modules::get_setting( 'global', 'user_lockout_message' );
377
 
378
- if ( ! empty( $message ) ) {
379
- die( $message );
380
- } else {
381
-
382
- die( __( 'You have been locked out due to too many invalid login attempts.', 'better-wp-security' ) );
383
-
384
  }
385
 
386
  } else { //just lockout the host
387
 
388
  $message = ITSEC_Modules::get_setting( 'global', 'lockout_message' );
389
 
390
- if ( ! empty( $message ) ) {
391
- die( $message );
392
- } else {
 
393
 
394
- die( __( 'error', 'better-wp-security' ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
- }
397
 
 
 
398
  }
399
 
 
 
 
 
 
 
 
 
 
 
400
  }
401
 
402
  /**
@@ -799,15 +896,27 @@ final class ITSEC_Lockout {
799
  $this->send_lockout_email( $good_host, $good_user, $good_username, $host_expiration, $user_expiration, $reason );
800
  }
801
 
 
 
 
 
 
 
 
 
 
 
802
  if ( $good_host !== false ) {
803
 
804
  ITSEC_Lib::release_lock( $lock );
805
- $this->execute_lock();
806
 
807
  } else {
808
 
 
 
809
  ITSEC_Lib::release_lock( $lock );
810
- $this->execute_lock( true );
811
 
812
  }
813
 
79
  add_action( 'itsec_purge_lockouts', array( $this, 'purge_lockouts' ) );
80
 
81
  //Check for host lockouts
82
+ add_action( 'init', array( $this, 'check_current_user_for_host_lockouts' ) );
83
 
84
  // Ensure that locked out users are prevented from checking logins.
85
  add_filter( 'authenticate', array( $this, 'check_authenticate_lockout' ), 30 );
128
  return $user;
129
  }
130
 
131
+ /**
132
+ * Lockout a user on every page load if there host becomes locked.
133
+ */
134
+ public function check_current_user_for_host_lockouts() {
135
+
136
+ if ( ! is_user_logged_in() ) {
137
+ return;
138
+ }
139
+
140
+ global $wpdb;
141
+
142
+ $host = ITSEC_Lib::get_ip();
143
+ $host_check = $wpdb->get_var( $wpdb->prepare( "SELECT `lockout_host` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_host` = %s;", array(
144
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
145
+ $host
146
+ ) ) );
147
+
148
+ if ( $host_check ) {
149
+ $this->execute_lock();
150
+ }
151
+ }
152
+
153
  /**
154
  * Checks if the host or user is locked out and executes lockout
155
  *
156
  * @since 4.0
157
  *
158
+ * @param mixed $user WordPress user object or false.
159
+ * @param mixed $username The username to check.
160
+ * @param string $type Lockout type asking for the check.
161
  *
162
  * @return void
163
  */
164
+ public function check_lockout( $user = false, $username = false, $type = '' ) {
165
  global $wpdb, $itsec_globals;
166
 
167
  $wpdb->hide_errors(); //Hide database errors in case the tables aren't there
183
  $user_id = $user->ID;
184
 
185
  if ( $username !== false && $username != '' ) {
186
+ $username_check = $wpdb->get_results( "SELECT `lockout_username`, `lockout_type` FROM `" . $wpdb->base_prefix . "itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > '" . date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ) . "' AND `lockout_username`='" . $username . "';" );
187
  }
188
 
189
+ $host_check = $wpdb->get_results( "SELECT `lockout_host`, `lockout_type` FROM `" . $wpdb->base_prefix . "itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > '" . date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ) . "' AND `lockout_host`='" . $host . "';" );
190
 
191
  }
192
 
193
  if ( $user_id !== 0 && $user_id !== null ) {
194
 
195
+ $user_check = $wpdb->get_results( "SELECT `lockout_user`, `lockout_type` FROM `" . $wpdb->base_prefix . "itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > '" . date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ) . "' AND `lockout_user`=" . intval( $user_id ) . ";" );
196
 
197
  }
198
 
202
  ITSEC_Lib::create_database_tables();
203
  }
204
 
205
+ if ( $host_check ) {
206
 
207
+ $type = $type ? $type : $host_check[0]->lockout_type;
208
+ $this->execute_lock( array( 'type' => $type ) );
209
+
210
+ } elseif ( $user_check || $username_check ) {
211
 
212
+ if ( ! $type ) {
213
+ $type = $user_check ? $user_check[0]->lockout_type : $username_check[0]->lockout_type;
214
+ }
215
+
216
+ $lock_context = array( 'user_lock' => true, 'type' => $type );
217
+
218
+ if ( $user ) {
219
+ $lock_context['user'] = $user;
220
+ } elseif ( $username ) {
221
+ $lock_context['username'] = $username;
222
+ }
223
 
224
+ $this->execute_lock( $lock_context );
225
 
226
  }
227
 
228
  }
229
 
230
+ /**
231
+ * Check if a given username is locked out.
232
+ *
233
+ * @param string $username
234
+ *
235
+ * @return bool
236
+ */
237
+ public function is_username_locked_out( $username ) {
238
+
239
+ /** @var wpdb $wpdb */
240
+ global $wpdb;
241
+
242
+ return (bool) $wpdb->get_var( $wpdb->prepare(
243
+ "SELECT `lockout_username` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_username` = %s;",
244
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $username
245
+ ) );
246
+ }
247
+
248
+ /**
249
+ * Check if a given user is locked out.
250
+ *
251
+ * @param string $user_id
252
+ *
253
+ * @return bool
254
+ */
255
+ public function is_user_locked_out( $user_id ) {
256
+
257
+ /** @var wpdb $wpdb */
258
+ global $wpdb;
259
+
260
+ return (bool) $wpdb->get_var( $wpdb->prepare(
261
+ "SELECT `lockout_user` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_user` = %d;",
262
+ date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $user_id
263
+ ) );
264
+ }
265
+
266
  /**
267
  * This persists a lockout to storage or performs a permanent ban if appropriate.
268
  *
407
  /**
408
  * Executes lockout (locks user out)
409
  *
410
+ * @param array $context
411
+ * @param bool $deprecated Deprecated argument. Previously whether this is a network lock.
412
  *
413
  * @return void
414
  */
415
+ public function execute_lock( $context = array(), $deprecated = false ) {
416
 
417
+ if ( func_num_args() > 1 ) {
418
+ _deprecated_argument( __METHOD__, '6.5.0', 'A network lockout should be specified in the $context parameter.' );
419
  }
420
 
421
+ if ( is_array( $context ) ) {
422
+ $context = wp_parse_args( $context, array( 'user_lock' => false, 'network_lock' => false, 'type' => '' ) );
423
+ $user = $context['user_lock'];
424
+ $network = $context['network_lock'];
425
+ } else {
426
+ $user = $context;
427
+ $network = $deprecated;
428
  }
429
 
430
+ if ( ITSEC_Lib::is_ip_whitelisted( ITSEC_Lib::get_ip() ) ) {
431
+ return;
432
+ }
 
433
 
434
  if ( $network === true ) { //lockout triggered by iThemes Network
435
 
436
  $message = ITSEC_Modules::get_setting( 'global', 'community_lockout_message' );
437
 
438
+ if ( ! $message ) {
439
+ $message = __( 'Your IP address has been flagged as a threat by the iThemes Security network.', 'better-wp-security' );
 
 
 
 
440
  }
441
 
442
  } elseif ( $user === true ) { //lockout the user
443
 
444
  $message = ITSEC_Modules::get_setting( 'global', 'user_lockout_message' );
445
 
446
+ if ( ! $message ) {
447
+ $message = __( 'You have been locked out due to too many invalid login attempts.', 'better-wp-security' );
 
 
 
 
448
  }
449
 
450
  } else { //just lockout the host
451
 
452
  $message = ITSEC_Modules::get_setting( 'global', 'lockout_message' );
453
 
454
+ if ( ! $message ) {
455
+ $message = __( 'Error.', 'better-wp-security' );
456
+ }
457
+ }
458
 
459
+ $formatted = false;
460
+
461
+ if ( $context['type'] ) {
462
+ /**
463
+ * Filter the lockout message displayed to the user.
464
+ *
465
+ * @param string $message
466
+ * @param string $type
467
+ * @param array $context
468
+ */
469
+ $message = apply_filters( "itsec_{$context['type']}_lockout_message", $message, $context );
470
+
471
+ /**
472
+ * Filter whether to print the lockout error message with formatting or not.
473
+ *
474
+ * @param bool $formatted
475
+ * @param string $type
476
+ * @param array $context
477
+ */
478
+ $formatted = apply_filters( "itsec_{$context['type']}_lockout_format_message", false, $context );
479
+ }
480
 
481
+ $current_user = wp_get_current_user();
482
 
483
+ if ( is_object( $current_user ) && isset( $current_user->ID ) ) {
484
+ wp_logout();
485
  }
486
 
487
+ if ( $formatted ) {
488
+ wp_die( $message, '', array( 'response' => 403 ) );
489
+ } else {
490
+ @header( 'HTTP/1.0 403 Forbidden' );
491
+ @header( 'Cache-Control: no-cache, must-revalidate, max-age=0' );
492
+ @header( 'Expires: Thu, 22 Jun 1978 00:28:00 GMT' );
493
+ @header( 'Pragma: no-cache' );
494
+
495
+ die( $message );
496
+ }
497
  }
498
 
499
  /**
896
  $this->send_lockout_email( $good_host, $good_user, $good_username, $host_expiration, $user_expiration, $reason );
897
  }
898
 
899
+ $lock_context = array(
900
+ 'type' => $type,
901
+ );
902
+
903
+ if ( $user ) {
904
+ $lock_context['user'] = get_userdata( $user );
905
+ } elseif ( $username ) {
906
+ $lock_context['username'] = $username;
907
+ }
908
+
909
  if ( $good_host !== false ) {
910
 
911
  ITSEC_Lib::release_lock( $lock );
912
+ $this->execute_lock( $lock_context );
913
 
914
  } else {
915
 
916
+ $lock_context['user_lock'] = true;
917
+
918
  ITSEC_Lib::release_lock( $lock );
919
+ $this->execute_lock( $lock_context );
920
 
921
  }
922
 
core/modules/brute-force/class-itsec-brute-force.php CHANGED
@@ -194,10 +194,10 @@ class ITSEC_Brute_Force {
194
  }
195
 
196
  if ( empty( $user_id ) ) {
197
- $itsec_lockout->check_lockout( false, $username );
198
  } else {
199
- $itsec_lockout->check_lockout( $user_id );
200
- };
201
 
202
  $itsec_logger->log_event( 'brute_force', 5, $details, ITSEC_Lib::get_ip(), $username, intval( $user_id ) );
203
  $itsec_lockout->do_lockout( 'brute_force', $username );
194
  }
195
 
196
  if ( empty( $user_id ) ) {
197
+ $itsec_lockout->check_lockout( false, $username, 'brute_force' );
198
  } else {
199
+ $itsec_lockout->check_lockout( $user_id, false, 'brute_force' );
200
+ }
201
 
202
  $itsec_logger->log_event( 'brute_force', 5, $details, ITSEC_Lib::get_ip(), $username, intval( $user_id ) );
203
  $itsec_lockout->do_lockout( 'brute_force', $username );
core/modules/ipcheck/class-itsec-ipcheck.php CHANGED
@@ -234,7 +234,7 @@ class ITSEC_IPCheck {
234
 
235
  if ( $this->settings['enable_ban'] && $this->is_ip_banned() ) {
236
  $itsec_logger->log_event( 'ipcheck', 10, array(), ITSEC_Lib::get_ip() );
237
- $itsec_lockout->execute_lock( false, true );
238
  }
239
  }
240
 
@@ -254,7 +254,7 @@ class ITSEC_IPCheck {
254
 
255
  if ( $this->settings['enable_ban'] && $this->report_ip() ) {
256
  $itsec_logger->log_event( 'ipcheck', 10, array(), ITSEC_Lib::get_ip() );
257
- $itsec_lockout->execute_lock( false, true );
258
  }
259
  }
260
 
234
 
235
  if ( $this->settings['enable_ban'] && $this->is_ip_banned() ) {
236
  $itsec_logger->log_event( 'ipcheck', 10, array(), ITSEC_Lib::get_ip() );
237
+ $itsec_lockout->execute_lock( array( 'network_lock' => true ) );
238
  }
239
  }
240
 
254
 
255
  if ( $this->settings['enable_ban'] && $this->report_ip() ) {
256
  $itsec_logger->log_event( 'ipcheck', 10, array(), ITSEC_Lib::get_ip() );
257
+ $itsec_lockout->execute_lock( array( 'network_lock' => true ) );
258
  }
259
  }
260
 
core/modules/pro/settings-page.php CHANGED
@@ -4,36 +4,36 @@
4
  */
5
 
6
 
7
- final class ITSEC_Malware_Scheduling_Settings_Page extends ITSEC_Module_Settings_Page {
8
  public function __construct() {
9
- $this->id = 'malware-scheduling';
10
- $this->title = __( 'Malware Scan Scheduling', 'better-wp-security' );
11
- $this->description = __( 'Protect your site with automated malware scans. When this feature is enabled, the site will be automatically scanned each day. If a problem is found, an email is sent to select users.', 'better-wp-security' );
12
  $this->type = 'recommended';
13
  $this->pro = true;
14
  $this->upsell = true;
15
- $this->upsell_url = 'http://ithemes.com/security/wordpress-malware-scan/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
16
 
17
  parent::__construct();
18
  }
19
  }
20
- new ITSEC_Malware_Scheduling_Settings_Page();
21
 
22
 
23
- final class ITSEC_Privilege_Escalation_Settings_Page extends ITSEC_Module_Settings_Page {
24
  public function __construct() {
25
- $this->id = 'privilege';
26
- $this->title = __( 'Privilege Escalation', 'better-wp-security' );
27
- $this->description = __( 'Allow administrators to temporarily grant extra access to a user of the site for a specified period of time.', 'better-wp-security' );
28
  $this->type = 'recommended';
29
  $this->pro = true;
30
  $this->upsell = true;
31
- $this->upsell_url = 'https://ithemes.com/security/wordpress-privilege-escalation/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
32
 
33
  parent::__construct();
34
  }
35
  }
36
- new ITSEC_Privilege_Escalation_Settings_Page();
37
 
38
 
39
  final class ITSEC_Password_Expiration_Settings_Page extends ITSEC_Module_Settings_Page {
@@ -52,6 +52,22 @@ final class ITSEC_Password_Expiration_Settings_Page extends ITSEC_Module_Setting
52
  new ITSEC_Password_Expiration_Settings_Page();
53
 
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  final class ITSEC_Recaptcha_Settings_Page extends ITSEC_Module_Settings_Page {
56
  public function __construct() {
57
  $this->id = 'recaptcha';
@@ -103,36 +119,36 @@ final class ITSEC_Two_Factor_Settings_Page extends ITSEC_Module_Settings_Page {
103
  new ITSEC_Two_Factor_Settings_Page();
104
 
105
 
106
- final class ITSEC_User_Logging_Settings_Page extends ITSEC_Module_Settings_Page {
107
  public function __construct() {
108
- $this->id = 'user-logging';
109
- $this->title = __( 'User Logging', 'better-wp-security' );
110
- $this->description = __( 'Log user actions such as login, saving content and others.', 'better-wp-security' );
111
  $this->type = 'recommended';
112
  $this->pro = true;
113
  $this->upsell = true;
114
- $this->upsell_url = 'https://ithemes.com/security/wordpress-user-log/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
115
 
116
  parent::__construct();
117
  }
118
  }
119
- new ITSEC_User_Logging_Settings_Page();
120
 
121
 
122
- final class ITSEC_User_Security_Check_Settings_Page extends ITSEC_Module_Settings_Page {
123
  public function __construct() {
124
- $this->id = 'user-security-check';
125
- $this->title = __( 'User Security Check', 'better-wp-security' );
126
- $this->description = __( 'Every user on your site affects overall security. See how your users might be affecting your security and take action when needed.', 'better-wp-security' );
127
  $this->type = 'recommended';
128
  $this->pro = true;
129
  $this->upsell = true;
130
- $this->upsell_url = 'https://ithemes.com/security/wordpress-user-security-check/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
131
 
132
  parent::__construct();
133
  }
134
  }
135
- new ITSEC_User_Security_Check_Settings_Page();
136
 
137
 
138
  final class ITSEC_Version_Management_Settings_Page extends ITSEC_Module_Settings_Page {
4
  */
5
 
6
 
7
+ final class ITSEC_Magic_Links_Settings_Page extends ITSEC_Module_Settings_Page {
8
  public function __construct() {
9
+ $this->id = 'magic-links';
10
+ $this->title = __( 'Magic Links', 'better-wp-security' );
11
+ $this->description = __( 'Send an email with a Magic Link that bypasses a username lockout.', 'better-wp-security' );
12
  $this->type = 'recommended';
13
  $this->pro = true;
14
  $this->upsell = true;
15
+ $this->upsell_url = 'http://ithemes.com/security/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
16
 
17
  parent::__construct();
18
  }
19
  }
20
+ new ITSEC_Magic_Links_Settings_Page();
21
 
22
 
23
+ final class ITSEC_Malware_Scheduling_Settings_Page extends ITSEC_Module_Settings_Page {
24
  public function __construct() {
25
+ $this->id = 'malware-scheduling';
26
+ $this->title = __( 'Malware Scan Scheduling', 'better-wp-security' );
27
+ $this->description = __( 'Protect your site with automated malware scans. When this feature is enabled, the site will be automatically scanned each day. If a problem is found, an email is sent to select users.', 'better-wp-security' );
28
  $this->type = 'recommended';
29
  $this->pro = true;
30
  $this->upsell = true;
31
+ $this->upsell_url = 'http://ithemes.com/security/wordpress-malware-scan/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
32
 
33
  parent::__construct();
34
  }
35
  }
36
+ new ITSEC_Malware_Scheduling_Settings_Page();
37
 
38
 
39
  final class ITSEC_Password_Expiration_Settings_Page extends ITSEC_Module_Settings_Page {
52
  new ITSEC_Password_Expiration_Settings_Page();
53
 
54
 
55
+ final class ITSEC_Privilege_Escalation_Settings_Page extends ITSEC_Module_Settings_Page {
56
+ public function __construct() {
57
+ $this->id = 'privilege';
58
+ $this->title = __( 'Privilege Escalation', 'better-wp-security' );
59
+ $this->description = __( 'Allow administrators to temporarily grant extra access to a user of the site for a specified period of time.', 'better-wp-security' );
60
+ $this->type = 'recommended';
61
+ $this->pro = true;
62
+ $this->upsell = true;
63
+ $this->upsell_url = 'https://ithemes.com/security/wordpress-privilege-escalation/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
64
+
65
+ parent::__construct();
66
+ }
67
+ }
68
+ new ITSEC_Privilege_Escalation_Settings_Page();
69
+
70
+
71
  final class ITSEC_Recaptcha_Settings_Page extends ITSEC_Module_Settings_Page {
72
  public function __construct() {
73
  $this->id = 'recaptcha';
119
  new ITSEC_Two_Factor_Settings_Page();
120
 
121
 
122
+ final class ITSEC_User_Security_Check_Settings_Page extends ITSEC_Module_Settings_Page {
123
  public function __construct() {
124
+ $this->id = 'user-security-check';
125
+ $this->title = __( 'User Security Check', 'better-wp-security' );
126
+ $this->description = __( 'Every user on your site affects overall security. See how your users might be affecting your security and take action when needed.', 'better-wp-security' );
127
  $this->type = 'recommended';
128
  $this->pro = true;
129
  $this->upsell = true;
130
+ $this->upsell_url = 'https://ithemes.com/security/wordpress-user-security-check/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
131
 
132
  parent::__construct();
133
  }
134
  }
135
+ new ITSEC_User_Security_Check_Settings_Page();
136
 
137
 
138
+ final class ITSEC_User_Logging_Settings_Page extends ITSEC_Module_Settings_Page {
139
  public function __construct() {
140
+ $this->id = 'user-logging';
141
+ $this->title = __( 'User Logging', 'better-wp-security' );
142
+ $this->description = __( 'Log user actions such as login, saving content and others.', 'better-wp-security' );
143
  $this->type = 'recommended';
144
  $this->pro = true;
145
  $this->upsell = true;
146
+ $this->upsell_url = 'https://ithemes.com/security/wordpress-user-log/?utm_source=wordpressadmin&utm_medium=widget&utm_campaign=itsecfreecta';
147
 
148
  parent::__construct();
149
  }
150
  }
151
+ new ITSEC_User_Logging_Settings_Page();
152
 
153
 
154
  final class ITSEC_Version_Management_Settings_Page extends ITSEC_Module_Settings_Page {
core/modules/security-check/scanner.php CHANGED
@@ -12,6 +12,7 @@ final class ITSEC_Security_Check_Scanner {
12
  'ban-users' => __( 'Banned Users', 'better-wp-security' ),
13
  'backup' => __( 'Database Backups', 'better-wp-security' ),
14
  'brute-force' => __( 'Local Brute Force Protection', 'better-wp-security' ),
 
15
  'malware-scheduling' => __( 'Malware Scan Scheduling', 'better-wp-security' ),
16
  'network-brute-force' => __( 'Network Brute Force Protection', 'better-wp-security' ),
17
  'strong-passwords' => __( 'Strong Passwords', 'better-wp-security' ),
@@ -48,6 +49,7 @@ final class ITSEC_Security_Check_Scanner {
48
 
49
  self::enforce_activation( 'backup', __( 'Database Backups', 'better-wp-security' ) );
50
  self::enforce_activation( 'brute-force', __( 'Local Brute Force Protection', 'better-wp-security' ) );
 
51
  self::enforce_activation( 'malware-scheduling', __( 'Malware Scan Scheduling', 'better-wp-security' ) );
52
  self::enforce_setting( 'malware-scheduling', 'email_notifications', true, __( 'Enabled the Email Notifications setting in Malware Scan Scheduling.', 'better-wp-security' ) );
53
 
12
  'ban-users' => __( 'Banned Users', 'better-wp-security' ),
13
  'backup' => __( 'Database Backups', 'better-wp-security' ),
14
  'brute-force' => __( 'Local Brute Force Protection', 'better-wp-security' ),
15
+ 'magic-links' => __( 'Magic Links', 'better-wp-security' ),
16
  'malware-scheduling' => __( 'Malware Scan Scheduling', 'better-wp-security' ),
17
  'network-brute-force' => __( 'Network Brute Force Protection', 'better-wp-security' ),
18
  'strong-passwords' => __( 'Strong Passwords', 'better-wp-security' ),
49
 
50
  self::enforce_activation( 'backup', __( 'Database Backups', 'better-wp-security' ) );
51
  self::enforce_activation( 'brute-force', __( 'Local Brute Force Protection', 'better-wp-security' ) );
52
+ self::enforce_activation( 'magic-links', __( 'Magic Links', 'better-wp-security' ) );
53
  self::enforce_activation( 'malware-scheduling', __( 'Malware Scan Scheduling', 'better-wp-security' ) );
54
  self::enforce_setting( 'malware-scheduling', 'email_notifications', true, __( 'Enabled the Email Notifications setting in Malware Scan Scheduling.', 'better-wp-security' ) );
55
 
core/modules/wordpress-tweaks/class-itsec-wordpress-tweaks.php CHANGED
@@ -70,6 +70,18 @@ final class ITSEC_WordPress_Tweaks {
70
 
71
  $this->settings = ITSEC_Modules::get_settings( 'wordpress-tweaks' );
72
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  // Functional code for the allow_xmlrpc_multiauth setting.
74
  if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST && ! $this->settings['allow_xmlrpc_multiauth'] ) {
75
  add_filter( 'authenticate', array( $this, 'block_multiauth_attempts' ), 0, 3 );
@@ -116,6 +128,48 @@ final class ITSEC_WordPress_Tweaks {
116
  }
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  /**
120
  * Require capabilities for reading from WordPress object routes.
121
  *
70
 
71
  $this->settings = ITSEC_Modules::get_settings( 'wordpress-tweaks' );
72
 
73
+
74
+ // Functional code for the valid_user_login_type setting.
75
+ if ( 'email' === $this->settings['valid_user_login_type'] ) {
76
+ add_action( 'login_init', array( $this, 'add_gettext_filter' ) );
77
+ add_filter( 'authenticate', array( $this, 'add_gettext_filter' ), 0 );
78
+ remove_filter( 'authenticate', 'wp_authenticate_username_password', 20 );
79
+ } else if ( 'username' === $this->settings['valid_user_login_type'] ) {
80
+ add_action( 'login_init', array( $this, 'add_gettext_filter' ) );
81
+ add_filter( 'authenticate', array( $this, 'add_gettext_filter' ), 0 );
82
+ remove_filter( 'authenticate', 'wp_authenticate_email_password', 20 );
83
+ }
84
+
85
  // Functional code for the allow_xmlrpc_multiauth setting.
86
  if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST && ! $this->settings['allow_xmlrpc_multiauth'] ) {
87
  add_filter( 'authenticate', array( $this, 'block_multiauth_attempts' ), 0, 3 );
128
  }
129
  }
130
 
131
+ /**
132
+ * Add filter for gettext to change text for the valid_user_login_type setting changes.
133
+ *
134
+ * @return null
135
+ */
136
+ public function add_gettext_filter() {
137
+ if ( ! has_filter( 'gettext', array( $this, 'filter_gettext' ) ) ) {
138
+ add_filter( 'gettext', array( $this, 'filter_gettext' ), 20, 3 );
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Filter text for the valid_user_login_type setting changes.
144
+ *
145
+ * @param string $translation Translated text.
146
+ * @param string $text Text to translate.
147
+ * @param string $domain Text domain. Unique identifier for retrieving translated strings.
148
+ *
149
+ * @return string
150
+ */
151
+ public function filter_gettext( $translation, $text, $domain ) {
152
+ if ( 'default' !== $domain ) {
153
+ return $translation;
154
+ }
155
+
156
+ if ( 'Username or Email Address' === $text ) {
157
+ if ( 'email' === $this->settings['valid_user_login_type'] ) {
158
+ return esc_html__( 'Email Address', 'better-wp-security' );
159
+ } else if ( 'username' === $this->settings['valid_user_login_type'] ) {
160
+ return esc_html__( 'Username', 'better-wp-security' );
161
+ }
162
+ } else if ( '<strong>ERROR</strong>: Invalid username, email address or incorrect password.' === $text ) {
163
+ if ( 'email' === $this->settings['valid_user_login_type'] ) {
164
+ return __( '<strong>ERROR</strong>: Invalid email address or incorrect password.', 'better-wp-security' );
165
+ } else if ( 'username' === $this->settings['valid_user_login_type'] ) {
166
+ return __( '<strong>ERROR</strong>: Invalid username or incorrect password.', 'better-wp-security' );
167
+ }
168
+ }
169
+
170
+ return $translation;
171
+ }
172
+
173
  /**
174
  * Require capabilities for reading from WordPress object routes.
175
  *
core/modules/wordpress-tweaks/settings-page.php CHANGED
@@ -38,6 +38,12 @@ final class ITSEC_WordPress_Tweaks_Settings_Page extends ITSEC_Module_Settings_P
38
  'default-access' => esc_html__( 'Default Access', 'better-wp-security' ),
39
  );
40
 
 
 
 
 
 
 
41
  ?>
42
  <p><?php esc_html_e( 'Note: These settings are listed as advanced because they block common forms of attacks but they can also block legitimate plugins and themes that rely on the same techniques. When activating the settings below, we recommend enabling them one by one to test that everything on your site is still working as expected.', 'better-wp-security' ); ?></p>
43
  <p><?php esc_html_e( 'Remember, some of these settings might conflict with other plugins or themes, so test your site after enabling each setting.', 'better-wp-security' ); ?></p>
@@ -140,6 +146,18 @@ final class ITSEC_WordPress_Tweaks_Settings_Page extends ITSEC_Module_Settings_P
140
  <p class="description"><?php printf( wp_kses( __( 'Enabling this feature helps protect visitors to this site (including logged in users) from phishing attacks launched by a linked site. Details on tabnapping via target="_blank" links can be found in <a href="%s">this article</a>.', 'better-wp-security' ), array( 'a' => array( 'href' => array() ) ) ), esc_url( 'https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/' ) ); ?></p>
141
  </td>
142
  </tr>
 
 
 
 
 
 
 
 
 
 
 
 
143
  </table>
144
  <?php
145
 
38
  'default-access' => esc_html__( 'Default Access', 'better-wp-security' ),
39
  );
40
 
41
+ $valid_user_login_types = array(
42
+ 'both' => esc_html__( 'Email Address and Username (default)', 'better-wp-security' ),
43
+ 'email' => esc_html__( 'Email Address Only', 'better-wp-security' ),
44
+ 'username' => esc_html__( 'Username Only', 'better-wp-security' ),
45
+ );
46
+
47
  ?>
48
  <p><?php esc_html_e( 'Note: These settings are listed as advanced because they block common forms of attacks but they can also block legitimate plugins and themes that rely on the same techniques. When activating the settings below, we recommend enabling them one by one to test that everything on your site is still working as expected.', 'better-wp-security' ); ?></p>
49
  <p><?php esc_html_e( 'Remember, some of these settings might conflict with other plugins or themes, so test your site after enabling each setting.', 'better-wp-security' ); ?></p>
146
  <p class="description"><?php printf( wp_kses( __( 'Enabling this feature helps protect visitors to this site (including logged in users) from phishing attacks launched by a linked site. Details on tabnapping via target="_blank" links can be found in <a href="%s">this article</a>.', 'better-wp-security' ), array( 'a' => array( 'href' => array() ) ) ), esc_url( 'https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/' ) ); ?></p>
147
  </td>
148
  </tr>
149
+ <tr>
150
+ <th scope="row"><label for="itsec-wordpress-tweaks-valid_user_login_type"><?php esc_html_e( 'Login with Email Address or Username', 'better-wp-security' ); ?></label></th>
151
+ <td>
152
+ <p><?php esc_html_e( 'By default, WordPress allows users to log in using either an email address or username. This setting allows you to restrict logins to only accept email addresses or usernames.', 'better-wp-security' ); ?></p>
153
+ <?php $form->add_select( 'valid_user_login_type', $valid_user_login_types ); ?>
154
+ <ul>
155
+ <li><?php echo wp_kses( __( '<strong>Email Address and Username (Default)</strong> - Allow users to log in using their user\'s email address or username. This is the default WordPress behavior.', 'better-wp-security' ), array( 'strong' => array() ) ); ?></li>
156
+ <li><?php echo wp_kses( __( '<strong>Email Address Only</strong> - Users can only log in using their user\'s email address. This disables logging in using a username.', 'better-wp-security' ), array( 'strong' => array() ) ); ?></li>
157
+ <li><?php echo wp_kses( __( '<strong>Username Only</strong> - Users can only log in using their user\'s username. This disables logging in using an email address.', 'better-wp-security' ), array( 'strong' => array() ) ); ?></li>
158
+ </ul>
159
+ </td>
160
+ </tr>
161
  </table>
162
  <?php
163
 
core/modules/wordpress-tweaks/settings.php CHANGED
@@ -18,6 +18,7 @@ final class ITSEC_Wordpress_Tweaks_Settings extends ITSEC_Settings {
18
  'force_unique_nicename' => false,
19
  'disable_unused_author_pages' => false,
20
  'block_tabnapping' => false,
 
21
  );
22
  }
23
  }
18
  'force_unique_nicename' => false,
19
  'disable_unused_author_pages' => false,
20
  'block_tabnapping' => false,
21
+ 'valid_user_login_type' => 'both',
22
  );
23
  }
24
  }
core/modules/wordpress-tweaks/setup.php CHANGED
@@ -60,27 +60,29 @@ if ( ! class_exists( 'ITSEC_WordPress_Tweaks_Setup' ) ) {
60
  * @return void
61
  */
62
  public function execute_upgrade( $itsec_old_version ) {
 
63
 
64
- if ( $itsec_old_version < 4000 ) {
65
 
 
66
  global $itsec_bwps_options;
67
 
68
  ITSEC_Lib::create_database_tables();
69
 
70
- $current_options = get_site_option( 'itsec_tweaks' );
71
-
72
- // Don't do anything if settings haven't already been set, defaults exist in the module system and we prefer to use those
73
- if ( false !== $current_options ) {
74
- $current_options['wlwmanifest_header'] = isset( $itsec_bwps_options['st_manifest'] ) && $itsec_bwps_options['st_manifest'] == 1 ? true : false;
75
- $current_options['edituri_header'] = isset( $itsec_bwps_options['st_edituri'] ) && $itsec_bwps_options['st_edituri'] == 1 ? true : false;
76
- $current_options['comment_spam'] = isset( $itsec_bwps_options['st_comment'] ) && $itsec_bwps_options['st_comment'] == 1 ? true : false;
77
- $current_options['login_errors'] = isset( $itsec_bwps_options['st_loginerror'] ) && $itsec_bwps_options['st_loginerror'] == 1 ? true : false;
78
-
79
- update_site_option( 'itsec_tweaks', $current_options );
80
- ITSEC_Response::regenerate_server_config();
81
- ITSEC_Response::regenerate_wp_config();
82
  }
83
 
 
 
84
  }
85
 
86
  if ( $itsec_old_version < 4035 ) {
@@ -88,59 +90,31 @@ if ( ! class_exists( 'ITSEC_WordPress_Tweaks_Setup' ) ) {
88
  }
89
 
90
  if ( $itsec_old_version < 4041 ) {
91
- $current_options = get_site_option( 'itsec_tweaks' );
92
-
93
- // If there are no current options, go with the new defaults by not saving anything
94
- if ( is_array( $current_options ) ) {
95
- $new_module_settings = ITSEC_Modules::get_settings( 'wordpress-tweaks' );
96
-
97
- // Reduce to only settings in new module
98
- $current_options = array_intersect_key( $current_options, $new_module_settings );
99
-
100
- // Use new module settings as defaults for any missing settings
101
- $current_options = array_merge( $new_module_settings, $current_options );
102
-
103
- // If anything in this module is being used activate it, otherwise deactivate it
104
- $activate = false;
105
- foreach ( $current_options as $setting => $on ) {
106
- // False is actually "enabled" for blocking xmlrpc multiauth
107
- if ( ( 'allow_xmlrpc_multiauth' !== $setting && $on ) || ( 'allow_xmlrpc_multiauth' === $setting && ! $on ) ) {
108
- $activate = true;
109
- break;
110
- }
111
- }
112
- if ( $activate ) {
113
- ITSEC_Modules::activate( 'wordpress-tweaks' );
114
- } else {
115
- ITSEC_Modules::deactivate( 'wordpress-tweaks' );
116
- }
117
-
118
- ITSEC_Modules::set_settings( 'wordpress-tweaks', $current_options );
119
  }
 
 
 
120
  }
121
 
122
  if ( $itsec_old_version < 4050 ) {
123
- $settings = ITSEC_Modules::get_settings( 'wordpress-tweaks' );
124
-
125
- if ( isset( $settings['rest_api'] ) ) {
126
- if ( 'enable' === $settings['rest_api'] ) {
127
- $settings['rest_api'] = 'default-access';
128
- } else if ( in_array( $settings['rest_api'], array( 'disable', 'require-admin' ) ) ) {
129
- $settings['rest_api'] = 'restrict-access';
130
- }
131
-
132
- ITSEC_Modules::set_settings( 'wordpress-tweaks', $settings );
133
  }
134
  }
135
 
136
  if ( $itsec_old_version < 4073 ) {
137
- $settings = ITSEC_Modules::get_settings( 'wordpress-tweaks' );
138
-
139
  unset( $settings['safe_jquery'] );
140
  unset( $settings['jquery_version'] );
141
-
142
- ITSEC_Modules::set_settings( 'wordpress-tweaks', $settings );
143
  }
 
 
 
144
  }
145
 
146
  }
60
  * @return void
61
  */
62
  public function execute_upgrade( $itsec_old_version ) {
63
+ $settings = ITSEC_Modules::get_settings( 'wordpress-tweaks' );
64
 
 
65
 
66
+ if ( $itsec_old_version < 4000 ) {
67
  global $itsec_bwps_options;
68
 
69
  ITSEC_Lib::create_database_tables();
70
 
71
+ if ( isset( $itsec_bwps_options['st_manifest'] ) && $itsec_bwps_options['st_manifest'] ) {
72
+ $settings['wlwmanifest_header'] = true;
73
+ }
74
+ if ( isset( $itsec_bwps_options['st_edituri'] ) && $itsec_bwps_options['st_edituri'] ) {
75
+ $settings['edituri_header'] = true;
76
+ }
77
+ if ( isset( $itsec_bwps_options['st_comment'] ) && $itsec_bwps_options['st_comment'] ) {
78
+ $settings['comment_spam'] = true;
79
+ }
80
+ if ( isset( $itsec_bwps_options['st_loginerror'] ) && $itsec_bwps_options['st_loginerror'] ) {
81
+ $settings['login_errors'] = true;
 
82
  }
83
 
84
+ ITSEC_Response::regenerate_server_config();
85
+ ITSEC_Response::regenerate_wp_config();
86
  }
87
 
88
  if ( $itsec_old_version < 4035 ) {
90
  }
91
 
92
  if ( $itsec_old_version < 4041 ) {
93
+ $old_settings = get_site_option( 'itsec_tweaks' );
94
+
95
+ if ( is_array( $old_settings ) ) {
96
+ $settings = array_merge( $settings, $old_settings );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  }
98
+ } else {
99
+ // Time to get rid of the cruft.
100
+ delete_site_option( 'itsec_tweaks' );
101
  }
102
 
103
  if ( $itsec_old_version < 4050 ) {
104
+ if ( 'enable' === $settings['rest_api'] ) {
105
+ $settings['rest_api'] = 'default-access';
106
+ } else if ( in_array( $settings['rest_api'], array( 'disable', 'require-admin' ) ) ) {
107
+ $settings['rest_api'] = 'restrict-access';
 
 
 
 
 
 
108
  }
109
  }
110
 
111
  if ( $itsec_old_version < 4073 ) {
 
 
112
  unset( $settings['safe_jquery'] );
113
  unset( $settings['jquery_version'] );
 
 
114
  }
115
+
116
+
117
+ ITSEC_Modules::set_settings( 'wordpress-tweaks', $settings );
118
  }
119
 
120
  }
core/modules/wordpress-tweaks/validator.php CHANGED
@@ -20,6 +20,7 @@ class ITSEC_WordPress_Tweaks_Validator extends ITSEC_Validator {
20
  $this->sanitize_setting( 'bool', 'force_unique_nicename', __( 'Force Unique Nickname', 'better-wp-security' ) );
21
  $this->sanitize_setting( 'bool', 'disable_unused_author_pages', __( 'Disable Extra User Archives', 'better-wp-security' ) );
22
  $this->sanitize_setting( 'bool', 'block_tabnapping', __( 'Protect Against Tabnapping', 'better-wp-security' ) );
 
23
  }
24
 
25
  protected function validate_settings() {
20
  $this->sanitize_setting( 'bool', 'force_unique_nicename', __( 'Force Unique Nickname', 'better-wp-security' ) );
21
  $this->sanitize_setting( 'bool', 'disable_unused_author_pages', __( 'Disable Extra User Archives', 'better-wp-security' ) );
22
  $this->sanitize_setting( 'bool', 'block_tabnapping', __( 'Protect Against Tabnapping', 'better-wp-security' ) );
23
+ $this->sanitize_setting( array( 'both', 'email', 'username' ), 'valid_user_login_type', __( 'Login with Email Address or Username', 'better-wp-security' ) );
24
  }
25
 
26
  protected function validate_settings() {
core/notify.php CHANGED
@@ -49,21 +49,28 @@ class ITSEC_Notify {
49
  return false;
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
52
  if ( ! ITSEC_Lib::get_lock( 'daily-digest' ) ) {
53
  return false;
54
  }
55
 
56
  if ( ! $use_cron ) {
57
-
58
- $global = ITSEC_Modules::get_settings_obj( 'global' );
59
- ITSEC_Storage::reload();
60
- $global->load();
61
-
62
- $last_sent = $global->get('digest_last_sent' );
63
- $yesterday = ITSEC_Core::get_current_time_gmt() - DAY_IN_SECONDS;
64
 
65
  // Send digest if it has been 24 hours
66
  if ( $last_sent > $yesterday ) {
 
67
  return false;
68
  }
69
  }
@@ -75,6 +82,37 @@ class ITSEC_Notify {
75
  return $result;
76
  }
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  /**
79
  * Send the daily digest email.
80
  *
@@ -155,7 +193,7 @@ class ITSEC_Notify {
155
 
156
  ITSEC_Modules::set_setting( 'global', 'digest_last_sent', ITSEC_Core::get_current_time_gmt() );
157
  ITSEC_Modules::set_setting( 'global', 'digest_messages', array() );
158
-
159
 
160
  $subject = esc_html__( 'Daily Security Digest', 'better-wp-security' );
161
  $mail->set_subject( $subject );
49
  return false;
50
  }
51
 
52
+ // Check the cached digest_last_sent value. This will be fast but may be inaccurate.
53
+ if ( ! $use_cron ) {
54
+ $last_sent = ITSEC_Modules::get_setting( 'global', 'digest_last_sent' );
55
+ $yesterday = ITSEC_Core::get_current_time_gmt() - DAY_IN_SECONDS;
56
+
57
+ if ( $last_sent > $yesterday ) {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ // Attempt to acquire a lock so only one process can send the daily digest at a time.
63
  if ( ! ITSEC_Lib::get_lock( 'daily-digest' ) ) {
64
  return false;
65
  }
66
 
67
  if ( ! $use_cron ) {
68
+ // This prevents errors where the last sent value is loaded in memory early in the request, before another process has finished sending the value.
69
+ $last_sent = $this->get_last_sent_uncached();
 
 
 
 
 
70
 
71
  // Send digest if it has been 24 hours
72
  if ( $last_sent > $yesterday ) {
73
+
74
  return false;
75
  }
76
  }
82
  return $result;
83
  }
84
 
85
+ /**
86
+ * Get the time the daily digest was last sent directly from the database.
87
+ *
88
+ * @return int
89
+ */
90
+ private function get_last_sent_uncached() {
91
+
92
+ /** @var $wpdb \wpdb */
93
+ global $wpdb;
94
+
95
+ $option = 'itsec-storage';
96
+ $storage = array();
97
+
98
+ if ( is_multisite() ) {
99
+ $network_id = get_current_site()->id;
100
+ $row = $wpdb->get_row( $wpdb->prepare( "SELECT meta_value FROM $wpdb->sitemeta WHERE meta_key = %s AND site_id = %d", $option, $network_id ) );
101
+
102
+ if ( is_object( $row ) ) {
103
+ $storage = maybe_unserialize( $row->meta_value );
104
+ }
105
+ } else {
106
+ $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );
107
+
108
+ if ( is_object( $row ) ) {
109
+ $storage = maybe_unserialize( $row->option_value );
110
+ }
111
+ }
112
+
113
+ return isset( $storage['global'], $storage['global']['digest_last_sent'] ) ? $storage['global']['digest_last_sent'] : 0;
114
+ }
115
+
116
  /**
117
  * Send the daily digest email.
118
  *
193
 
194
  ITSEC_Modules::set_setting( 'global', 'digest_last_sent', ITSEC_Core::get_current_time_gmt() );
195
  ITSEC_Modules::set_setting( 'global', 'digest_messages', array() );
196
+ ITSEC_Storage::save();
197
 
198
  $subject = esc_html__( 'Daily Security Digest', 'better-wp-security' );
199
  $mail->set_subject( $subject );
history.txt CHANGED
@@ -680,3 +680,11 @@
680
  6.5.1 - 2017-08-23 - Chris Jean & Timothy Jacobs
681
  Bug Fix: Fixed logical error that prevented backups from executing.
682
  Bug Fix: Fixed issue that could cause database locks to flood the database.
 
 
 
 
 
 
 
 
680
  6.5.1 - 2017-08-23 - Chris Jean & Timothy Jacobs
681
  Bug Fix: Fixed logical error that prevented backups from executing.
682
  Bug Fix: Fixed issue that could cause database locks to flood the database.
683
+ 6.6.0 - 2017-09-06 - Chris Jean & Timothy Jacobs
684
+ New Feature: Added a new setting in WordPress Tweaks: "Login with Email Address or Username".
685
+ Enhancement: Host email images from the plugin instead of relying on iThemes servers to help email clients marking messages as spam or blocking images.
686
+ Bug Fix: Error when searching for modules preventing modules from appearing.
687
+ Bug Fix: Use the wp_options table when acquiring locks in Multisite.
688
+ Bug Fix: Prevent duplicate daily digest emails on sites with high load.
689
+ Misc: Added Magic Links, a new Pro-only feature, to be activated by Security Check.
690
+ Misc: Rearranged modules to be listed alphabetically.
readme.txt CHANGED
@@ -3,7 +3,7 @@ Contributors: ithemes, chrisjean, gerroald, mattdanner, timothyblynjacobs
3
  Tags: security, security plugin, malware, hack, secure, block, SSL, admin, htaccess, lockdown, login, protect, protection, anti virus, attack, injection, login security, maintenance, permissions, prevention, authentication, administration, password, brute force, ban, permissions, bots, user agents, xml rpc, security log
4
  Requires at least: 4.6
5
  Tested up to: 4.8.1
6
- Stable tag: 6.5.1
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
@@ -188,6 +188,15 @@ Free support may be available with the help of the community in the <a href="htt
188
 
189
  == Changelog ==
190
 
 
 
 
 
 
 
 
 
 
191
  = 6.5.1 =
192
  * Bug Fix: Fixed logical error that prevented backups from executing.
193
  * Bug Fix: Fixed issue that could cause database locks to flood the database.
@@ -358,5 +367,5 @@ Free support may be available with the help of the community in the <a href="htt
358
 
359
  == Upgrade Notice ==
360
 
361
- = 6.5.1 =
362
- Version 6.5.1 contains important bug fixes. It is recommended for all users.
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.6
5
  Tested up to: 4.8.1
6
+ Stable tag: 6.6.0
7
  License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
 
188
 
189
  == Changelog ==
190
 
191
+ = 6.6.0 =
192
+ * New Feature: Added a new setting in WordPress Tweaks: "Login with Email Address or Username".
193
+ * Enhancement: Host email images from the plugin instead of relying on iThemes servers to help email clients marking messages as spam or blocking images.
194
+ * Bug Fix: Error when searching for modules preventing modules from appearing.
195
+ * Bug Fix: Use the wp_options table when acquiring locks in Multisite.
196
+ * Bug Fix: Prevent duplicate daily digest emails on sites with high load.
197
+ * Misc: Added Magic Links, a new Pro-only feature, to be activated by Security Check.
198
+ * Misc: Rearranged modules to be listed alphabetically.
199
+
200
  = 6.5.1 =
201
  * Bug Fix: Fixed logical error that prevented backups from executing.
202
  * Bug Fix: Fixed issue that could cause database locks to flood the database.
367
 
368
  == Upgrade Notice ==
369
 
370
+ = 6.6.0 =
371
+ Version 6.6.0 contains important bug fixes. It is recommended for all users.