: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
use WordfenceLS\Settings\Model_DB;
use WordfenceLS\Settings\Model_WPOptions;
class Controller_Settings {
const OPTION_XMLRPC_ENABLED = 'xmlrpc-enabled';
const OPTION_2FA_WHITELISTED = 'whitelisted';
const OPTION_IP_SOURCE = 'ip-source';
const OPTION_IP_TRUSTED_PROXIES = 'ip-trusted-proxies';
const OPTION_REQUIRE_2FA_ADMIN = 'require-2fa.administrator';
const OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED = 'require-2fa-grace-period-enabled';
const OPTION_REQUIRE_2FA_GRACE_PERIOD = 'require-2fa-grace-period';
const OPTION_REQUIRE_2FA_USER_GRACE_PERIOD = '2fa-user-grace-period';
const OPTION_REMEMBER_DEVICE_ENABLED = 'remember-device';
const OPTION_REMEMBER_DEVICE_DURATION = 'remember-device-duration';
const OPTION_ALLOW_XML_RPC = 'allow-xml-rpc';
const OPTION_ENABLE_AUTH_CAPTCHA = 'enable-auth-captcha';
const OPTION_CAPTCHA_TEST_MODE = 'recaptcha-test-mode';
const OPTION_RECAPTCHA_SITE_KEY = 'recaptcha-site-key';
const OPTION_RECAPTCHA_SECRET = 'recaptcha-secret';
const OPTION_RECAPTCHA_THRESHOLD = 'recaptcha-threshold';
const OPTION_DELETE_ON_DEACTIVATION = 'delete-deactivation';
const OPTION_PREFIX_REQUIRED_2FA_ROLE = 'required-2fa-role';
const OPTION_ENABLE_WOOCOMMERCE_INTEGRATION = 'enable-woocommerce-integration';
const OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION = 'enable-woocommerce-account-integration';
const OPTION_ENABLE_SHORTCODE = 'enable-shortcode';
const OPTION_ENABLE_LOGIN_HISTORY_COLUMNS = 'enable-login-history-columns';
const OPTION_STACK_UI_COLUMNS = 'stack-ui-columns';
const OPTION_GLOBAL_NOTICES = 'global-notices';
const OPTION_LAST_SECRET_REFRESH = 'last-secret-refresh';
const OPTION_USE_NTP = 'use-ntp';
const OPTION_ALLOW_DISABLING_NTP = 'allow-disabling-ntp';
const OPTION_NTP_FAILURE_COUNT = 'ntp-failure-count';
const OPTION_NTP_OFFSET = 'ntp-offset';
const OPTION_SHARED_HASH_SECRET_KEY = 'shared-hash-secret';
const OPTION_SHARED_SYMMETRIC_SECRET_KEY = 'shared-symmetric-secret';
const OPTION_DISMISSED_FRESH_INSTALL_MODAL = 'dismissed-fresh-install-modal';
const OPTION_CAPTCHA_STATS = 'captcha-stats';
const OPTION_SCHEMA_VERSION = 'schema-version';
const OPTION_USER_COUNT_QUERY_STATE = 'user-count-query-state';
const OPTION_DISABLE_TEMPORARY_TABLES = 'disable-temporary-tables';
const DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD = 10;
const MAX_REQUIRE_2FA_USER_GRACE_PERIOD = 99;
const STATE_2FA_DISABLED = 'disabled';
const STATE_2FA_OPTIONAL = 'optional';
const STATE_2FA_REQUIRED = 'required';
protected $_settingsStorage;
* Returns the singleton Controller_Settings.
* @return Controller_Settings
public static function shared() {
$_shared = new Controller_Settings();
public function __construct($settingsStorage = false) {
$settingsStorage = new Model_DB();
$this->_settingsStorage = $settingsStorage;
$this->_migrate_admin_2fa_requirements_to_roles();
public function set_defaults() {
$this->_settingsStorage->set_multiple(array(
self::OPTION_XMLRPC_ENABLED => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_2FA_WHITELISTED => array('value' => '', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_IP_SOURCE => array('value' => Model_Request::IP_SOURCE_AUTOMATIC, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_IP_TRUSTED_PROXIES => array('value' => '', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REQUIRE_2FA_ADMIN => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD => array('value' => self::DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_GLOBAL_NOTICES => array('value' => '[]', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REMEMBER_DEVICE_ENABLED => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REMEMBER_DEVICE_DURATION => array('value' => (30 * 86400), 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ALLOW_XML_RPC => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_AUTH_CAPTCHA => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_CAPTCHA_STATS => array('value' => '{"counts":[0,0,0,0,0,0,0,0,0,0,0],"avg":0}', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_RECAPTCHA_THRESHOLD => array('value' => 0.5, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_LAST_SECRET_REFRESH => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_DELETE_ON_DEACTIVATION => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_SHORTCODE => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_STACK_UI_COLUMNS => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_SCHEMA_VERSION => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_USER_COUNT_QUERY_STATE => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_DISABLE_TEMPORARY_TABLES => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false)
public function set($key, $value, $already_validated = false) {
return $this->set_multiple(array($key => $value), $already_validated);
public function set_array($key, $value, $already_validated = false) {
return $this->set_multiple(array($key => json_encode($value)), $already_validated);
public function set_multiple($changes, $already_validated = false) {
if (!$already_validated && $this->validate_multiple($changes) !== true) {
$changes = $this->clean_multiple($changes);
$changes = $this->preprocess_multiple($changes);
$this->_settingsStorage->set_multiple($changes);
public function get($key, $default = false) {
return $this->_settingsStorage->get($key, $default);
public function get_bool($key, $default = false) {
return $this->_truthy_to_bool($this->get($key, $default));
public function get_int($key, $default = 0) {
return intval($this->get($key, $default));
public function get_float($key, $default = 0.0) {
return (float) $this->get($key, $default);
public function get_array($key, $default = array()) {
$value = $this->get($key, null);
$value = @json_decode($value, true);
return is_array($value) ? $value : $default;
public function remove($key) {
$this->_settingsStorage->remove($key);
* Validates whether a user-entered setting value is acceptable. Returns true if valid or an error message if not.
public function validate($key, $value) {
case self::OPTION_XMLRPC_ENABLED:
case self::OPTION_REQUIRE_2FA_ADMIN:
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED:
case self::OPTION_REMEMBER_DEVICE_ENABLED:
case self::OPTION_ALLOW_XML_RPC:
case self::OPTION_ENABLE_AUTH_CAPTCHA:
case self::OPTION_CAPTCHA_TEST_MODE:
case self::OPTION_DISMISSED_FRESH_INSTALL_MODAL:
case self::OPTION_DELETE_ON_DEACTIVATION:
case self::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION:
case self::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION:
case self::OPTION_ENABLE_SHORTCODE:
case self::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS:
case self::OPTION_STACK_UI_COLUMNS:
case self::OPTION_USER_COUNT_QUERY_STATE:
case self::OPTION_DISABLE_TEMPORARY_TABLES:
case self::OPTION_LAST_SECRET_REFRESH:
return is_numeric($value); //Left using is_numeric to prevent issues with existing values
case self::OPTION_SCHEMA_VERSION:
return Utility_Number::isInteger($value, 0);
case self::OPTION_GLOBAL_NOTICES:
case self::OPTION_CAPTCHA_STATS:
return preg_match('/^\[.*\]$/', $value) || preg_match('/^\{.*\}$/', $value); //Only a rough JSON validation
case self::OPTION_IP_TRUSTED_PROXIES:
case self::OPTION_2FA_WHITELISTED:
$parsed = array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $value)));
foreach ($parsed as $entry) {
if (!Controller_Whitelist::shared()->is_valid_range($entry)) {
return sprintf(__('The IP/range %s is invalid.', 'wordfence'), esc_html($entry));
case self::OPTION_IP_SOURCE:
if (!in_array($value, array(Model_Request::IP_SOURCE_AUTOMATIC, Model_Request::IP_SOURCE_REMOTE_ADDR, Model_Request::IP_SOURCE_X_FORWARDED_FOR, Model_Request::IP_SOURCE_X_REAL_IP))) {
return __('An invalid IP source was provided.', 'wordfence');
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD:
$gracePeriodEnd = strtotime($value);
if ($gracePeriodEnd <= \WordfenceLS\Controller_Time::time()) {
return __('The grace period end time must be in the future.', 'wordfence');
case self::OPTION_REMEMBER_DEVICE_DURATION:
return is_numeric($value) && $value > 0;
case self::OPTION_RECAPTCHA_THRESHOLD:
return is_numeric($value) && $value > 0 && $value <= 1;
case self::OPTION_RECAPTCHA_SITE_KEY:
$response = wp_remote_get('https://www.google.com/recaptcha/api.js?render=' . urlencode($value));
if (!is_wp_error($response)) {
$status = wp_remote_retrieve_response_code($response);
$data = wp_remote_retrieve_body($response);
if (strpos($data, 'grecaptcha') === false) {
return __('Unable to validate the reCAPTCHA site key. Please check the key and try again.', 'wordfence');
return sprintf(__('An error was encountered while validating the reCAPTCHA site key: %s', 'wordfence'), $response->get_error_message());
case self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD:
return is_numeric($value) && $value >= 0 && $value <= self::MAX_REQUIRE_2FA_USER_GRACE_PERIOD;
public function validate_multiple($values) {
foreach ($values as $key => $value) {
$status = $this->validate($key, $value);
* Cleans and normalizes a setting value for use in saving.
public function clean($key, $value) {
case self::OPTION_XMLRPC_ENABLED:
case self::OPTION_REQUIRE_2FA_ADMIN:
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED:
case self::OPTION_REMEMBER_DEVICE_ENABLED:
case self::OPTION_ALLOW_XML_RPC:
case self::OPTION_ENABLE_AUTH_CAPTCHA:
case self::OPTION_CAPTCHA_TEST_MODE:
case self::OPTION_DISMISSED_FRESH_INSTALL_MODAL:
case self::OPTION_DELETE_ON_DEACTIVATION:
case self::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION:
case self::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION:
case self::OPTION_ENABLE_SHORTCODE;
case self::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS:
case self::OPTION_STACK_UI_COLUMNS:
case self::OPTION_USER_COUNT_QUERY_STATE:
case self::OPTION_DISABLE_TEMPORARY_TABLES:
return $this->_truthy_to_bool($value);
case self::OPTION_REMEMBER_DEVICE_DURATION:
case self::OPTION_LAST_SECRET_REFRESH:
case self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD:
case self::OPTION_SCHEMA_VERSION:
case self::OPTION_RECAPTCHA_THRESHOLD:
case self::OPTION_IP_TRUSTED_PROXIES:
case self::OPTION_2FA_WHITELISTED:
$parsed = array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $value)));
foreach ($parsed as $item) {
$cleaned[] = $this->_sanitize_ip_range($item);
return implode("\n", $cleaned);
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD:
$dt = $this->_parse_local_time($value);
case self::OPTION_RECAPTCHA_SITE_KEY:
case self::OPTION_RECAPTCHA_SECRET:
public function clean_multiple($changes) {
foreach ($changes as $key => $value) {
$cleaned[$key] = $this->clean($key, $value);
private function get_required_2fa_role_key($role) {
return implode('.', array(self::OPTION_PREFIX_REQUIRED_2FA_ROLE, $role));
public function get_required_2fa_role_activation_time($role) {
$time = $this->get_int($this->get_required_2fa_role_key($role), -1);
public function get_user_2fa_grace_period() {
return $this->get_int(self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD, self::DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD);
* Preprocesses the value, returning true if it was saved here (e.g., saved 2fa enabled by assigning a role
* capability) or false if it is to be saved by the backing storage.
* @param array &$settings the array of settings to process, this function may append additional values from preprocessing
public function preprocess($key, $value, &$settings) {
if (preg_match('/^enabled-roles\.(.+)$/', $key, $matches)) { //Enabled roles are stored as capabilities rather than in the settings storage
if ($role === 'super-admin') {
elseif (in_array($value, array(self::STATE_2FA_OPTIONAL, self::STATE_2FA_REQUIRED))) {
$roleValid = Controller_Permissions::shared()->allow_2fa_self($role);
$roleValid = Controller_Permissions::shared()->disallow_2fa_self($role);
$settings[$this->get_required_2fa_role_key($role)] = ($value === self::STATE_2FA_REQUIRED ? time() : -1);
public function preprocess_multiple($changes) {
foreach ($changes as $key => $value) {
if (!$this->preprocess($key, $value, $remaining)) {
$remaining[$key] = $value;
* Returns a cleaned array containing the whitelist entries.
public function whitelisted_ips() {
return array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $this->get(self::OPTION_2FA_WHITELISTED, ''))));
* Returns a cleaned array containing the trusted proxy entries.
public function trusted_proxies() {
return array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $this->get(self::OPTION_IP_TRUSTED_PROXIES, ''))));
public function get_ntp_failure_count() {
return $this->get_int(self::OPTION_NTP_FAILURE_COUNT, 0);
public function reset_ntp_failure_count() {
$this->set(self::OPTION_NTP_FAILURE_COUNT, 0);
public function increment_ntp_failure_count() {
$count = $this->get_ntp_failure_count();
$this->set(self::OPTION_NTP_FAILURE_COUNT, $count);
public function is_ntp_disabled_via_constant() {
return defined('WORDFENCE_LS_DISABLE_NTP') && WORDFENCE_LS_DISABLE_NTP;
public function is_ntp_enabled($requireOffset = true) {
if ($this->is_ntp_cron_disabled())
if ($this->get_bool(self::OPTION_USE_NTP, true)) {
$offset = $this->get(self::OPTION_NTP_OFFSET, null);
return $offset !== null && abs((int)$offset) <= Controller_TOTP::TIME_WINDOW_LENGTH;
public function is_ntp_cron_disabled(&$failureCount = null) {
if ($this->is_ntp_disabled_via_constant())
$failureCount = $this->get_ntp_failure_count();
if ($failureCount >= Controller_Time::FAILURE_LIMIT) {
else if ($failureCount < 0) {
public function disable_ntp_cron() {
$this->set(self::OPTION_NTP_FAILURE_COUNT, -1);
public function are_login_history_columns_enabled() {
return Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS, true);
public function should_stack_ui_columns() {
return self::shared()->get_bool(Controller_Settings::OPTION_STACK_UI_COLUMNS, true);
* Translates a value to a boolean, correctly interpreting various textual representations.
protected function _truthy_to_bool($value) {
if ($value === true || $value === false) {
if (is_numeric($value)) {
if (preg_match('/^(?:f(?:alse)?|no?|off)$/i', $value)) {
else if (preg_match('/^(?:t(?:rue)?|y(?:es)?|on)$/i', $value)) {
* Parses the given time string and returns its DateTime with the server's configured time zone.
* @param string $timestring