robotstxt-mediaaudit/includes/Admin/Settings.php
2026-06-08 18:01:39 +00:00

988 lines
27 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Settings page handler.
*
* @package MediaRightsAudit\Admin
*/
namespace MediaRightsAudit\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registers and renders the plugin Settings page with tab navigation.
*
* All tabs share a single option (robotstxt_mediaaudit_settings). A hidden
* _tab sentinel in each form submission tells sanitize() which fields to
* process, so saving one tab never clears another tab's data.
*/
class Settings {
/**
* WordPress Settings API option group name.
*/
const OPTION_GROUP = 'robotstxt_mediaaudit';
/**
* Option name in the database.
*/
const OPTION_NAME = 'robotstxt_mediaaudit_settings';
/**
* Default alert hostnames seeded on first activation.
*
* @var list<string>
*/
private const DEFAULT_FILTER_INCLUDE = array(
'123rf.com',
'afp.com',
'alamy.com',
'arcangel.com',
'artgrid.io',
'audioblocks.com',
'auroraphotos.com',
'backgridusa.com',
'bigstockphoto.com',
'blendimages.com',
'bridgemanimages.com',
'canstockphoto.com',
'cavanimages.com',
'creativemarket.com',
'crestock.com',
'cultura-rm.com',
'depositphotos.com',
'designbundles.net',
'diomedia.com',
'dreamstime.com',
'elements.envato.com',
'epa.eu',
'filmsupply.com',
'flaticon.com',
'fotolia.com',
'freepik.com',
'gallerystock.com',
'gettyimages.com',
'goffphotos.com',
'granger.com',
'graphicstock.com',
'imago-images.de',
'istockphoto.com',
'maryevans.com',
'mindenpictures.com',
'mintimages.com',
'mostphotos.com',
'naturepl.com',
'newsroom.ap.org',
'nhpa.co.uk',
'offset.shutterstock.com',
'pacificpressagency.com',
'panthermedia.net',
'photoshot.com',
'pictures.reuters.com',
'pixtastock.com',
'plainpicture.com',
'pond5.com',
'robertharding.com',
'sciencephoto.com',
'shutterstock.com',
'splashnews.com',
'stock.adobe.com',
'stocksy.com',
'storyblocks.com',
'trevillion.com',
'vecteezy.com',
'videoblocks.com',
'wenn.com',
'yayimages.com',
'zumapress.com',
);
/**
* Default ignored hostnames seeded on first activation.
*
* @var list<string>
*/
private const DEFAULT_FILTER_EXCLUDE = array(
'9gag.com',
'artstation.com',
'bandcamp.com',
'behance.net',
'blogger.com',
'bsky.app',
'dailymotion.com',
'deviantart.com',
'discord.com',
'douyin.com',
'dribbble.com',
'facebook.com',
'fb.com',
'flickr.com',
'gfycat.com',
'giphy.com',
'imgur.com',
'instagram.com',
'linkedin.com',
'livejournal.com',
'loom.com',
'mastodon.social',
'medium.com',
'notion.site',
'onlyfans.com',
'patreon.com',
'periscope.tv',
'pinterest.com',
'qq.com',
'reddit.com',
'snapchat.com',
'soundcloud.com',
'spotify.com',
'squarespace.com',
'streamable.com',
'substack.com',
'telegram.org',
'tenor.com',
'threads.net',
'tiktok.com',
'tumblr.com',
'twitch.tv',
'twitter.com',
'vimeo.com',
'vk.com',
'weebly.com',
'weibo.com',
'whatsapp.com',
'wix.com',
'wordpress.com',
'wordpress.org',
'x.com',
'xiaohongshu.com',
'youtube.com',
'zhihu.com',
);
/**
* Valid tab keys (order defines display order).
*
* @var list<string>
*/
private const TAB_KEYS = array( 'general', 'api', 'filters', 'external' );
/**
* Seeds the default filter lists into the option when they have not been set yet.
*
* Called once on plugin activation. Does nothing if the keys already exist,
* so existing user customisations are never overwritten.
*
* @return void
*/
public static function maybe_seed_defaults(): void {
$raw = get_option( self::OPTION_NAME, array() );
$options = is_array( $raw ) ? $raw : array();
$changed = false;
if ( ! array_key_exists( 'filter_include', $options ) ) {
$options['filter_include'] = self::DEFAULT_FILTER_INCLUDE;
$changed = true;
}
if ( ! array_key_exists( 'filter_exclude', $options ) ) {
$options['filter_exclude'] = self::DEFAULT_FILTER_EXCLUDE;
$changed = true;
}
if ( $changed ) {
update_option( self::OPTION_NAME, $options );
}
}
/**
* Returns translated tab labels keyed by tab ID.
*
* @return array<string, string>
*/
private function get_tabs(): array {
return array(
'general' => __( 'General', 'robotstxt-mediaaudit' ),
'api' => __( 'API Credentials', 'robotstxt-mediaaudit' ),
'filters' => __( 'Filters', 'robotstxt-mediaaudit' ),
'external' => __( 'External Scanning', 'robotstxt-mediaaudit' ),
);
}
/**
* Returns the active tab key derived from the current request.
*
* @return string
*/
private function get_active_tab(): string {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$raw = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general';
return in_array( $raw, self::TAB_KEYS, true ) ? $raw : 'general';
}
/**
* Returns the effective settings for the current context.
*
* In Multisite central mode, reads from the shared network site option so that
* all providers and scanners pick up network-wide API credentials and filter
* lists without changing per-site DB records.
*
* @return array<string, mixed>
*/
public static function get_effective_settings(): array {
if ( is_multisite() && 'central' === get_site_option( 'robotstxt_mediaaudit_network_mode', 'per_site' ) ) {
$raw = get_site_option( 'robotstxt_mediaaudit_network_settings', array() );
} else {
$raw = get_option( self::OPTION_NAME, array() );
}
return is_array( $raw ) ? $raw : array();
}
/**
* Sanitizes a settings array for the network admin, merging against $current.
*
* Mirrors the tab-based logic of sanitize() but accepts the existing values
* as a parameter rather than reading from get_option(), so it can be used
* for both per-site and network site options.
*
* @param array<string, mixed> $input Raw POST input (must include '_tab').
* @param array<string, mixed> $current Existing option values to merge against.
*
* @return array<string, mixed>
*/
public function sanitize_for_network( array $input, array $current ): array {
return $this->sanitize_with_base( $input, $current );
}
/**
* Registers all settings, sections, and fields via the WordPress Settings API.
*
* Hooked to admin_init.
*
* @return void
*/
public function register(): void {
register_setting(
self::OPTION_GROUP,
self::OPTION_NAME,
array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize' ),
'default' => array(),
'autoload' => false,
)
);
// --- General tab ---
add_settings_section(
'mra_general',
'',
array( $this, 'section_general_status' ),
'mra-settings-general'
);
add_settings_field(
'delete_on_uninstall',
__( 'Delete data on uninstall', 'robotstxt-mediaaudit' ),
array( $this, 'field_delete_on_uninstall' ),
'mra-settings-general',
'mra_general'
);
// --- API Credentials tab ---
add_settings_section(
'mra_api_credentials',
'',
'__return_false',
'mra-settings-api'
);
add_settings_field(
'google_vision_api_key',
__( 'Google Cloud Vision API Key', 'robotstxt-mediaaudit' ),
array( $this, 'field_google_vision_api_key' ),
'mra-settings-api',
'mra_api_credentials'
);
add_settings_field(
'tineye_api_key',
__( 'TinEye API Key', 'robotstxt-mediaaudit' ),
array( $this, 'field_tineye_api_key' ),
'mra-settings-api',
'mra_api_credentials'
);
add_settings_field(
'picdefense_user_id',
__( 'PicDefense User ID', 'robotstxt-mediaaudit' ),
array( $this, 'field_picdefense_user_id' ),
'mra-settings-api',
'mra_api_credentials'
);
add_settings_field(
'picdefense_api_key',
__( 'PicDefense API Key', 'robotstxt-mediaaudit' ),
array( $this, 'field_picdefense_api_key' ),
'mra-settings-api',
'mra_api_credentials'
);
// --- Filters tab ---
add_settings_section(
'mra_filters_include',
__( 'Alert Hostnames', 'robotstxt-mediaaudit' ),
array( $this, 'section_filters_include' ),
'mra-settings-filters'
);
add_settings_field(
'filter_include',
__( 'Hostnames', 'robotstxt-mediaaudit' ),
array( $this, 'field_filter_include' ),
'mra-settings-filters',
'mra_filters_include'
);
add_settings_section(
'mra_filters_exclude',
__( 'Ignored Hostnames', 'robotstxt-mediaaudit' ),
array( $this, 'section_filters_exclude' ),
'mra-settings-filters'
);
add_settings_field(
'filter_exclude',
__( 'Hostnames', 'robotstxt-mediaaudit' ),
array( $this, 'field_filter_exclude' ),
'mra-settings-filters',
'mra_filters_exclude'
);
// --- External Scanning tab ---
add_settings_section(
'mra_external_scanning',
'',
'__return_false',
'mra-settings-external'
);
add_settings_field(
'external_batch_size',
__( 'Batch Size', 'robotstxt-mediaaudit' ),
array( $this, 'field_external_batch_size' ),
'mra-settings-external',
'mra_external_scanning'
);
add_settings_field(
'rate_limit_per_minute',
__( 'Rate Limit (requests/min)', 'robotstxt-mediaaudit' ),
array( $this, 'field_rate_limit_per_minute' ),
'mra-settings-external',
'mra_external_scanning'
);
}
/**
* Sanitises the settings array on save.
*
* Reads the submitted _tab sentinel to determine which fields are present
* and merges them onto the existing stored values, leaving all other tabs
* untouched.
*
* @param mixed $input Raw POST input.
*
* @return array<string, mixed>
*/
public function sanitize( $input ): array {
$raw = get_option( self::OPTION_NAME, array() );
$current = is_array( $raw ) ? $raw : array();
return $this->sanitize_with_base( $input, $current );
}
/**
* Core tab-based sanitization logic shared by sanitize() and sanitize_for_network().
*
* @param mixed $input Raw POST input.
* @param array<string, mixed> $current Existing values to merge against.
*
* @return array<string, mixed>
*/
private function sanitize_with_base( $input, array $current ): array {
$output = $current;
if ( ! is_array( $input ) ) {
return $output;
}
$tab_raw = $input['_tab'] ?? '';
$tab = is_string( $tab_raw ) ? sanitize_key( $tab_raw ) : '';
if ( 'general' === $tab ) {
$output['delete_on_uninstall'] = ! empty( $input['delete_on_uninstall'] );
}
if ( 'api' === $tab ) {
$key_val = $input['google_vision_api_key'] ?? null;
$output['google_vision_api_key'] = is_string( $key_val ) ? sanitize_text_field( $key_val ) : '';
$tineye_val = $input['tineye_api_key'] ?? null;
$output['tineye_api_key'] = is_string( $tineye_val ) ? sanitize_text_field( $tineye_val ) : '';
$pd_uid_val = $input['picdefense_user_id'] ?? null;
$output['picdefense_user_id'] = is_string( $pd_uid_val ) ? sanitize_text_field( $pd_uid_val ) : '';
$pd_key_val = $input['picdefense_api_key'] ?? null;
$output['picdefense_api_key'] = is_string( $pd_key_val ) ? sanitize_text_field( $pd_key_val ) : '';
}
if ( 'filters' === $tab ) {
$output['filter_include'] = $this->sanitize_hostname_list( $input['filter_include'] ?? '' );
$output['filter_exclude'] = $this->sanitize_hostname_list( $input['filter_exclude'] ?? '' );
$overlap = array_intersect( $output['filter_include'], $output['filter_exclude'] );
if ( ! empty( $overlap ) ) {
$output['filter_exclude'] = array_values( array_diff( $output['filter_exclude'], $overlap ) );
add_settings_error(
self::OPTION_NAME,
'mra_filter_overlap',
sprintf(
/* translators: %s: comma-separated list of hostnames */
__( 'The following hostnames were removed from the exclusion list because they already appear in the alert list: %s', 'robotstxt-mediaaudit' ),
esc_html( implode( ', ', $overlap ) )
),
'warning'
);
}
}
if ( 'external' === $tab ) {
$batch_val = $input['external_batch_size'] ?? null;
$batch = is_numeric( $batch_val ) ? (int) $batch_val : 10;
$output['external_batch_size'] = max( 1, min( 100, $batch ) );
$rl_val = $input['rate_limit_per_minute'] ?? null;
$rl = is_numeric( $rl_val ) ? (int) $rl_val : 10;
$output['rate_limit_per_minute'] = max( 1, min( 60, $rl ) );
}
unset( $output['_tab'] );
return $output;
}
/**
* Public proxy for sanitize_hostname_list(), used by Network\Settings.
*
* @param mixed $raw Raw textarea value.
*
* @return list<string>
*/
public function sanitize_hostname_list_public( mixed $raw ): array {
return $this->sanitize_hostname_list( $raw );
}
/**
* Sanitises a newline-separated hostname list into a deduplicated sorted array.
*
* @param mixed $raw Raw textarea value.
*
* @return list<string>
*/
private function sanitize_hostname_list( mixed $raw ): array {
if ( ! is_string( $raw ) ) {
return array();
}
$split = preg_split( '/\r?\n/', $raw );
$lines = is_array( $split ) ? $split : array();
$result = array();
foreach ( $lines as $line ) {
$h = strtolower( sanitize_text_field( trim( $line ) ) );
if ( '' === $h ) {
continue;
}
// Preserve wildcard prefix before stripping URL parts.
$prefix = '';
if ( str_starts_with( $h, '*.' ) ) {
$prefix = '*.';
$h = substr( $h, 2 );
}
// Strip scheme (http://, https://, etc.).
$stripped = preg_replace( '/^[a-z][a-z0-9+\-.]*:\/\//', '', $h );
if ( is_string( $stripped ) ) {
$h = $stripped;
}
// Strip path, query string, and fragment — keep only host[:port].
$h = substr( $h, 0, strcspn( $h, '/?#' ) );
// Strip port number.
$no_port = preg_replace( '/:\d+$/', '', $h );
if ( is_string( $no_port ) ) {
$h = $no_port;
}
$h = trim( $h, '. ' );
// Strip www. prefix — example.com already covers www.example.com at match time.
if ( str_starts_with( $h, 'www.' ) ) {
$h = substr( $h, 4 );
}
$h = $prefix . $h;
if ( preg_match( '/^(\*\.)?[a-z0-9][a-z0-9\-]*(\.[a-z0-9][a-z0-9\-]*)+$/', $h ) ) {
$result[] = $h;
}
}
$result = array_values( array_unique( $result ) );
sort( $result );
return $result;
}
/**
* Renders the API credentials status block shown at the top of the General tab.
*
* @return void
*/
public function section_general_status(): void {
$gv_ok = $this->is_google_vision_configured();
$te_ok = $this->is_tineye_configured();
$pd_ok = $this->is_picdefense_configured();
?>
<div class="mra-provider-status">
<strong><?php esc_html_e( 'API Credentials Status', 'robotstxt-mediaaudit' ); ?></strong>
<ul>
<li>
<?php if ( $gv_ok ) : ?>
<span class="dashicons dashicons-yes-alt" style="color:green;"></span>
<?php else : ?>
<span class="dashicons dashicons-warning" style="color:orange;"></span>
<?php endif; ?>
<?php esc_html_e( 'Google Cloud Vision', 'robotstxt-mediaaudit' ); ?>
</li>
<li>
<?php if ( $te_ok ) : ?>
<span class="dashicons dashicons-yes-alt" style="color:green;"></span>
<?php else : ?>
<span class="dashicons dashicons-warning" style="color:orange;"></span>
<?php endif; ?>
<?php esc_html_e( 'TinEye', 'robotstxt-mediaaudit' ); ?>
</li>
<li>
<?php if ( $pd_ok ) : ?>
<span class="dashicons dashicons-yes-alt" style="color:green;"></span>
<?php else : ?>
<span class="dashicons dashicons-warning" style="color:orange;"></span>
<?php endif; ?>
PicDefense
</li>
</ul>
</div>
<?php
}
/**
* Renders the description for the "Alert Hostnames" filter section.
*
* @return void
*/
public function section_filters_include(): void {
?>
<p class="description">
<?php esc_html_e( 'Matches from these hostnames are flagged as critical copyright alerts. Entering example.com also covers all its subdomains.', 'robotstxt-mediaaudit' ); ?>
</p>
<?php
}
/**
* Renders the description for the "Ignored Hostnames" filter section.
*
* @return void
*/
public function section_filters_exclude(): void {
?>
<p class="description">
<?php esc_html_e( 'Matches from these hostnames are silently ignored and not counted as potential copyright issues. Entering example.com also covers all its subdomains. A hostname already present in the alert list cannot be added here.', 'robotstxt-mediaaudit' ); ?>
</p>
<?php
}
/**
* Renders the "Delete data on uninstall" checkbox field.
*
* @return void
*/
public function field_delete_on_uninstall(): void {
$options = (array) get_option( self::OPTION_NAME, array() );
$checked = ! empty( $options['delete_on_uninstall'] );
?>
<label>
<input
type="checkbox"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[delete_on_uninstall]"
value="1"
<?php checked( $checked ); ?>
/>
<?php esc_html_e( 'Remove all plugin data (tables and options) when the plugin is uninstalled.', 'robotstxt-mediaaudit' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'By default, data is preserved after uninstall. Enable this only if you want a clean removal.', 'robotstxt-mediaaudit' ); ?>
</p>
<?php
}
/**
* Renders the Google Cloud Vision API key field.
*
* @return void
*/
public function field_google_vision_api_key(): void {
$options = (array) get_option( self::OPTION_NAME, array() );
$val = $options['google_vision_api_key'] ?? '';
$value = is_string( $val ) ? $val : '';
?>
<input
type="text"
id="google_vision_api_key"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[google_vision_api_key]"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text"
autocomplete="off"
/>
<p class="description">
<?php esc_html_e( 'Required for Google Cloud Vision Web Detection. Obtain from the Google Cloud Console.', 'robotstxt-mediaaudit' ); ?>
&mdash;
<a href="https://cloud.google.com/vision/pricing" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'View pricing', 'robotstxt-mediaaudit' ); ?>
</a>
</p>
<table class="mra-pricing-table">
<thead>
<tr>
<th><?php esc_html_e( 'Monthly requests', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Price per 1,000', 'robotstxt-mediaaudit' ); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><?php esc_html_e( 'First 1,000', 'robotstxt-mediaaudit' ); ?></td>
<td><?php esc_html_e( 'Free', 'robotstxt-mediaaudit' ); ?></td>
</tr>
<tr>
<td>1,001 5,000,000</td>
<td>$1.50</td>
</tr>
<tr>
<td>5,000,001+</td>
<td>$0.60</td>
</tr>
</tbody>
</table>
<?php
}
/**
* Renders the TinEye API key field.
*
* @return void
*/
public function field_tineye_api_key(): void {
$options = (array) get_option( self::OPTION_NAME, array() );
$val = $options['tineye_api_key'] ?? '';
$value = is_string( $val ) ? $val : '';
?>
<input
type="text"
id="tineye_api_key"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[tineye_api_key]"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text"
autocomplete="off"
/>
<p class="description">
<?php esc_html_e( 'Required for TinEye reverse image search. Obtain from your TinEye Commercial account.', 'robotstxt-mediaaudit' ); ?>
&mdash;
<a href="https://services.tineye.com/TinEyeAPI" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'View pricing', 'robotstxt-mediaaudit' ); ?>
</a>
</p>
<table class="mra-pricing-table">
<thead>
<tr>
<th><?php esc_html_e( 'Bundle', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Searches', 'robotstxt-mediaaudit' ); ?></th>
<th><?php esc_html_e( 'Price', 'robotstxt-mediaaudit' ); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><?php esc_html_e( 'Starter', 'robotstxt-mediaaudit' ); ?></td>
<td>500</td>
<td>$50</td>
</tr>
<tr>
<td><?php esc_html_e( 'Standard', 'robotstxt-mediaaudit' ); ?></td>
<td>5,000</td>
<td>$200</td>
</tr>
<tr>
<td><?php esc_html_e( 'Professional', 'robotstxt-mediaaudit' ); ?></td>
<td>25,000</td>
<td>$600</td>
</tr>
</tbody>
</table>
<?php
}
/**
* Renders the PicDefense User ID field.
*
* @return void
*/
public function field_picdefense_user_id(): void {
$options = (array) get_option( self::OPTION_NAME, array() );
$val = $options['picdefense_user_id'] ?? '';
$value = is_string( $val ) ? $val : '';
?>
<input
type="text"
id="picdefense_user_id"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[picdefense_user_id]"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text"
autocomplete="off"
/>
<p class="description">
<?php esc_html_e( 'Required for PicDefense copyright risk analysis. Your PicDefense account user ID.', 'robotstxt-mediaaudit' ); ?>
</p>
<?php
}
/**
* Renders the PicDefense API key field.
*
* @return void
*/
public function field_picdefense_api_key(): void {
$options = (array) get_option( self::OPTION_NAME, array() );
$val = $options['picdefense_api_key'] ?? '';
$value = is_string( $val ) ? $val : '';
?>
<input
type="password"
id="picdefense_api_key"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[picdefense_api_key]"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text"
autocomplete="off"
/>
<p class="description">
<?php esc_html_e( 'Required for PicDefense copyright risk analysis. Your PicDefense API key.', 'robotstxt-mediaaudit' ); ?>
&mdash;
<a href="https://picdefense.io" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'View pricing', 'robotstxt-mediaaudit' ); ?>
</a>
</p>
<?php
}
/**
* Renders the alert hostname filter textarea.
*
* @return void
*/
public function field_filter_include(): void {
$options = (array) get_option( self::OPTION_NAME, array() );
$raw = $options['filter_include'] ?? array();
$strings = array();
if ( is_array( $raw ) ) {
foreach ( $raw as $item ) {
if ( is_string( $item ) ) {
$strings[] = $item;
}
}
}
$value = implode( "\n", $strings );
?>
<textarea
id="filter_include"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[filter_include]"
rows="8"
class="large-text code"
placeholder="example.com"
><?php echo esc_textarea( $value ); ?></textarea>
<p class="description">
<?php esc_html_e( 'One hostname per line. example.com covers all its subdomains. Explicit wildcard: *.example.com.', 'robotstxt-mediaaudit' ); ?>
</p>
<?php
}
/**
* Renders the ignored hostname filter textarea.
*
* @return void
*/
public function field_filter_exclude(): void {
$options = (array) get_option( self::OPTION_NAME, array() );
$raw = $options['filter_exclude'] ?? array();
$strings = array();
if ( is_array( $raw ) ) {
foreach ( $raw as $item ) {
if ( is_string( $item ) ) {
$strings[] = $item;
}
}
}
$value = implode( "\n", $strings );
?>
<textarea
id="filter_exclude"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[filter_exclude]"
rows="8"
class="large-text code"
placeholder="cdn.example.com"
><?php echo esc_textarea( $value ); ?></textarea>
<p class="description">
<?php esc_html_e( 'One hostname per line. example.com covers all its subdomains. Explicit wildcard: *.example.com.', 'robotstxt-mediaaudit' ); ?>
</p>
<?php
}
/**
* Renders the external batch size field.
*
* @return void
*/
public function field_external_batch_size(): void {
$options = (array) get_option( self::OPTION_NAME, array() );
$raw = $options['external_batch_size'] ?? 10;
$value = is_numeric( $raw ) ? (int) $raw : 10;
?>
<input
type="number"
id="external_batch_size"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[external_batch_size]"
value="<?php echo esc_attr( (string) $value ); ?>"
min="1"
max="100"
class="small-text"
/>
<p class="description">
<?php esc_html_e( 'Number of images to scan per scheduled batch (1100). Default: 10.', 'robotstxt-mediaaudit' ); ?>
</p>
<?php
}
/**
* Renders the per-minute rate limit field.
*
* @return void
*/
public function field_rate_limit_per_minute(): void {
$options = (array) get_option( self::OPTION_NAME, array() );
$raw = $options['rate_limit_per_minute'] ?? 10;
$value = is_numeric( $raw ) ? (int) $raw : 10;
?>
<input
type="number"
id="rate_limit_per_minute"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[rate_limit_per_minute]"
value="<?php echo esc_attr( (string) $value ); ?>"
min="1"
max="60"
class="small-text"
/>
<p class="description">
<?php esc_html_e( 'Maximum API requests per minute per provider (160). Default: 10.', 'robotstxt-mediaaudit' ); ?>
</p>
<?php
}
/**
* Returns whether Google Cloud Vision is configured.
*
* @return bool
*/
private function is_google_vision_configured(): bool {
$raw = get_option( self::OPTION_NAME, array() );
$opts = is_array( $raw ) ? $raw : array();
$key = $opts['google_vision_api_key'] ?? '';
return is_string( $key ) && '' !== trim( $key );
}
/**
* Returns whether TinEye is configured.
*
* @return bool
*/
private function is_tineye_configured(): bool {
$raw = get_option( self::OPTION_NAME, array() );
$opts = is_array( $raw ) ? $raw : array();
$key = $opts['tineye_api_key'] ?? '';
return is_string( $key ) && '' !== trim( $key );
}
/**
* Returns whether PicDefense is configured.
*
* @return bool
*/
private function is_picdefense_configured(): bool {
$raw = get_option( self::OPTION_NAME, array() );
$opts = is_array( $raw ) ? $raw : array();
$uid = $opts['picdefense_user_id'] ?? '';
$key = $opts['picdefense_api_key'] ?? '';
return is_string( $uid ) && '' !== trim( $uid ) && is_string( $key ) && '' !== trim( $key );
}
/**
* Renders the Settings admin page.
*
* @return void
*/
public function render(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'robotstxt-mediaaudit' ) );
}
$tab = $this->get_active_tab();
$tabs = $this->get_tabs();
$page_url = admin_url( 'admin.php?page=robotstxt-mediaaudit-settings' );
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<nav class="nav-tab-wrapper">
<?php foreach ( $tabs as $key => $label ) : ?>
<a
href="<?php echo esc_url( add_query_arg( 'tab', $key, $page_url ) ); ?>"
class="nav-tab<?php echo ( $tab === $key ) ? ' nav-tab-active' : ''; ?>"
><?php echo esc_html( $label ); ?></a>
<?php endforeach; ?>
</nav>
<form method="post" action="options.php">
<?php settings_fields( self::OPTION_GROUP ); ?>
<input
type="hidden"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[_tab]"
value="<?php echo esc_attr( $tab ); ?>"
/>
<?php do_settings_sections( 'mra-settings-' . $tab ); ?>
<?php submit_button(); ?>
</form>
</div>
<?php
}
}