Files
2026-04-28 15:13:50 +02:00

298 lines
6.2 KiB
PHP

<?php
namespace AIOSEO\Plugin\Common\Options;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class that holds all sensitive options for AIOSEO.
*
* This class stores sensitive values (tokens, keys, etc.) separately from the
* regular options to prevent accidental exposure. Unlike other option classes:
* - Values can only be read via the explicit get() method.
* - The all() method is voided to prevent bulk exposure.
* - No magic __get/__set methods are provided.
*
* @since 4.9.6
*/
class SensitiveOptions {
/**
* The option name used for DB storage.
*
* @since 4.9.6
*
* @var string
*/
protected $optionsName = 'aioseo_sensitive_options';
/**
* Whether we need to save on shutdown.
*
* @since 4.9.6
*
* @var bool
*/
protected $shouldSave = false;
/**
* The current values stored in memory.
*
* @since 4.9.6
*
* @var array
*/
protected $values = [];
/**
* The list of allowed keys.
*
* @since 4.9.6
*
* @var array
*/
protected $allowedKeys = [
'connectLicenseKey',
'aiAccessToken',
'semrushAccessToken',
'semrushRefreshToken',
'searchStatisticsProfileKey',
'searchStatisticsProfileToken',
'searchStatisticsTrustToken',
'siteAnalysisConnectToken'
];
/**
* Class constructor.
*
* @since 4.9.6
*/
public function __construct() {
$this->init();
add_action( 'shutdown', [ $this, 'save' ] );
}
/**
* Initializes the options from the database.
*
* @since 4.9.6
*
* @return void
*/
protected function init() {
$dbValues = $this->getFromDb();
// Only load values for allowed keys.
foreach ( $this->allowedKeys as $key ) {
$this->values[ $key ] = isset( $dbValues[ $key ] ) ? $dbValues[ $key ] : '';
}
}
/**
* Reads the raw option values from the database.
*
* Subclasses can override this to change the storage mechanism
* (e.g. use get_site_option for network-wide storage).
*
* @since 4.9.6
*
* @return array The decoded option values.
*/
protected function getFromDb() {
$dbValues = json_decode( (string) get_option( $this->optionsName, '' ), true );
return is_array( $dbValues ) ? $dbValues : [];
}
/**
* Gets a sensitive option value.
*
* This is the only way to read a sensitive value. Using an explicit method
* instead of magic __get prevents unintentional reads.
*
* @since 4.9.6
*
* @param string $key The option key.
* @return string The option value, or empty string if not found.
*/
public function get( $key ) {
if ( ! $this->isAllowedKey( $key ) ) {
return '';
}
return isset( $this->values[ $key ] ) ? $this->values[ $key ] : '';
}
/**
* Checks whether a sensitive option has a non-empty value.
*
* Use this instead of get() when you only need to know whether a value
* exists, to avoid unnecessarily reading the sensitive value into scope.
*
* @since 4.9.6
*
* @param string $key The option key.
* @return bool Whether the option has a value.
*/
public function hasValue( $key ) {
if ( ! $this->isAllowedKey( $key ) ) {
return false;
}
return ! empty( $this->values[ $key ] );
}
/**
* Sets a sensitive option value.
*
* @since 4.9.6
*
* @param string $key The option key.
* @param string $value The value to store.
* @return void
*/
public function set( $key, $value ) {
if ( ! $this->isAllowedKey( $key ) ) {
return;
}
$this->values[ $key ] = sanitize_text_field( (string) $value );
$this->shouldSave = true;
}
/**
* Deletes a sensitive option value.
*
* @since 4.9.6
*
* @param string $key The option key.
* @return void
*/
public function delete( $key ) {
$this->set( $key, '' );
}
/**
* Overridden to return an empty array.
*
* This prevents bulk exposure of all sensitive values.
*
* @since 4.9.6
*
* @return array Always returns an empty array.
*/
public function all() {
return [];
}
/**
* Returns an array of boolean indicators for each sensitive option.
*
* This is used to provide the frontend with information about which
* sensitive options have values, without exposing the values themselves.
*
* @since 4.9.6
*
* @return array An associative array of boolean indicators.
*/
public function allHas() {
$result = [];
foreach ( $this->allowedKeys as $key ) {
$result[ 'has' . ucfirst( $key ) ] = $this->hasValue( $key );
}
return $result;
}
/**
* Saves the options to the database.
*
* @since 4.9.6
*
* @param bool $force Whether to force saving.
* @return void
*/
public function save( $force = false ) {
if ( ! $this->shouldSave && ! $force ) {
return;
}
// Only save allowed keys.
$toSave = [];
foreach ( $this->allowedKeys as $key ) {
if ( ! empty( $this->values[ $key ] ) ) {
$toSave[ $key ] = $this->values[ $key ];
}
}
$this->saveToDb( $toSave );
$this->shouldSave = false;
}
/**
* Writes the option values to the database.
*
* Subclasses can override this to change the storage mechanism
* (e.g. use update_site_option for network-wide storage).
*
* @since 4.9.6
*
* @param array $values The values to save.
* @return void
*/
protected function saveToDb( $values ) {
update_option( $this->optionsName, wp_json_encode( $values ), false );
}
/**
* Checks whether a key is in the allowed keys list.
*
* @since 4.9.6
*
* @param string $key The key to check.
* @return bool Whether the key is allowed.
*/
protected function isAllowedKey( $key ) {
return in_array( $key, $this->allowedKeys, true );
}
/**
* Returns the list of allowed keys.
*
* @since 4.9.6
*
* @return array The allowed keys.
*/
public function getAllowedKeys() {
return $this->allowedKeys;
}
/**
* Registers an additional sensitive key.
*
* This allows addons to register their own sensitive keys.
*
* @since 4.9.6
*
* @param string $key The key to register.
* @return void
*/
public function registerKey( $key ) {
if ( in_array( $key, $this->allowedKeys, true ) ) {
return;
}
$this->allowedKeys[] = $key;
// Load the value from DB if it exists.
$dbValues = $this->getFromDb();
if ( isset( $dbValues[ $key ] ) ) {
$this->values[ $key ] = $dbValues[ $key ];
} else {
$this->values[ $key ] = '';
}
}
}