0, 'test_recipient' => '', 'spam_retention_days' => 30, 'mail_from' => '', 'mail_from_name' => '', // Spam. 'spam_provider' => 'basic', 'max_links' => 3, 'rate_limit' => 5, // Privacy. 'retention_days' => 365, 'delete_data' => 0, ); } /** * Settings as stored, with defaults applied. Constants are not merged here; * use value() and from_address() for resolved values. * * @return array */ public static function get() { $stored = get_option( self::OPTION, array() ); return wp_parse_args( is_array( $stored ) ? $stored : array(), self::defaults() ); } /** * Days that spam and held-for-review submissions are kept: 1 to 3650. * Held messages must go eventually, so an empty field keeps the current * period instead of becoming the shortest one. * * @param mixed $value Submitted value. * @param int|null $existing Currently stored period. * @return int */ public static function spam_retention_days( $value, $existing = null ) { $value = is_scalar( $value ) ? trim( (string) $value ) : ''; if ( '' === $value ) { $existing = (int) ( $existing ?? 0 ); return $existing > 0 ? min( 3650, $existing ) : (int) self::defaults()['spam_retention_days']; } return min( 3650, max( 1, (int) $value ) ); } /** * Persist settings. * * @param array $settings Sanitized settings. */ public static function update( array $settings ) { return Persistence::option( self::OPTION, $settings ); } /** * Constant overrides, keyed by setting name. * * @return array Setting name => constant name. */ public static function constant_map() { $suffixes = array( 'mail_from' => 'MAIL_FROM', 'mail_from_name' => 'MAIL_FROM_NAME', ); $map = array(); foreach ( $suffixes as $key => $suffix ) { $name = 'ACCESSIBLE_FORMS_' . $suffix; // Sites configured before version 0.5 used shorter names; keep honoring them. $legacy = 'AF_' . $suffix; $map[ $key ] = ! defined( $name ) && defined( $legacy ) ? $legacy : $name; } return $map; } /** * Whether a given setting is locked by a wp-config.php constant. * * @param string $key Setting name. * @return bool */ public static function is_constant( $key ) { $map = self::constant_map(); return isset( $map[ $key ] ) && defined( $map[ $key ] ); } /** * Resolve a single value, preferring a constant when one is defined. * * @param string $key Setting name. * @return mixed */ public static function value( $key ) { $map = self::constant_map(); if ( isset( $map[ $key ] ) && defined( $map[ $key ] ) ) { return constant( $map[ $key ] ); } $settings = self::get(); return $settings[ $key ] ?? null; } /** * The From address and name used for notifications. * * An empty address means "leave WordPress alone", which is the safe default: * wp_mail() will use its own wordpress@yourdomain sender. * * @return array{address: string, name: string} */ public static function from_address() { $address = sanitize_email( (string) self::value( 'mail_from' ) ); $name = sanitize_text_field( (string) self::value( 'mail_from_name' ) ); return array( 'address' => is_email( $address ) ? $address : '', 'name' => $name, ); } /** Resolve an encrypted optional-service credential, including legacy values. */ public static function secret( $key ) { $settings = self::get(); return self::decrypt( (string) ( $settings[ $key ] ?? '' ) ); } /** * Encrypt a secret for storage. * * Protects the password from anyone who obtains a database dump without the * filesystem. It is not protection against someone who already has both, and * the settings screen says so plainly. * * @param string $plain Plain text. * @return string Storable string. */ public static function encrypt( $plain ) { if ( '' === $plain ) { return ''; } if ( ! function_exists( 'openssl_encrypt' ) || '' === self::cipher_key() ) { return ''; } $key = self::cipher_key(); $iv = random_bytes( 12 ); $tag = ''; $cipher = openssl_encrypt( $plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag ); if ( false === $cipher ) { return ''; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return 'enc:v2:' . base64_encode( $iv . $tag . $cipher ); } /** * Decrypt a stored secret. * * @param string $stored Stored string. * @return string */ public static function decrypt( $stored ) { if ( '' === $stored ) { return ''; } if ( 0 === strpos( $stored, 'plain:' ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode return (string) base64_decode( substr( $stored, 6 ) ); } if ( 0 !== strpos( $stored, 'enc:' ) ) { // Legacy unencrypted values are migrated on the next settings save. return $stored; } if ( ! function_exists( 'openssl_decrypt' ) || '' === self::cipher_key() ) { return ''; } if ( 0 === strpos( $stored, 'enc:v2:' ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decode authenticated ciphertext. $raw = base64_decode( substr( $stored, 7 ), true ); if ( false === $raw || strlen( $raw ) <= 28 ) { return ''; } $plain = openssl_decrypt( substr( $raw, 28 ), 'aes-256-gcm', self::cipher_key(), OPENSSL_RAW_DATA, substr( $raw, 0, 12 ), substr( $raw, 12, 16 ) ); return false === $plain ? '' : $plain; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $raw = base64_decode( substr( $stored, 4 ), true ); if ( false === $raw || strlen( $raw ) <= 16 ) { return ''; } $iv = substr( $raw, 0, 16 ); $plain = openssl_decrypt( substr( $raw, 16 ), 'aes-256-cbc', self::cipher_key(), OPENSSL_RAW_DATA, $iv ); return false === $plain ? '' : $plain; } /** * Key material derived from the site's own salts. * * @return string */ private static function cipher_key() { $salt = ( defined( 'AUTH_KEY' ) ? AUTH_KEY : '' ) . ( defined( 'AUTH_SALT' ) ? AUTH_SALT : '' ); if ( '' === $salt ) { return ''; } return hash( 'sha256', $salt, true ); } }