177 lines
4.9 KiB
PHP
177 lines
4.9 KiB
PHP
<?php
|
|
/**
|
|
* Google Cloud Vision Web Detection provider.
|
|
*
|
|
* @package MediaRightsAudit\External
|
|
*/
|
|
|
|
namespace MediaRightsAudit\External;
|
|
|
|
use MediaRightsAudit\Admin\Settings;
|
|
|
|
/**
|
|
* Sends images to the Google Cloud Vision API (WEB_DETECTION feature) and
|
|
* normalises the response into a ScanResult.
|
|
*
|
|
* Reads the API key from the plugin settings (google_vision_api_key).
|
|
* Supports public-URL image submissions; for private/staging sites the
|
|
* file_url may not be publicly accessible — a warning is surfaced via
|
|
* the scan error message in that case.
|
|
*/
|
|
class GoogleVisionProvider extends AbstractProvider {
|
|
|
|
/**
|
|
* Google Vision API endpoint (without key).
|
|
*/
|
|
const ENDPOINT = 'https://vision.googleapis.com/v1/images:annotate';
|
|
|
|
/**
|
|
* Returns the machine-readable provider identifier.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function provider_slug(): string {
|
|
return 'google_vision';
|
|
}
|
|
|
|
/**
|
|
* Returns the human-readable provider display name.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function provider_name(): string {
|
|
return __( 'Google Cloud Vision', 'robotstxt-mediaaudit' );
|
|
}
|
|
|
|
/**
|
|
* Sends the image to Google Cloud Vision Web Detection and returns a ScanResult.
|
|
*
|
|
* @param int $attachment_id WordPress attachment ID.
|
|
* @param string $file_url Public URL of the image.
|
|
*
|
|
* @return ScanResult
|
|
*
|
|
* @throws \RuntimeException On configuration, HTTP, or parse errors.
|
|
*/
|
|
protected function do_scan( int $attachment_id, string $file_url ): ScanResult {
|
|
$api_key = $this->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<mixed, mixed> $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<mixed, mixed> $decoded Full decoded API response.
|
|
*
|
|
* @return array<mixed, mixed>
|
|
*/
|
|
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();
|
|
}
|
|
}
|