robotstxt-mediaaudit/includes/External/TinEyeProvider.php
2026-06-03 06:31:47 +00:00

156 lines
4.2 KiB
PHP

<?php
/**
* TinEye reverse image search provider.
*
* @package MediaRightsAudit\External
*/
namespace MediaRightsAudit\External;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use MediaRightsAudit\Admin\Settings;
/**
* Submits images to the TinEye Commercial API and normalises the response
* into a ScanResult.
*
* Uses URL-based image submission (GET request with image_url parameter).
* Reads the API key from the plugin settings (tineye_api_key).
* Match count reflects the number of distinct matching images found;
* top domains are extracted from the backlink URLs of all matches.
*/
class TinEyeProvider extends AbstractProvider {
/**
* TinEye Commercial API search endpoint.
*/
const ENDPOINT = 'https://api.tineye.com/rest/search/';
/**
* Returns the machine-readable provider identifier.
*
* @return string
*/
public function provider_slug(): string {
return 'tineye';
}
/**
* Returns the human-readable provider display name.
*
* @return string
*/
public function provider_name(): string {
return 'TinEye';
}
/**
* Submits the image URL to TinEye 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( 'TinEye API key is not configured.' );
}
$url = add_query_arg(
array(
'api_key' => $api_key,
'image_url' => $file_url,
),
self::ENDPOINT
);
$response = wp_remote_get( $url, array( '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( 'TinEye 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 TinEye API response.' );
}
return $this->parse_response( $decoded );
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
/**
* Reads the TinEye 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['tineye_api_key'] ?? null;
return is_string( $val ) ? trim( $val ) : '';
}
/**
* Parses a decoded TinEye API response into a ScanResult.
*
* Navigates results.matches, extracts backlink page URLs to build the
* domain-frequency map, and counts distinct image matches.
*
* @param array<mixed, mixed> $decoded json_decode()'d API response.
*
* @return ScanResult
*/
private function parse_response( array $decoded ): ScanResult {
$results_raw = $decoded['results'] ?? null;
$results = is_array( $results_raw ) ? $results_raw : array();
$matches_raw = $results['matches'] ?? null;
$matches = is_array( $matches_raw ) ? $matches_raw : array();
$backlink_items = array();
foreach ( $matches as $match ) {
if ( ! is_array( $match ) ) {
continue;
}
$bls_raw = $match['backlinks'] ?? null;
if ( ! is_array( $bls_raw ) ) {
continue;
}
foreach ( $bls_raw as $bl ) {
if ( ! is_array( $bl ) ) {
continue;
}
// 'backlink' is the page URL; 'url' is the direct image URL.
$page_url_val = $bl['backlink'] ?? null;
if ( is_string( $page_url_val ) && '' !== $page_url_val ) {
$backlink_items[] = array( 'url' => $page_url_val );
}
}
}
$match_count = count( $matches );
$top_domains = $this->extract_top_domains( $backlink_items );
return new ScanResult( $match_count, $top_domains, $decoded );
}
}