v1.0.0
This commit is contained in:
commit
6708bbb67f
43 changed files with 8342 additions and 0 deletions
177
includes/External/GoogleVisionProvider.php
vendored
Normal file
177
includes/External/GoogleVisionProvider.php
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue