'', // The service-account key file, ciphertext at rest; '' when disconnected. 'sa_email' => '', // client_email from the key — plaintext, it's what the owner grants in GSC. 'property' => '', // The matched Search Console property (sc-domain:… or URL prefix). 'connected_at' => 0, 'last_poll_at' => 0, 'last_error' => '', ); } /** * The resolved, defaults-merged settings. * * @return array */ public function all() { if ( null === $this->cache ) { $stored = get_option( self::OPTION, array() ); $this->cache = wp_parse_args( is_array( $stored ) ? $stored : array(), $this->defaults() ); } return $this->cache; } /** * One resolved value. * * @param string $key Field name. * @param mixed $fallback Returned when the field is unknown. * @return mixed */ public function get( $key, $fallback = null ) { $all = $this->all(); return array_key_exists( $key, $all ) ? $all[ $key ] : $fallback; } /** * Whether a key and a matched property are stored. * * @return bool */ public function connected() { $all = $this->all(); return '' !== (string) $all['sa_json'] && '' !== (string) $all['property']; } /** * The decrypted service-account JSON, or '' when none is stored (or it * can't be decrypted — e.g. the site's salts were rotated). * * @return string */ public function sa_json() { return Crypto::decrypt( (string) $this->get( 'sa_json', '' ) ); } /** * Store a working connection. The key is encrypted at this single * persistence point, so it is only ever STORED as ciphertext. * * @param string $sa_json The service-account key JSON, plaintext. * @param string $email The service-account email from the key. * @param string $property The matched GSC property. * @return void */ public function connect( $sa_json, $email, $property ) { $all = $this->all(); $all['sa_json'] = Crypto::encrypt_if_needed( (string) $sa_json ); $all['sa_email'] = sanitize_email( (string) $email ); $all['property'] = sanitize_text_field( (string) $property ); $all['connected_at'] = time(); $all['last_poll_at'] = 0; $all['last_error'] = ''; $this->persist( $all ); Auth::forget(); } /** * Forget the connection. Stored aggregates are the owner's history and stay. * * @return void */ public function disconnect() { $this->persist( $this->defaults() ); Auth::forget(); } /** * Record the outcome of a poll. * * @param string $error Human-readable failure, or '' for a clean poll. * @return void */ public function record_poll( $error = '' ) { $all = $this->all(); $all['last_poll_at'] = time(); $all['last_error'] = sanitize_text_field( (string) $error ); $this->persist( $all ); } /** * The connection state as the admin UI may see it — the key never crosses * the REST boundary in either direction; the email does, because granting * it Search Console access IS the setup step. * * @return array */ public function public_view() { $all = $this->all(); return array( 'connected' => $this->connected(), 'property' => (string) $all['property'], 'saEmail' => (string) $all['sa_email'], 'connectedAt' => (int) $all['connected_at'], 'lastPollAt' => (int) $all['last_poll_at'], 'lastError' => (string) $all['last_error'], ); } /** * Persist and refresh the cache. * * @param array $all The full settings array. * @return void */ private function persist( array $all ) { update_option( self::OPTION, $all, false ); // autoload OFF — read on demand only. $this->cache = $all; } }