This commit is contained in:
Javier Casares 2026-06-03 06:28:34 +00:00
commit 83d629568a
20 changed files with 1321 additions and 15 deletions

View file

@ -37,7 +37,10 @@ abstract class AbstractProvider {
* @return positive-int
*/
public function rate_limit(): int {
return 10;
$raw = get_option( 'robotstxt_mediaaudit_settings', array() );
$opts = is_array( $raw ) ? $raw : array();
$val = $opts['rate_limit_per_minute'] ?? null;
return is_numeric( $val ) ? max( 1, (int) $val ) : 10;
}
/**

View file

@ -138,7 +138,12 @@ class ExternalScanner {
* @return void
*/
public static function process_scheduled_batch(): void {
self::scan_batch();
$raw = get_option( 'robotstxt_mediaaudit_settings', array() );
$opts = is_array( $raw ) ? $raw : array();
$bs_val = $opts['external_batch_size'] ?? null;
$batch_size = is_numeric( $bs_val ) ? max( 1, (int) $bs_val ) : 10;
self::scan_batch( $batch_size );
if ( self::get_pending_count() > 0 ) {
Scheduler::schedule_single( self::AS_HOOK );

156
includes/External/PicDefenseProvider.php vendored Normal file
View file

@ -0,0 +1,156 @@
<?php
/**
* PicDefense copyright risk analysis provider.
*
* @package MediaRightsAudit\External
*/
namespace MediaRightsAudit\External;
use MediaRightsAudit\Admin\Settings;
/**
* Submits images to the PicDefense API and normalises the response
* into a ScanResult.
*
* Uses POST-based image URL submission with X-API-TOKEN header authentication.
* Reads credentials (picdefense_user_id and picdefense_api_key) from plugin settings.
* Match count reflects backlink_count from the API response; top domains are
* extracted from the found_at array.
*/
class PicDefenseProvider extends AbstractProvider {
/**
* PicDefense API endpoint.
*/
const ENDPOINT = 'https://app.picdefense.io/apiRouterv2/checkImageRisk';
/**
* Returns the machine-readable provider identifier.
*
* @return string
*/
public function provider_slug(): string {
return 'picdefense';
}
/**
* Returns the human-readable provider display name.
*
* @return string
*/
public function provider_name(): string {
return 'PicDefense';
}
/**
* Submits the image URL to PicDefense 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 {
list( $user_id, $api_key ) = $this->get_credentials();
if ( '' === $user_id || '' === $api_key ) {
throw new \RuntimeException( 'PicDefense credentials are not configured.' );
}
$body = wp_json_encode( array( 'url' => $file_url ) );
if ( false === $body ) {
throw new \RuntimeException( 'Failed to encode PicDefense request body.' );
}
$response = wp_remote_post(
self::ENDPOINT,
array(
'headers' => array(
'Content-Type' => 'application/json; charset=utf-8',
'X-API-TOKEN' => $user_id . ':' . $api_key,
),
'body' => $body,
'timeout' => 60,
)
);
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( 'PicDefense 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 PicDefense API response.' );
}
return $this->parse_response( $decoded );
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
/**
* Reads PicDefense credentials from plugin settings.
*
* @return array{string, string} [user_id, api_key] empty strings if not configured.
*/
private function get_credentials(): array {
$raw = get_option( Settings::OPTION_NAME, array() );
$opts = is_array( $raw ) ? $raw : array();
$uid_val = $opts['picdefense_user_id'] ?? null;
$key_val = $opts['picdefense_api_key'] ?? null;
$user_id = is_string( $uid_val ) ? trim( $uid_val ) : '';
$api_key = is_string( $key_val ) ? trim( $key_val ) : '';
return array( $user_id, $api_key );
}
/**
* Parses a decoded PicDefense API response into a ScanResult.
*
* @param array<mixed, mixed> $decoded json_decode()'d API response.
*
* @return ScanResult
*
* @throws \RuntimeException When the API response indicates failure.
*/
private function parse_response( array $decoded ): ScanResult {
$status_val = $decoded['status'] ?? null;
$status = is_numeric( $status_val ) ? (int) $status_val : 0;
if ( 1 !== $status ) {
throw new \RuntimeException( 'PicDefense API returned non-success status.' );
}
$data_raw = $decoded['data'] ?? null;
$data_arr = is_array( $data_raw ) ? $data_raw : array();
$data = isset( $data_arr[0] ) && is_array( $data_arr[0] ) ? $data_arr[0] : array();
$bc_val = $data['backlink_count'] ?? null;
$match_count = is_numeric( $bc_val ) ? (int) $bc_val : 0;
$found_at_raw = $data['found_at'] ?? null;
$found_at = is_array( $found_at_raw ) ? $found_at_raw : array();
$found_at = array_slice( $found_at, 0, 10 );
$top_domains = array();
foreach ( $found_at as $domain ) {
if ( is_string( $domain ) && '' !== $domain ) {
$top_domains[ $domain ] = 1;
}
}
return new ScanResult( $match_count, $top_domains, $decoded );
}
}

View file

@ -136,9 +136,10 @@ class TinEyeProvider extends AbstractProvider {
if ( ! is_array( $bl ) ) {
continue;
}
$url_val = $bl['url'] ?? null;
if ( is_string( $url_val ) && '' !== $url_val ) {
$backlink_items[] = array( 'url' => $url_val );
// '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 );
}
}
}