<?php /** * This module is not considered part of the public API, only internal. */ namespace FortAwesome; require_once trailingslashit( FONTAWESOME_DIR_PATH ) . 'includes/class-fontawesome-exception.php'; use \WP_Error, \InvalidArgumentException; /** * Provides read/write access to the Font Awesome API settings. */ class FontAwesome_API_Settings { /** * Name of the option used to store API settings. * * @since 4.0.0 * @ignore */ const OPTIONS_KEY = 'font-awesome-api-settings'; /** * Current access token. * * @internal * @ignore */ protected $access_token = null; /** * Expiration time for current access token. * * @internal * @ignore */ protected $access_token_expiration_time = null; /** * Current API token. * * @internal * @ignore */ protected $api_token = null; /** * Singleton instance. * * @internal * @ignore */ protected static $instance = null; /** * Encryption method. * * @internal * @ignore */ protected $encryption_method = null; /** * Encryption cipher length. * * @internal * @ignore */ protected $encryption_cipher_length = null; /** * Encryption key. * * @internal * @ignore */ protected $encryption_key = null; /** * Encryption salt. * * @internal * @ignore */ protected $encryption_salt = null; /** * Preferred encryption method. * * @internal * @ignore */ const PREFERRED_ENCRYPTION_METHOD = 'aes-256-ctr'; /** * Returns the FontAwesome_API_Settings singleton instance. * * Internal use only. Not part of this plugin's public API. * * @return FontAwesome_API_Settings */ public static function instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Resets the singleton instance referenced by this class and returns that new instance. * All previous releases metadata held in the previous instance will be abandoned. * * @return FontAwesome_API_Settings */ public static function reset() { self::$instance = null; return self::instance(); } /** * Private constructor. * * @ignore */ private function __construct() { $this->initialize(); } /** * Initialize and instance * * Internal use only. * * @internal * @ignore */ public function initialize() { $this->prepare_encryption(); $option = $this->get_option(); if ( ! is_array( $option ) || ! isset( $option['api_token'] ) || ! array_key_exists( 'access_token', $option ) || ! array_key_exists( 'access_token_expiration_time', $option ) ) { return; } $this->api_token = is_string( $option['api_token'] ) ? $this->decrypt( $option['api_token'] ) : null; $this->access_token = is_string( $option['access_token'] ) ? $this->decrypt( $option['access_token'] ) : null; $this->access_token_expiration_time = is_numeric( $option['access_token_expiration_time'] ) ? $option['access_token_expiration_time'] : null; } /** * Writes current config. * * Internal use only. Not part of this plugin's public API. * * @ignore * @internal * @return bool whether write succeeded or needs no update */ public function write() { $new_api_token = is_string( $this->api_token() ) ? $this->encrypt( $this->api_token() ) : null; $new_access_token = is_string( $this->access_token() ) ? $this->encrypt( $this->access_token() ) : null; $new_access_token_expiration_time = is_numeric( $this->access_token_expiration_time() ) ? $this->access_token_expiration_time() : null; $new_option_value = array( 'api_token' => $new_api_token, 'access_token' => $new_access_token, 'access_token_expiration_time' => $new_access_token_expiration_time, ); $old_option_value = $this->get_option(); if ( is_array( $old_option_value ) && array_key_exists( 'api_token', $old_option_value ) && $old_option_value['api_token'] === $new_option_value['api_token'] && array_key_exists( 'access_token', $old_option_value ) && $old_option_value['access_token'] === $new_option_value['access_token'] && array_key_exists( 'access_token_expiration_time', $old_option_value ) && $old_option_value['access_token_expiration_time'] === $new_option_value['access_token_expiration_time'] ) { // They are already equivalent, so we don't need to write again. return true; } if ( file_exists( trailingslashit( ABSPATH ) . 'font-awesome-api.ini' ) ) { /** * Remove the old API settings file if it exists. * Anything previously stored in it will be obsolete. */ @unlink( trailingslashit( ABSPATH ) . 'font-awesome-api.ini' ); } return $this->update_option( $new_option_value ); } /** * Removes current API Token and related settings, setting them all to null, * and deletes the option store. * * Internal use only. Not part of this plugin's public API. * * @internal * @ignore */ public function remove() { delete_option( self::OPTIONS_KEY ); self::reset(); } /** * Returns the current API Token. * * Internal use only. Not part of this plugin's public API. * * @ignore * @internal */ public function api_token() { return $this->api_token; } /** * Sets the API Token. * * Internal use only. Not part of this plugin's public API. */ public function set_api_token( $api_token ) { $this->api_token = $api_token; } /** * Returns the current access token. * * Internal use only. Not part of this plugin's public API. * * @ignore * @internal */ public function access_token() { return $this->access_token; } /** * Sets the current access_token. * * Internal use only. Not part of this plugin's public API. */ public function set_access_token( $access_token ) { $this->access_token = $access_token; } /** * Sets the current access_token_expiration_time. * * Internal use only. Not part of this plugin's public API. * * @param int $access_token_expiration_time time in unix epoch seconds as non-zero integer value * @throws InvalidArgumentException if the given param is zero or cannot be cast as an integer */ public function set_access_token_expiration_time( $access_token_expiration_time ) { $int_val = intval( $access_token_expiration_time ); if ( 0 !== $int_val ) { $this->access_token_expiration_time = $access_token_expiration_time; } else { throw new InvalidArgumentException(); } } /** * Returns the expiration time for the current access token. * * Internal use only. Not part of this plugin's public API. * * @ignore * @internal */ public function access_token_expiration_time() { return $this->access_token_expiration_time; } /** * Requests an access_token with the current api_token. Stores the result * upon successfully retrieving an access token. * * Internal use only. Not part of this plugin's API. * * @ignore * @internal * @throws ApiTokenMissingException * @throws ApiTokenEndpointRequestException * @throws ApiTokenEndpointResponseException * @throws ApiTokenInvalidException * @throws AccessTokenStorageException * @return void */ public function request_access_token() { if ( ! is_string( $this->api_token() ) ) { throw new ApiTokenMissingException(); } $response = $this->post( array( 'body' => '', 'headers' => array( 'authorization' => 'Bearer ' . $this->api_token(), ), ) ); if ( is_wp_error( $response ) ) { throw ApiTokenEndpointRequestException::with_wp_error( add_failed_request_diagnostics( $response ) ); } if ( 200 !== $response['response']['code'] ) { throw ApiTokenInvalidException::with_wp_response( $response ); } $body = json_decode( $response['body'], true ); if ( ! isset( $body['access_token'] ) || ! is_string( $body['access_token'] ) || ! isset( $body['expires_in'] ) || ! is_int( $body['expires_in'] ) ) { throw ApiTokenEndpointResponseException::with_wp_response( $response ); } $this->set_access_token( $body['access_token'] ); try { $this->set_access_token_expiration_time( $body['expires_in'] + time() ); } catch ( InvalidArgumentException $e ) { throw ApiTokenEndpointResponseException::with_wp_response( $response ); } $result = $this->write(); if ( ! boolval( $result ) ) { throw new AccessTokenStorageException(); } else { return true; } } /** * Determines encryption method, key, salt, and cipher iv length if * the openssl extension is available. * * Internal use only. * * @internal * @ignore */ public function prepare_encryption() { if ( ! extension_loaded( 'openssl' ) ) { return; } $methods = openssl_get_cipher_methods(); $method = null; if ( array_search( self::PREFERRED_ENCRYPTION_METHOD, $methods, true ) ) { $method = self::PREFERRED_ENCRYPTION_METHOD; } elseif ( is_array( $methods ) && count( $methods ) > 0 ) { // Take the first available method as a fallback. $method = $methods[0]; } if ( $method && defined( 'LOGGED_IN_SALT' ) && is_string( LOGGED_IN_SALT ) && defined( 'LOGGED_IN_KEY' ) && is_string( LOGGED_IN_KEY ) ) { $this->encryption_method = $method; $this->encryption_cipher_length = openssl_cipher_iv_length( $method ); $this->encryption_key = LOGGED_IN_KEY; $this->encryption_salt = LOGGED_IN_SALT; } } /** * Encrypts and returns data. * * Internal use only. * * This method is patterned after the Data_Encryption::encrypt() method * in the Site Kit by Google plugin, version 1.4.0, licensed under Apache v2.0. * https://www.apache.org/licenses/LICENSE-2.0 * * @ignore * @internal */ public function encrypt( $data ) { if ( ! $this->encryption_key ) { return $data; } $init_vec = openssl_random_pseudo_bytes( $this->encryption_cipher_length ); $raw = openssl_encrypt( $data . $this->encryption_salt, $this->encryption_method, $this->encryption_key, 0, $init_vec ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return base64_encode( $init_vec . $raw ); } /** * Decrypts and returns data. * * Internal use only. * * This method is patterned after the Data_Encryption::decrypt() method * in the Site Kit by Google plugin, version 1.4.0, licensed under Apache v2.0. * https://www.apache.org/licenses/LICENSE-2.0 * * @ignore * @internal */ public function decrypt( $data ) { if ( ! $this->encryption_method ) { return $data; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $raw = base64_decode( $data, true ); $init_vec = substr( $raw, 0, $this->encryption_cipher_length ); $raw = substr( $raw, $this->encryption_cipher_length ); $result = openssl_decrypt( $raw, $this->encryption_method, $this->encryption_key, 0, $init_vec ); if ( ! $result || substr( $result, - strlen( $this->encryption_salt ) ) !== $this->encryption_salt ) { return null; } return substr( $result, 0, - strlen( $this->encryption_salt ) ); } /** * Wrapper for wp_remote_post(). Mostly to make it easier to mock the network * request with a subclass. * * Internal use only. Not part of this plugin's public API. * * @ignore * @internal * @return WP_Error | array just like wp_remote_post() */ protected function post( $args ) { return wp_remote_post( FONTAWESOME_API_URL . '/token', $args ); } /** * Internal use only. Not part of this plugin's public API. * * @ignore * @internal */ public function get_option() { return get_option( self::OPTIONS_KEY ); } /** * Internal use only. Not part of this plugin's public API. * * @ignore * @internal */ public function update_option( $new_option_value ) { return update_option( self::OPTIONS_KEY, $new_option_value ); } } /** * Convenience global function to get a singleton instance of the API Settings. * * Internal use only. Not part of this plugin's public API. * * @return FontAwesome_API_Settings */ function fa_api_settings() { return FontAwesome_API_Settings::instance(); }