Connectors screen where AI keys are * entered. * * @var string */ const BLACKOUT_CONNECTORS_UI = 'connectors_ui'; /** * Blackout switch: watching outgoing requests. * * @var string */ const BLACKOUT_OUTBOUND = 'outbound'; /** * Outgoing requests are not watched. * * @var string */ const OUTBOUND_OFF = 'off'; /** * Outgoing requests are recorded but let through. * * @var string */ const OUTBOUND_MONITOR = 'monitor'; /** * Outgoing requests are refused. * * @var string */ const OUTBOUND_BLOCK = 'block'; /** * Cached settings. * * @var array|null */ private $cache = null; /** * File backups, held outside the autoloaded settings option. * * @var array */ const BACKUP_OPTIONS = array( 'htaccess_backup' => 'aiblackout_htaccess_backup', 'robots_backup' => 'aiblackout_robots_backup', ); /** * Returns the default settings. * * @return array Default settings. */ public function get_defaults(): array { return array( 'schema' => 1, 'policies' => array( 'groups' => array(), 'agents' => array(), ), 'default_by_purpose' => array( 'training' => Policy::BLOCK, 'dataset' => Policy::BLOCK, 'scraping-tool' => Policy::BLOCK, 'search' => Policy::ALLOW, 'user-fetch' => Policy::ALLOW, ), 'search_impact_ack' => array(), 'onboarding_complete' => false, 'onboarding_choice' => '', 'review_prompt_first_eligible_at' => 0, 'badge' => array( 'enabled' => false, ), 'preserve_settings' => false, 'declarations' => array( 'tdm_reservation' => true, 'tdm_policy_url' => '', 'content_signals' => true, 'signal_search' => self::SIGNAL_YES, 'signal_ai_input' => self::SIGNAL_NO, 'signal_ai_train' => self::SIGNAL_NO, 'noai' => true, 'noimageai' => true, ), 'blackout' => array( self::BLACKOUT_CORE_AI => true, self::BLACKOUT_ABILITIES => true, self::BLACKOUT_CONNECTORS_UI => true, self::BLACKOUT_OUTBOUND => true, 'outbound_mode' => self::OUTBOUND_MONITOR, 'providers' => null, 'integrations' => array(), ), ); } /** * Returns all settings, merged over defaults. * * @return array Settings. */ public function all(): array { if ( null !== $this->cache ) { return $this->cache; } $stored = get_option( self::OPTION, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } $defaults = $this->get_defaults(); $merged = array_merge( $defaults, array_intersect_key( $stored, $defaults ) ); // Shallow merge above, then merge the nested arrays we care about. foreach ( array( 'policies', 'default_by_purpose', 'declarations', 'blackout', 'badge' ) as $key ) { $merged[ $key ] = array_merge( $defaults[ $key ], isset( $stored[ $key ] ) && is_array( $stored[ $key ] ) ? $stored[ $key ] : array() ); } $this->cache = $merged; return $this->cache; } /** * Returns a single top-level setting. * * @param string $key Setting key. * @param mixed $fallback Value returned when the key is absent. * @return mixed Setting value. */ public function get( string $key, $fallback = null ) { if ( isset( self::BACKUP_OPTIONS[ $key ] ) ) { $stored = get_option( self::BACKUP_OPTIONS[ $key ], null ); return null === $stored ? $fallback : $stored; } $all = $this->all(); return array_key_exists( $key, $all ) ? $all[ $key ] : $fallback; } /** * Persists a single top-level setting. * * @param string $key Setting key. * @param mixed $value Value to store. * @return bool True when the option was written. */ public function set( string $key, $value ): bool { if ( isset( self::BACKUP_OPTIONS[ $key ] ) ) { $option = self::BACKUP_OPTIONS[ $key ]; if ( null === $value ) { return delete_option( $option ); } return update_option( $option, $value, false ); } $all = $this->all(); $all[ $key ] = $value; return $this->save( $all ); } /** * Persists the full settings array. * * @param array $settings Settings to store. * @return bool True when the option was written. */ public function save( array $settings ): bool { $this->cache = null; return update_option( self::OPTION, $settings, true ); } /** * Whether the user has explicitly acknowledged the search impact of * blocking an agent. * * @param string $ua User-agent token. * @return bool True when acknowledged. */ public function has_search_impact_ack( string $ua ): bool { $ack = $this->get( 'search_impact_ack', array() ); if ( ! is_array( $ack ) ) { return false; } foreach ( $ack as $acked ) { if ( 0 === strcasecmp( (string) $acked, $ua ) ) { return true; } } return false; } /** * Records that the user accepted the search impact of blocking an agent. * * @param string $ua User-agent token. * @return bool True when the option was written. */ public function add_search_impact_ack( string $ua ): bool { $ack = $this->get( 'search_impact_ack', array() ); if ( ! is_array( $ack ) ) { $ack = array(); } if ( ! $this->has_search_impact_ack( $ua ) ) { $ack[] = $ua; } return $this->set( 'search_impact_ack', array_values( $ack ) ); } /** * Withdraws the search-impact confirmation for one crawler. * * @param string $ua User-agent token. * @return bool True when the option was written. */ public function remove_search_impact_ack( string $ua ): bool { $ack = $this->get( 'search_impact_ack', array() ); if ( ! is_array( $ack ) ) { return true; } $kept = array(); foreach ( $ack as $acked ) { if ( 0 !== strcasecmp( (string) $acked, $ua ) ) { $kept[] = $acked; } } return $this->set( 'search_impact_ack', array_values( $kept ) ); } /** * Returns a single declaration setting. * * @param string $key Declaration key. * @param mixed $fallback Value returned when the key is absent. * @return mixed Setting value. */ public function get_declaration( string $key, $fallback = null ) { $declarations = $this->get( 'declarations', array() ); return is_array( $declarations ) && array_key_exists( $key, $declarations ) ? $declarations[ $key ] : $fallback; } /** * Returns a single AI Blackout setting. * * @param string $key Blackout key. * @param mixed $fallback Value returned when the key is absent. * @return mixed Setting value. */ public function get_blackout( string $key, $fallback = null ) { $blackout = $this->get( 'blackout', array() ); return is_array( $blackout ) && array_key_exists( $key, $blackout ) ? $blackout[ $key ] : $fallback; } /** * Whether an AI Blackout switch is on. * * @param string $key One of the BLACKOUT_* constants. * @return bool True when enabled. */ public function is_blackout_enabled( string $key ): bool { return ! empty( $this->get_blackout( $key ) ); } /** * Returns the outgoing-request mode. * * @return string One of the OUTBOUND_* constants. */ public function get_outbound_mode(): string { $mode = (string) $this->get_blackout( 'outbound_mode', self::OUTBOUND_MONITOR ); if ( ! in_array( $mode, array( self::OUTBOUND_OFF, self::OUTBOUND_MONITOR, self::OUTBOUND_BLOCK ), true ) ) { return self::OUTBOUND_MONITOR; } return $mode; } /** * Deletes the stored option entirely. * * @return bool True on success. */ public function delete(): bool { $this->cache = null; return delete_option( self::OPTION ); } }