the constant that overrides it. */ const CONSTANTS = array( 'host' => 'ADMINKEEP_SMTP_HOST', 'port' => 'ADMINKEEP_SMTP_PORT', 'encryption' => 'ADMINKEEP_SMTP_ENCRYPTION', 'username' => 'ADMINKEEP_SMTP_USER', 'password' => 'ADMINKEEP_SMTP_PASS', 'from_email' => 'ADMINKEEP_SMTP_FROM', 'from_name' => 'ADMINKEEP_SMTP_FROM_NAME', ); /** * Stored settings, already coerced. * * @var array */ private $stored; /** * Values of the constants that are defined, keyed by field. * * @var array */ private $constants; /** * Opens the stored password. * * @var SecretBox */ private $box; /** * Defaults for every stored field. * * @return array */ public static function defaults() { return array( 'host' => '', 'port' => 587, 'encryption' => 'tls', 'auth' => true, 'username' => '', 'password' => '', 'from_email' => '', 'from_name' => '', 'force_from' => false, 'force_name' => false, ); } /** * Build from explicit sources. * * @param array $stored The option's value. * @param array $constants Field key => value, for the constants that are defined. * @param SecretBox $box Opens the stored password. */ public function __construct( array $stored, array $constants, SecretBox $box ) { $this->stored = self::coerce( $stored ); $this->constants = array_intersect_key( $constants, self::CONSTANTS ); $this->box = $box; } /** * Build from this site's option, constants and salts. * * @return self */ public static function from_environment() { return new self( self::stored(), self::defined_constants(), SecretBox::from_salts() ); } /** * The option's raw value. * * @return array */ public static function stored() { $stored = get_option( self::OPTION, array() ); return is_array( $stored ) ? $stored : array(); } /** * The ADMINKEEP_SMTP_* constants that are defined, keyed by field. * * @return array */ public static function defined_constants() { $found = array(); foreach ( self::CONSTANTS as $key => $name ) { if ( defined( $name ) ) { $found[ $key ] = constant( $name ); } } return $found; } /** * One resolved value. The password is not available here: see password(). * * @param string $key Field key. * @return mixed */ public function get( $key ) { if ( 'password' === $key ) { return ''; } if ( $this->is_locked( $key ) ) { $coerced = self::coerce( array( $key => $this->constants[ $key ] ) ); return $coerced[ $key ]; } return isset( $this->stored[ $key ] ) ? $this->stored[ $key ] : null; } /** * Whether wp-config.php holds this field. * * @param string $key Field key. * @return bool */ public function is_locked( $key ) { return array_key_exists( $key, $this->constants ); } /** * Whether SMTP is in charge of sending. No host, no takeover: switching the feature on * changes nothing until a server is named. * * @return bool */ public function is_active() { return '' !== $this->get( 'host' ); } /** * Whether any password is set, readable or not. * * @return bool */ public function has_password() { if ( $this->is_locked( 'password' ) ) { return '' !== (string) $this->constants['password']; } return '' !== $this->stored['password']; } /** * The plain password. * * @return string|false False when a stored password cannot be opened, which happens after * the site's salts change. The owner has to enter it again. */ public function password() { if ( $this->is_locked( 'password' ) ) { return (string) $this->constants['password']; } return $this->box->decrypt( $this->stored['password'] ); } /** * Turn a form submission into the array to store. * * @param array $input Unslashed POST values for the form. * @param array $previous What is stored now. * @param array $constants Field key => value for the defined constants. * @param SecretBox $box Seals the password. * @return array */ public static function sanitize( array $input, array $previous, array $constants, SecretBox $box ) { $previous = self::coerce( $previous ); $password = $previous['password']; // The screen disables the field while a password is saved, and a disabled field is not // posted: no value means keep. Once the field is posted, what it holds is the answer, and // an empty one removes the saved password. if ( isset( $input['password'] ) && is_string( $input['password'] ) ) { // Not run through sanitize_text_field(): a password may legitimately hold <, % or // leading spaces, and it is sealed, never printed. $password = '' === $input['password'] ? '' : $box->encrypt( $input['password'] ); } $clean = self::coerce( array( 'host' => sanitize_text_field( self::text( $input, 'host' ) ), 'port' => isset( $input['port'] ) && is_scalar( $input['port'] ) ? $input['port'] : 0, 'encryption' => self::text( $input, 'encryption' ), 'auth' => ! empty( $input['auth'] ), 'username' => sanitize_text_field( self::text( $input, 'username' ) ), 'from_email' => sanitize_email( self::text( $input, 'from_email' ) ), 'from_name' => sanitize_text_field( self::text( $input, 'from_name' ) ), 'force_from' => ! empty( $input['force_from'] ), 'force_name' => ! empty( $input['force_name'] ), ) ); $clean['password'] = $password; // A value that lives in wp-config.php is never copied into the database, and what was // stored before the constant was defined stays: removing the constant later falls back // to it instead of leaving the field empty. foreach ( array_keys( array_intersect_key( $constants, self::CONSTANTS ) ) as $key ) { $clean[ $key ] = $previous[ $key ]; } return $clean; } /** * Extract a text value from input, ignoring arrays. Prevents "Array to string conversion" * warnings and silent storage of the literal string "Array" when form fields are attacked with * array indices like host[]=value. Returns empty string for missing, non-scalar, or null values. * * @param array $input Form input array. * @param string $key Field key. * @return string */ private static function text( array $input, $key ) { return isset( $input[ $key ] ) && is_scalar( $input[ $key ] ) ? (string) $input[ $key ] : ''; } /** * Force every field to its type and legal range, filling gaps from the defaults. * * @param array $values Raw values. * @return array */ private static function coerce( array $values ) { $defaults = self::defaults(); $values = array_merge( $defaults, array_intersect_key( $values, $defaults ) ); $port = (int) $values['port']; return array( 'host' => trim( (string) $values['host'] ), 'port' => $port >= 1 && $port <= 65535 ? $port : $defaults['port'], 'encryption' => in_array( $values['encryption'], array( 'none', 'ssl', 'tls' ), true ) ? $values['encryption'] : $defaults['encryption'], 'auth' => (bool) $values['auth'], 'username' => (string) $values['username'], 'password' => (string) $values['password'], 'from_email' => is_email( (string) $values['from_email'] ) ? (string) $values['from_email'] : '', 'from_name' => (string) $values['from_name'], 'force_from' => (bool) $values['force_from'], 'force_name' => (bool) $values['force_name'], ); } }