enabled. * - ascue_last_status What happened to the most recent form message (autoload off). */ final class Ascue_Settings { const OPTION_SITE_KEY = 'ascue_site_key'; const OPTION_API_URL = 'ascue_api_url'; const OPTION_INTEGRATIONS = 'ascue_integrations'; const OPTION_LAST_STATUS = 'ascue_last_status'; const DEFAULT_API_URL = 'https://api.ascue.dev'; /** * Activation: create the site key option with autoload off, so the * Settings API later only updates it and never adds it as autoloaded. * * @return void */ public static function install() { self::ensure_site_key_option(); } /** * Creates the site key option (empty, autoload off) if it does not exist yet. * Also called on admin_init to cover updates that skip the activation hook. * * @return void */ public static function ensure_site_key_option() { if ( null === get_option( self::OPTION_SITE_KEY, null ) ) { add_option( self::OPTION_SITE_KEY, '', '', false ); } } /** * The saved site key, or '' when none is configured. * * @return string */ public static function get_site_key() { $key = get_option( self::OPTION_SITE_KEY, '' ); return is_string( $key ) ? $key : ''; } /** * The API base URL without a trailing slash. * * @return string */ public static function get_api_url() { $url = get_option( self::OPTION_API_URL, self::DEFAULT_API_URL ); $url = is_string( $url ) ? self::clean_api_url( $url ) : ''; return '' === $url ? self::DEFAULT_API_URL : $url; } /** * Whether the integration with the given id is enabled. Integrations are on * by default, including ones added in later plugin versions. * * @param string $id Integration id, such as "cf7". * @return bool */ public static function is_integration_enabled( $id ) { $saved = get_option( self::OPTION_INTEGRATIONS, null ); if ( ! is_array( $saved ) || ! array_key_exists( $id, $saved ) ) { return true; } return (bool) $saved[ $id ]; } /** * Sanitize callback for the site key. An empty submission keeps the saved * key (the field is never pre-filled), unless "Remove saved key" is ticked. * * @param mixed $value Submitted value. * @return string */ public static function sanitize_site_key( $value ) { // The Settings API verified the nonce in options.php before calling this. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['ascue_remove_site_key'] ) ) { return ''; } $key = self::clean_site_key( $value ); if ( '' === $key ) { return self::get_site_key(); } return $key; } /** * Strips everything that cannot be part of a site key. * * @param mixed $value Raw value. * @return string */ public static function clean_site_key( $value ) { if ( ! is_string( $value ) ) { return ''; } $key = preg_replace( '/[^A-Za-z0-9_.:\-]/', '', wp_strip_all_tags( $value ) ); return is_string( $key ) ? substr( $key, 0, 200 ) : ''; } /** * Sanitize callback for the API URL. Falls back to the default for anything * that is not an http(s) URL. * * @param mixed $value Submitted value. * @return string */ public static function sanitize_api_url( $value ) { $url = is_string( $value ) ? self::clean_api_url( $value ) : ''; if ( '' === $url ) { if ( is_string( $value ) && '' !== trim( $value ) && function_exists( 'add_settings_error' ) ) { add_settings_error( self::OPTION_API_URL, 'ascue_invalid_api_url', __( 'The API URL must start with https:// or http://. The default URL was saved instead.', 'ascue' ) ); } return self::DEFAULT_API_URL; } return $url; } /** * Normalizes an API URL: http(s) only, no trailing slash, no query string. * * @param string $value Raw URL. * @return string The URL, or '' if invalid. */ public static function clean_api_url( $value ) { $url = esc_url_raw( trim( $value ), array( 'https', 'http' ) ); if ( '' === $url ) { return ''; } $parts = wp_parse_url( $url ); if ( empty( $parts['scheme'] ) || empty( $parts['host'] ) ) { return ''; } $clean = strtolower( $parts['scheme'] ) . '://' . $parts['host']; if ( ! empty( $parts['port'] ) ) { $clean .= ':' . (int) $parts['port']; } if ( ! empty( $parts['path'] ) ) { // The plugin adds /v1/messages itself; tolerate a pasted ".../v1". $clean .= preg_replace( '#/v1/?$#', '', $parts['path'] ); } return untrailingslashit( $clean ); } /** * Sanitize callback for the integration checkboxes. * * @param mixed $value Submitted value: array of id => "1" for ticked boxes, or null. * @return array */ public static function sanitize_integrations( $value ) { $value = is_array( $value ) ? $value : array(); $clean = array(); foreach ( array_keys( Ascue_Plugin::integration_definitions() ) as $id ) { $clean[ $id ] = ! empty( $value[ $id ] ); } return $clean; } /** * Records what happened to the latest form message, for the settings page. * * @param string $integration Integration id. * @param bool $accepted Whether Ascue accepted the message. * @param string $detail Short reason, such as "HTTP 503" or a transport error. * @return void */ public static function record_last_status( $integration, $accepted, $detail ) { update_option( self::OPTION_LAST_STATUS, array( 'time' => time(), 'integration' => (string) $integration, 'accepted' => (bool) $accepted, 'detail' => substr( (string) $detail, 0, 200 ), ), false ); } /** * The latest form message status, or null. * * @return array{time:int,integration:string,accepted:bool,detail:string}|null */ public static function get_last_status() { $status = get_option( self::OPTION_LAST_STATUS, null ); if ( ! is_array( $status ) || empty( $status['time'] ) ) { return null; } return array( 'time' => (int) $status['time'], 'integration' => isset( $status['integration'] ) ? (string) $status['integration'] : '', 'accepted' => ! empty( $status['accepted'] ), 'detail' => isset( $status['detail'] ) ? (string) $status['detail'] : '', ); } }