: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
class wfCentralAPIRequest {
* @param string $endpoint
* @param string|null $token
public function __construct($endpoint, $method = 'GET', $token = null, $body = array(), $args = array()) {
$this->endpoint = $endpoint;
* Handles an internal error when making a Central API request (e.g., a second sodium_compat library with an
* incompatible interface loading instead or in addition to ours).
* @param Exception|Throwable $e
public static function handleInternalCentralAPIError($e) {
error_log('Wordfence encountered an internal Central API error: ' . $e->getMessage());
error_log('Wordfence stack trace: ' . $e->getTraceAsString());
public function execute() {
$args = wp_parse_args($this->getArgs(), $args);
$args['method'] = $this->getMethod();
if (empty($args['headers'])) {
$args['headers'] = array();
$token = $this->getToken();
$args['headers']['Authorization'] = 'Bearer ' . $token;
$args['headers']['Content-Type'] = 'application/json';
$args['body'] = json_encode($this->getBody());
$http = _wp_http_get_object();
$response = $http->request(WORDFENCE_CENTRAL_API_URL_SEC . $this->getEndpoint(), $args);
if (!is_wp_error($response)) {
$body = wp_remote_retrieve_body($response);
$statusCode = wp_remote_retrieve_response_code($response);
// Check if site has been disconnected on Central's end, but the plugin is still trying to connect.
if ($statusCode === 404 && strpos($body, 'Site has been disconnected') !== false) {
// Increment attempt count.
$centralDisconnectCount = get_site_transient('wordfenceCentralDisconnectCount');
set_site_transient('wordfenceCentralDisconnectCount', ++$centralDisconnectCount, 86400);
// Once threshold is hit, disconnect Central.
if ($centralDisconnectCount > 3) {
wfRESTConfigController::disconnectConfig();
return new wfCentralAPIResponse($response);
public function getEndpoint() {
* @param string $endpoint
public function setEndpoint($endpoint) {
$this->endpoint = $endpoint;
public function getMethod() {
public function setMethod($method) {
public function getToken() {
public function setToken($token) {
public function getBody() {
public function setBody($body) {
public function getArgs() {
public function setArgs($args) {
class wfCentralAPIResponse {
public static function parseErrorJSON($json) {
$data = json_decode($json, true);
if (is_array($data) && array_key_exists('message', $data)) {
public function __construct($response = null) {
$this->response = $response;
public function getStatusCode() {
return wp_remote_retrieve_response_code($this->getResponse());
public function getBody() {
return wp_remote_retrieve_body($this->getResponse());
public function getJSONBody() {
return json_decode($this->getBody(), true);
public function isError() {
if (is_wp_error($this->getResponse())) {
$statusCode = $this->getStatusCode();
return !($statusCode >= 200 && $statusCode < 300);
public function returnErrorArray() {
/* translators: 1. HTTP status code. 2. Error message. */
__('HTTP %1$d received from Wordfence Central: %2$s', 'wordfence'),
$this->getStatusCode(), $this->parseErrorJSON($this->getBody())),
public function getResponse() {
* @param array|null $response
public function setResponse($response) {
$this->response = $response;
class wfCentralAuthenticatedAPIRequest extends wfCentralAPIRequest {
* @param string $endpoint
public function __construct($endpoint, $method = 'GET', $body = array(), $args = array()) {
parent::__construct($endpoint, $method, null, $body, $args);
* @throws wfCentralAPIException
public function getToken() {
$token = parent::getToken();
$token = get_transient('wordfenceCentralJWT' . wfConfig::get('wordfenceCentralSiteID'));
for ($i = 0; $i < $this->retries; $i++) {
$token = $this->fetchToken();
} catch (wfCentralConfigurationException $e) {
wfConfig::set('wordfenceCentralConfigurationIssue', true);
throw new wfCentralAPIException(__('Fetching token for Wordfence Central authentication due to configuration issue.', 'wordfence'));
} catch (wfCentralAPIException $e) {
throw new wfCentralAPIException(__('Unable to authenticate with Wordfence Central.', 'wordfence'));
$tokenContents = wfJWT::extractTokenContents($token);
if (!empty($tokenContents['body']['exp'])) {
set_transient('wordfenceCentralJWT' . wfConfig::get('wordfenceCentralSiteID'), $token, $tokenContents['body']['exp'] - time());
wfConfig::set('wordfenceCentralConfigurationIssue', false);
public function fetchToken() {
require_once(WORDFENCE_PATH . '/lib/sodium_compat_fast.php');
$siteID = wfConfig::get('wordfenceCentralSiteID');
throw new wfCentralAPIException(__('Wordfence Central site ID has not been created yet.', 'wordfence'));
$secretKey = wfConfig::get('wordfenceCentralSecretKey');
throw new wfCentralAPIException(__('Wordfence Central secret key has not been created yet.', 'wordfence'));
$request = new wfCentralAPIRequest(sprintf('/site/%s/login', $siteID), 'GET', null, array(), $defaultArgs);
$nonceResponse = $request->execute();
if ($nonceResponse->isError()) {
$errorArray = $nonceResponse->returnErrorArray();
throw new wfCentralAPIException($errorArray['errorMsg']);
$body = $nonceResponse->getJSONBody();
if (!is_array($body) || !isset($body['nonce'])) {
throw new wfCentralAPIException(__('Invalid response received from Wordfence Central when fetching nonce.', 'wordfence'));
// Sign nonce to pull down JWT.
$data = $nonce . '|' . $siteID;
$signature = ParagonIE_Sodium_Compat::crypto_sign_detached($data, $secretKey);
catch (SodiumException $e) {
throw new wfCentralConfigurationException('Signing failed, likely due to malformed secret key', $e);
$request = new wfCentralAPIRequest(sprintf('/site/%s/login', $siteID), 'POST', null, array(
'signature' => ParagonIE_Sodium_Compat::bin2hex($signature),
$authResponse = $request->execute();
if ($authResponse->isError()) {
$errorArray = $authResponse->returnErrorArray();
throw new wfCentralAPIException($errorArray['errorMsg']);
$body = $authResponse->getJSONBody();
throw new wfCentralAPIException(__('Invalid response received from Wordfence Central when fetching token.', 'wordfence'));
if (!isset($body['jwt'])) { // Possible authentication error.
throw new wfCentralAPIException(__('Unable to authenticate with Wordfence Central.', 'wordfence'));
class wfCentralAPIException extends Exception {
class wfCentralConfigurationException extends RuntimeException {
public function __construct($message, $previous = null) {
parent::__construct($message, 0, $previous);
public static function isSupported() {
return function_exists('register_rest_route') && version_compare(phpversion(), '5.3', '>=');
public static function isConnected() {
return self::isSupported() && ((bool) self::_isConnected());
public static function isPartialConnection() {
return !self::_isConnected() && wfConfig::get('wordfenceCentralSiteID');
public static function _isConnected($forceUpdate = false) {
if (!isset($isConnected) || $forceUpdate) {
$isConnected = wfConfig::get('wordfenceCentralConnected', false);
* @return bool|wfCentralAPIResponse
public static function sendIssue($issue) {
return self::sendIssues(array($issue));
* @return bool|wfCentralAPIResponse
public static function sendIssues($issues) {
foreach ($issues as $issue) {
if (array_key_exists('id', $issueData)) {
$issueData['id'] = $issue['id'];
$siteID = wfConfig::get('wordfenceCentralSiteID');
$request = new wfCentralAuthenticatedAPIRequest('/site/' . $siteID . '/issues', 'POST', array(
$response = $request->execute();
catch (wfCentralAPIException $e) {
wfCentralAPIRequest::handleInternalCentralAPIError($e);
wfCentralAPIRequest::handleInternalCentralAPIError($t);
* @return bool|wfCentralAPIResponse
public static function deleteIssue($issueID) {
return self::deleteIssues(array($issueID));
* @return bool|wfCentralAPIResponse
public static function deleteIssues($issues) {
$siteID = wfConfig::get('wordfenceCentralSiteID');
$request = new wfCentralAuthenticatedAPIRequest('/site/' . $siteID . '/issues', 'DELETE', array(
$response = $request->execute();
catch (wfCentralAPIException $e) {
wfCentralAPIRequest::handleInternalCentralAPIError($e);
wfCentralAPIRequest::handleInternalCentralAPIError($t);
* @return bool|wfCentralAPIResponse
public static function deleteNewIssues() {
$siteID = wfConfig::get('wordfenceCentralSiteID');
$request = new wfCentralAuthenticatedAPIRequest('/site/' . $siteID . '/issues', 'DELETE', array(
$response = $request->execute();
catch (wfCentralAPIException $e) {
wfCentralAPIRequest::handleInternalCentralAPIError($e);
wfCentralAPIRequest::handleInternalCentralAPIError($t);