storage = $storage; $this->client = $client; $this->verifier = $verifier; $this->clock = null === $clock ? 'time' : $clock; } /** @return array|\WP_Error */ public function maybe_refresh( $reason = 'request', $force = false ) { $settings = $this->storage->get(); if ( ! $this->storage->management_ready( $settings ) ) { return new \WP_Error( 'release_requires_ready_binding', __( 'Achla must report this verified site as active and indexed before releases are checked.', 'achlaai' ), array( 'status' => 409 ) ); } $now = call_user_func( $this->clock ); $expires = Protocol::parse_canonical_time( $settings['release']['control_expires_at'] ); if ( null !== $expires && $expires <= $now ) { $this->block_expired_control(); $settings = $this->storage->get(); } if ( ! $force && (int) $settings['release']['next_refresh_at'] > $now ) { return $settings['release']; } if ( ! $this->acquire_lock( $now ) ) { return $settings['release']; } try { return $this->refresh( $reason ); } finally { delete_option( self::REFRESH_LOCK_OPTION ); } } /** @return array|\WP_Error */ public function refresh( $reason = 'manual' ) { $settings = $this->storage->get(); $now = call_user_func( $this->clock ); $project_id = $settings['connection']['project_id']; if ( ! $this->storage->management_ready( $settings ) || 'verified' !== $this->storage->reconcile_origin() || ! Protocol::is_identifier( $project_id ) ) { return new \WP_Error( 'release_requires_verified_binding', __( 'Verify this site before checking widget releases.', 'achlaai' ), array( 'status' => 409 ) ); } $timeout = in_array( $reason, array( 'manual', 'admin', 'cron' ), true ) ? 6 : 2; $response = $this->client->fetch_widget_release( $project_id, $timeout ); if ( is_wp_error( $response ) ) { $this->record_error( $response->get_error_code(), 'achla_network_error' === $response->get_error_code(), $now ); return $response; } $result = $this->verifier->verify_response( $response, (int) $settings['release']['control_sequence'], (string) $settings['release']['control_hash'], $project_id, $now ); if ( is_wp_error( $result ) ) { $this->record_error( $result->get_error_code(), false, $now ); return $result; } $settings = $this->storage->get(); $control_expires = Protocol::parse_canonical_time( $result['control_expires_at'] ); $settings['release'] = array( 'status' => $result['killed'] ? 'killed' : 'ready', 'error_code' => '', 'control_sequence' => $result['control_sequence'], 'control_hash' => $result['control_hash'], 'control_generated_at' => $result['control_generated_at'], 'control_expires_at' => $result['control_expires_at'], 'manifest' => $result['killed'] ? null : $result['manifest'], 'verified_at' => $now, 'next_refresh_at' => min( $now + self::REFRESH_INTERVAL, max( $now + 30, $control_expires - 60 ) ), 'refresh_failures' => 0, ); $this->storage->save( $settings ); return $settings['release']; } /** @return array|null */ public function frontend_release() { $settings = $this->storage->get(); $release = $settings['release']; if ( ! $this->storage->management_ready( $settings ) || 'ready' !== $release['status'] || ! is_array( $release['manifest'] ) ) { return null; } $manifest = $release['manifest']; $expires = isset( $manifest['expiresAt'] ) ? Protocol::parse_canonical_time( $manifest['expiresAt'] ) : null; $control_generated = Protocol::parse_canonical_time( $release['control_generated_at'] ); $control_expires = Protocol::parse_canonical_time( $release['control_expires_at'] ); $now = call_user_func( $this->clock ); if ( null === $control_generated || null === $control_expires || $control_generated > $now + Release_Verifier::CLOCK_SKEW || $control_expires <= $now || $control_expires <= $control_generated || $control_expires - $control_generated > Release_Verifier::MAX_CONTROL_LIFETIME ) { $this->block_expired_control(); return null; } if ( null === $expires || $expires <= $now || empty( $manifest['policy']['enabled'] ) || ! empty( $manifest['policy']['killSwitch'] ) ) { return null; } return $manifest; } /** * @param string $code Safe classified error code. * @param bool $preserve_verified Preserve an unexpired verified release on transport failure. */ private function record_error( $code, $preserve_verified, $now ) { $settings = $this->storage->get(); $settings['release']['error_code'] = preg_match( '/^[a-z0-9_]+$/', $code ) ? $code : 'release_error'; $manifest = $settings['release']['manifest']; $expires = is_array( $manifest ) && isset( $manifest['expiresAt'] ) ? Protocol::parse_canonical_time( $manifest['expiresAt'] ) : null; $control_expires = Protocol::parse_canonical_time( $settings['release']['control_expires_at'] ); $failures = min( 6, (int) $settings['release']['refresh_failures'] + 1 ); $settings['release']['refresh_failures'] = $failures; $settings['release']['next_refresh_at'] = $now + min( 900, 30 * ( 2 ** ( $failures - 1 ) ) ); if ( ! $preserve_verified || 'ready' !== $settings['release']['status'] || null === $expires || $expires <= $now || null === $control_expires || $control_expires <= $now ) { $settings['release']['status'] = 'blocked'; } if ( 'release_control_expired' === $code ) { $settings['release']['manifest'] = null; } $this->storage->save( $settings ); } private function block_expired_control() { $settings = $this->storage->get(); $settings['release']['status'] = 'blocked'; $settings['release']['error_code'] = 'release_control_expired'; $settings['release']['manifest'] = null; $this->storage->save( $settings ); } /** @return bool */ private function acquire_lock( $now ) { $expires = get_option( self::REFRESH_LOCK_OPTION, 0 ); if ( is_numeric( $expires ) && (int) $expires <= $now ) { delete_option( self::REFRESH_LOCK_OPTION ); } return add_option( self::REFRESH_LOCK_OPTION, $now + self::LOCK_TTL, '', 'no' ); } }