This commit is contained in:
Javier Casares 2026-06-03 06:24:03 +00:00
commit 6708bbb67f
43 changed files with 8342 additions and 0 deletions

View file

@ -0,0 +1,177 @@
<?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();
}
}