array( 'label' => __( 'Production', 'fabrixa-studio' ), 'studio' => 'https://studio.fabrixa.com', 'api' => 'https://api.fabrixa.com', ), 'custom' => array( 'label' => __( 'Custom', 'fabrixa-studio' ), 'studio' => '', 'api' => '', ), ); } private static $instance = null; public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_action( 'admin_menu', array( $this, 'add_menu' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); } public function add_menu() { /* * manage_options, not manage_woocommerce. The screen holds the webhook * secret, and options.php requires manage_options to save it anyway — so a * shop manager could previously open a page they could not submit, while * reading the secret out of its markup. */ add_submenu_page( 'woocommerce', __( 'Fabrixa Studio', 'fabrixa-studio' ), __( 'Fabrixa Studio', 'fabrixa-studio' ), 'manage_options', 'fabrixa-studio', array( $this, 'render_page' ) ); } public function register_settings() { register_setting( 'fabrixa_studio', self::OPT_APP_KEY, array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'fabrixa_studio', self::OPT_ENVIRONMENT, array( 'sanitize_callback' => array( $this, 'sanitize_environment' ) ) ); register_setting( 'fabrixa_studio', self::OPT_CUSTOM_STUDIO, array( 'sanitize_callback' => 'esc_url_raw' ) ); register_setting( 'fabrixa_studio', self::OPT_CUSTOM_API, array( 'sanitize_callback' => 'esc_url_raw' ) ); register_setting( 'fabrixa_studio', self::OPT_WEBHOOK_SECRET, array( 'sanitize_callback' => array( $this, 'sanitize_secret_webhook' ) ) ); } /** * Keep a stored secret when the field comes back empty. * * The settings screen never renders the secret, so every ordinary save * submits an empty field — and a disabled field (one a constant has taken * over) submits nothing at all, which reaches here as null. Both mean "no new * value", not "erase it". * * The cost is that a secret cannot be cleared from this screen, only replaced. * Clearing it is what the constant and a database edit are for; silently * losing one to a stray save is the worse failure. * * @param string|null $value Submitted value. * @param string $option Option being saved. * @return string */ private function sanitize_secret( $value, $option ) { $value = sanitize_text_field( (string) $value ); return ( '' === $value ) ? (string) get_option( $option, '' ) : $value; } /** * @param string|null $value Submitted value. * @return string */ public function sanitize_secret_webhook( $value ) { return $this->sanitize_secret( $value, self::OPT_WEBHOOK_SECRET ); } /** * Placeholder standing in for a secret that is never rendered. * * @param bool $stored Whether a value is saved. * @return string */ private static function secret_placeholder( $stored ) { return $stored ? '••••••••••••' : __( 'not set', 'fabrixa-studio' ); } public function sanitize_environment( $value ) { $envs = self::environments(); return isset( $envs[ $value ] ) ? $value : self::DEFAULT_ENVIRONMENT; } /* ---- Static getters used across the plugin ---- */ public static function app_key() { return get_option( self::OPT_APP_KEY, '' ); } /** * Shared secret for verifying incoming webhooks. * * A wp-config.php constant wins over the stored option so production secrets * can live outside the database. */ public static function webhook_secret() { if ( defined( 'FABRIXA_WEBHOOK_SECRET' ) && FABRIXA_WEBHOOK_SECRET ) { return FABRIXA_WEBHOOK_SECRET; } return get_option( self::OPT_WEBHOOK_SECRET, '' ); } /** * Currently selected environment. * * Validated against the known set on read, not just on save: a site that was * configured before an environment was retired still has the old value in the * database, and studio_url() would index a key that no longer exists. */ public static function environment() { $stored = get_option( self::OPT_ENVIRONMENT, self::DEFAULT_ENVIRONMENT ); $envs = self::environments(); return isset( $envs[ $stored ] ) ? $stored : self::DEFAULT_ENVIRONMENT; } public static function studio_url() { $env = self::environment(); if ( 'custom' === $env ) { return untrailingslashit( get_option( self::OPT_CUSTOM_STUDIO, '' ) ); } $envs = self::environments(); return untrailingslashit( $envs[ $env ]['studio'] ); } public static function api_base() { $env = self::environment(); if ( 'custom' === $env ) { return untrailingslashit( get_option( self::OPT_CUSTOM_API, '' ) ); } $envs = self::environments(); return untrailingslashit( $envs[ $env ]['api'] ); } public function render_page() { $current_env = self::environment(); $envs = self::environments(); ?>