get_api_key(); if ( '' === $api_key ) { throw new \RuntimeException( 'Google Vision API key is not configured.' ); } $body = wp_json_encode( array( 'requests' => array( array( 'image' => array( 'source' => array( 'imageUri' => $file_url ) ), 'features' => array( array( 'type' => 'WEB_DETECTION', 'maxResults' => 100, ), ), ), ), ) ); if ( false === $body ) { throw new \RuntimeException( 'Failed to encode Google Vision request body.' ); } $response = wp_remote_post( self::ENDPOINT, array( 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8', 'X-Goog-Api-Key' => $api_key, ), 'body' => $body, 'timeout' => 30, ) ); if ( is_wp_error( $response ) ) { throw new \RuntimeException( $response->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } $http_code = (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $http_code ) { throw new \RuntimeException( sprintf( 'Google Vision API returned HTTP %d.', $http_code ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } $raw_body = wp_remote_retrieve_body( $response ); $decoded = json_decode( $raw_body, true ); if ( ! is_array( $decoded ) ) { throw new \RuntimeException( 'Failed to parse Google Vision API response.' ); } return $this->parse_response( $decoded ); } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- /** * Reads the Google Vision API key from plugin settings. * * @return string Empty string if not configured. */ private function get_api_key(): string { $raw = get_option( Settings::OPTION_NAME, array() ); $opts = is_array( $raw ) ? $raw : array(); $val = $opts['google_vision_api_key'] ?? null; return is_string( $val ) ? trim( $val ) : ''; } /** * Parses the decoded Vision API response into a ScanResult. * * @param array $decoded json_decode()'d API response. * * @return ScanResult */ private function parse_response( array $decoded ): ScanResult { $web_detection = $this->extract_web_detection( $decoded ); $pages_raw = $web_detection['pagesWithMatchingImages'] ?? null; $pages = is_array( $pages_raw ) ? $pages_raw : array(); $full_raw = $web_detection['fullMatchingImages'] ?? null; $full = is_array( $full_raw ) ? $full_raw : array(); $partial_raw = $web_detection['partialMatchingImages'] ?? null; $partial = is_array( $partial_raw ) ? $partial_raw : array(); $match_count = count( $pages ) + count( $full ); $top_domains = $this->extract_top_domains( array_merge( $pages, $full, $partial ) ); return new ScanResult( $match_count, $top_domains, $decoded ); } /** * Extracts the webDetection sub-object from the full API response. * * @param array $decoded Full decoded API response. * * @return array */ private function extract_web_detection( array $decoded ): array { $responses_raw = $decoded['responses'] ?? null; if ( ! is_array( $responses_raw ) ) { return array(); } $first = reset( $responses_raw ); if ( ! is_array( $first ) ) { return array(); } $wd = $first['webDetection'] ?? null; return is_array( $wd ) ? $wd : array(); } }