384 lines
15 KiB
PHP
384 lines
15 KiB
PHP
<?php
|
||
/**
|
||
* Network admin Settings page.
|
||
*
|
||
* @package MediaRightsAudit\Network
|
||
*/
|
||
|
||
namespace MediaRightsAudit\Network;
|
||
|
||
use MediaRightsAudit\Admin\Settings as SiteSettings;
|
||
|
||
if ( ! defined( 'ABSPATH' ) ) {
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Manages network-wide plugin settings stored as site options.
|
||
*
|
||
* In central mode, all sites inherit API credentials and filter lists from the
|
||
* network option robotstxt_mediaaudit_network_settings. In per_site mode, each
|
||
* site uses its own robotstxt_mediaaudit_settings option.
|
||
*
|
||
* The mode toggle, API credentials, filter lists, and external scanning config
|
||
* are all managed here in the network admin.
|
||
*/
|
||
class Settings {
|
||
|
||
/**
|
||
* Network option key for shared settings (API keys, filters, external config).
|
||
*/
|
||
const NETWORK_OPTION = 'robotstxt_mediaaudit_network_settings';
|
||
|
||
/**
|
||
* Network option key for the operating mode.
|
||
*/
|
||
const MODE_OPTION = 'robotstxt_mediaaudit_network_mode';
|
||
|
||
/**
|
||
* Valid operating modes.
|
||
*
|
||
* @var list<string>
|
||
*/
|
||
private const VALID_MODES = array( 'per_site', 'central' );
|
||
|
||
/**
|
||
* Valid settings tabs (reuses same tab structure as per-site Settings).
|
||
*
|
||
* @var list<string>
|
||
*/
|
||
private const TAB_KEYS = array( 'mode', 'api', 'filters', 'external' );
|
||
|
||
/**
|
||
* Returns the active tab 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'] ) : 'mode';
|
||
return in_array( $raw, self::TAB_KEYS, true ) ? $raw : 'mode';
|
||
}
|
||
|
||
/**
|
||
* Handles the network_admin_edit_ action for saving settings.
|
||
*
|
||
* Redirects back to the settings page after saving.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function save(): void {
|
||
if ( ! current_user_can( 'manage_network_options' ) ) {
|
||
wp_die( esc_html__( 'You do not have permission to manage network settings.', 'robotstxt-mediaaudit' ) );
|
||
}
|
||
|
||
check_admin_referer( 'mra_network_settings_nonce' );
|
||
|
||
$tab_raw = isset( $_POST['_tab'] ) && is_string( $_POST['_tab'] ) ? sanitize_key( $_POST['_tab'] ) : '';
|
||
|
||
$this->save_tab( $tab_raw );
|
||
|
||
wp_safe_redirect(
|
||
add_query_arg(
|
||
array(
|
||
'page' => 'robotstxt-mediaaudit-network-settings',
|
||
'tab' => $tab_raw,
|
||
'updated' => '1',
|
||
),
|
||
network_admin_url( 'admin.php' )
|
||
)
|
||
);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Saves one tab's data to the appropriate site option.
|
||
*
|
||
* @param string $tab Tab key being saved.
|
||
*
|
||
* @return void
|
||
*/
|
||
private function save_tab( string $tab ): void {
|
||
if ( 'mode' === $tab ) {
|
||
$raw_mode = isset( $_POST['network_mode'] ) && is_string( $_POST['network_mode'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||
? sanitize_key( $_POST['network_mode'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||
: 'per_site';
|
||
$mode = in_array( $raw_mode, self::VALID_MODES, true ) ? $raw_mode : 'per_site';
|
||
|
||
$previous_mode = get_site_option( self::MODE_OPTION, 'per_site' );
|
||
update_site_option( self::MODE_OPTION, $mode );
|
||
|
||
// Migrate settings from main site on first switch to central mode.
|
||
if ( 'central' === $mode && 'central' !== $previous_mode ) {
|
||
$this->maybe_migrate_from_main_site();
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
$raw = get_site_option( self::NETWORK_OPTION, array() );
|
||
$current = is_array( $raw ) ? $raw : array();
|
||
$output = $current;
|
||
|
||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||
if ( 'api' === $tab ) {
|
||
$output['google_vision_api_key'] = sanitize_text_field( wp_unslash( is_string( $_POST['google_vision_api_key'] ?? null ) ? $_POST['google_vision_api_key'] : '' ) );
|
||
$output['tineye_api_key'] = sanitize_text_field( wp_unslash( is_string( $_POST['tineye_api_key'] ?? null ) ? $_POST['tineye_api_key'] : '' ) );
|
||
$output['picdefense_user_id'] = sanitize_text_field( wp_unslash( is_string( $_POST['picdefense_user_id'] ?? null ) ? $_POST['picdefense_user_id'] : '' ) );
|
||
$output['picdefense_api_key'] = sanitize_text_field( wp_unslash( is_string( $_POST['picdefense_api_key'] ?? null ) ? $_POST['picdefense_api_key'] : '' ) );
|
||
}
|
||
|
||
if ( 'filters' === $tab ) {
|
||
$site_settings = new SiteSettings();
|
||
// sanitize_hostname_list_public() fully sanitizes the raw textarea value.
|
||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||
$output['filter_include'] = $site_settings->sanitize_hostname_list_public( wp_unslash( is_string( $_POST['filter_include'] ?? null ) ? $_POST['filter_include'] : '' ) );
|
||
$output['filter_exclude'] = $site_settings->sanitize_hostname_list_public( wp_unslash( is_string( $_POST['filter_exclude'] ?? null ) ? $_POST['filter_exclude'] : '' ) );
|
||
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||
|
||
$overlap = array_intersect( $output['filter_include'], $output['filter_exclude'] );
|
||
if ( ! empty( $overlap ) ) {
|
||
$output['filter_exclude'] = array_values( array_diff( $output['filter_exclude'], $overlap ) );
|
||
}
|
||
}
|
||
|
||
if ( 'external' === $tab ) {
|
||
$batch_val = is_numeric( $_POST['external_batch_size'] ?? null ) ? (int) $_POST['external_batch_size'] : 10;
|
||
$output['external_batch_size'] = max( 1, min( 100, $batch_val ) );
|
||
|
||
$rl_val = is_numeric( $_POST['rate_limit_per_minute'] ?? null ) ? (int) $_POST['rate_limit_per_minute'] : 10;
|
||
$output['rate_limit_per_minute'] = max( 1, min( 60, $rl_val ) );
|
||
}
|
||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||
|
||
update_site_option( self::NETWORK_OPTION, $output );
|
||
}
|
||
|
||
/**
|
||
* Copies the main site's settings to the network option as a starting point.
|
||
*
|
||
* Only runs once — if the network option already has API credentials set,
|
||
* it is not overwritten.
|
||
*
|
||
* @return void
|
||
*/
|
||
private function maybe_migrate_from_main_site(): void {
|
||
$existing = get_site_option( self::NETWORK_OPTION, array() );
|
||
if ( is_array( $existing ) && ! empty( $existing ) ) {
|
||
return;
|
||
}
|
||
|
||
$main_site_id = get_main_site_id();
|
||
switch_to_blog( $main_site_id );
|
||
$site_settings = get_option( SiteSettings::OPTION_NAME, array() );
|
||
restore_current_blog();
|
||
|
||
if ( is_array( $site_settings ) && ! empty( $site_settings ) ) {
|
||
update_site_option( self::NETWORK_OPTION, $site_settings );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Renders the network Settings page.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function render(): void {
|
||
if ( ! current_user_can( 'manage_network_options' ) ) {
|
||
wp_die( esc_html__( 'You do not have permission to access this page.', 'robotstxt-mediaaudit' ) );
|
||
}
|
||
|
||
$tab = $this->get_active_tab();
|
||
$mode_raw = get_site_option( self::MODE_OPTION, 'per_site' );
|
||
$mode = is_string( $mode_raw ) ? $mode_raw : 'per_site';
|
||
$opts_raw = get_site_option( self::NETWORK_OPTION, array() );
|
||
$opts = is_array( $opts_raw ) ? $opts_raw : array();
|
||
|
||
$tabs = array(
|
||
'mode' => __( 'Mode', 'robotstxt-mediaaudit' ),
|
||
'api' => __( 'API Credentials', 'robotstxt-mediaaudit' ),
|
||
'filters' => __( 'Filters', 'robotstxt-mediaaudit' ),
|
||
'external' => __( 'External Scanning', 'robotstxt-mediaaudit' ),
|
||
);
|
||
|
||
$page_url = network_admin_url( 'admin.php?page=robotstxt-mediaaudit-network-settings' );
|
||
|
||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||
$updated = isset( $_GET['updated'] ) && '1' === $_GET['updated'];
|
||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||
?>
|
||
<div class="wrap">
|
||
<h1><?php esc_html_e( 'Media Audit — Network Settings', 'robotstxt-mediaaudit' ); ?></h1>
|
||
|
||
<?php \MediaRightsAudit\Admin\ManagerNotice::render_settings_notice(); ?>
|
||
|
||
<?php if ( $updated ) : ?>
|
||
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Settings saved.', 'robotstxt-mediaaudit' ); ?></p></div>
|
||
<?php endif; ?>
|
||
|
||
<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="<?php echo esc_url( network_admin_url( 'edit.php?action=mra_network_settings' ) ); ?>">
|
||
<?php wp_nonce_field( 'mra_network_settings_nonce' ); ?>
|
||
<input type="hidden" name="_tab" value="<?php echo esc_attr( $tab ); ?>" />
|
||
|
||
<?php if ( 'mode' === $tab ) : ?>
|
||
<?php $this->render_mode_tab( $mode ); ?>
|
||
|
||
<?php elseif ( 'api' === $tab ) : ?>
|
||
<?php $this->render_api_tab( $opts ); ?>
|
||
|
||
<?php elseif ( 'filters' === $tab ) : ?>
|
||
<?php $this->render_filters_tab( $opts ); ?>
|
||
|
||
<?php elseif ( 'external' === $tab ) : ?>
|
||
<?php $this->render_external_tab( $opts ); ?>
|
||
<?php endif; ?>
|
||
|
||
<?php submit_button(); ?>
|
||
</form>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Renders the Mode tab.
|
||
*
|
||
* @param string $current_mode Current operating mode.
|
||
*
|
||
* @return void
|
||
*/
|
||
private function render_mode_tab( string $current_mode ): void {
|
||
?>
|
||
<h2><?php esc_html_e( 'Operating Mode', 'robotstxt-mediaaudit' ); ?></h2>
|
||
<p class="description">
|
||
<?php esc_html_e( 'Choose how Media Audit operates across this network.', 'robotstxt-mediaaudit' ); ?>
|
||
</p>
|
||
<table class="form-table" role="presentation">
|
||
<tr>
|
||
<th scope="row"><?php esc_html_e( 'Mode', 'robotstxt-mediaaudit' ); ?></th>
|
||
<td>
|
||
<fieldset>
|
||
<label>
|
||
<input type="radio" name="network_mode" value="per_site"
|
||
<?php checked( $current_mode, 'per_site' ); ?> />
|
||
<strong><?php esc_html_e( 'Per site', 'robotstxt-mediaaudit' ); ?></strong>
|
||
</label>
|
||
<p class="description">
|
||
<?php esc_html_e( 'Each site manages its own media audit independently. No network admin aggregation. Site admins configure API credentials and run scans separately.', 'robotstxt-mediaaudit' ); ?>
|
||
</p>
|
||
<br />
|
||
<label>
|
||
<input type="radio" name="network_mode" value="central"
|
||
<?php checked( $current_mode, 'central' ); ?> />
|
||
<strong><?php esc_html_e( 'Central (network admin)', 'robotstxt-mediaaudit' ); ?></strong>
|
||
</label>
|
||
<p class="description">
|
||
<?php esc_html_e( 'API credentials and filter lists are configured here and shared across all sites. Data is stored per site but viewed and managed from this network admin panel. Site-level admin pages show a placeholder notice.', 'robotstxt-mediaaudit' ); ?>
|
||
</p>
|
||
</fieldset>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Renders the API Credentials tab (mirrors per-site Settings).
|
||
*
|
||
* @param array<string, mixed> $opts Current network settings.
|
||
*
|
||
* @return void
|
||
*/
|
||
private function render_api_tab( array $opts ): void {
|
||
$gv_key = is_string( $opts['google_vision_api_key'] ?? null ) ? $opts['google_vision_api_key'] : '';
|
||
$te_key = is_string( $opts['tineye_api_key'] ?? null ) ? $opts['tineye_api_key'] : '';
|
||
$pd_uid = is_string( $opts['picdefense_user_id'] ?? null ) ? $opts['picdefense_user_id'] : '';
|
||
$pd_key = is_string( $opts['picdefense_api_key'] ?? null ) ? $opts['picdefense_api_key'] : '';
|
||
?>
|
||
<p class="description"><?php esc_html_e( 'These credentials are shared across all sites in the network (central mode only).', 'robotstxt-mediaaudit' ); ?></p>
|
||
<table class="form-table" role="presentation">
|
||
<tr>
|
||
<th scope="row"><label for="mra_net_gv_key"><?php esc_html_e( 'Google Cloud Vision API Key', 'robotstxt-mediaaudit' ); ?></label></th>
|
||
<td><input type="text" id="mra_net_gv_key" name="google_vision_api_key" value="<?php echo esc_attr( $gv_key ); ?>" class="regular-text" autocomplete="off" /></td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="mra_net_te_key"><?php esc_html_e( 'TinEye API Key', 'robotstxt-mediaaudit' ); ?></label></th>
|
||
<td><input type="text" id="mra_net_te_key" name="tineye_api_key" value="<?php echo esc_attr( $te_key ); ?>" class="regular-text" autocomplete="off" /></td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="mra_net_pd_uid"><?php esc_html_e( 'PicDefense User ID', 'robotstxt-mediaaudit' ); ?></label></th>
|
||
<td><input type="text" id="mra_net_pd_uid" name="picdefense_user_id" value="<?php echo esc_attr( $pd_uid ); ?>" class="regular-text" autocomplete="off" /></td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="mra_net_pd_key"><?php esc_html_e( 'PicDefense API Key', 'robotstxt-mediaaudit' ); ?></label></th>
|
||
<td><input type="password" id="mra_net_pd_key" name="picdefense_api_key" value="<?php echo esc_attr( $pd_key ); ?>" class="regular-text" autocomplete="off" /></td>
|
||
</tr>
|
||
</table>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Renders the Filters tab.
|
||
*
|
||
* @param array<string, mixed> $opts Current network settings.
|
||
*
|
||
* @return void
|
||
*/
|
||
private function render_filters_tab( array $opts ): void {
|
||
$include_raw = $opts['filter_include'] ?? array();
|
||
$exclude_raw = $opts['filter_exclude'] ?? array();
|
||
|
||
$include_list = is_array( $include_raw ) ? array_filter( $include_raw, 'is_string' ) : array();
|
||
$exclude_list = is_array( $exclude_raw ) ? array_filter( $exclude_raw, 'is_string' ) : array();
|
||
?>
|
||
<p class="description"><?php esc_html_e( 'Hostname lists applied to all sites in the network (central mode only).', 'robotstxt-mediaaudit' ); ?></p>
|
||
<h3><?php esc_html_e( 'Alert Hostnames', 'robotstxt-mediaaudit' ); ?></h3>
|
||
<p class="description"><?php esc_html_e( 'Matches from these hostnames are flagged as copyright alerts. One hostname per line.', 'robotstxt-mediaaudit' ); ?></p>
|
||
<textarea name="filter_include" rows="8" class="large-text code"><?php echo esc_textarea( implode( "\n", $include_list ) ); ?></textarea>
|
||
|
||
<h3><?php esc_html_e( 'Ignored Hostnames', 'robotstxt-mediaaudit' ); ?></h3>
|
||
<p class="description"><?php esc_html_e( 'Matches from these hostnames are silently ignored. One hostname per line.', 'robotstxt-mediaaudit' ); ?></p>
|
||
<textarea name="filter_exclude" rows="8" class="large-text code"><?php echo esc_textarea( implode( "\n", $exclude_list ) ); ?></textarea>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Renders the External Scanning tab.
|
||
*
|
||
* @param array<string, mixed> $opts Current network settings.
|
||
*
|
||
* @return void
|
||
*/
|
||
private function render_external_tab( array $opts ): void {
|
||
$batch_size = is_numeric( $opts['external_batch_size'] ?? null ) ? (int) $opts['external_batch_size'] : 10;
|
||
$rate_limit = is_numeric( $opts['rate_limit_per_minute'] ?? null ) ? (int) $opts['rate_limit_per_minute'] : 10;
|
||
?>
|
||
<table class="form-table" role="presentation">
|
||
<tr>
|
||
<th scope="row"><label for="mra_net_batch"><?php esc_html_e( 'Batch Size', 'robotstxt-mediaaudit' ); ?></label></th>
|
||
<td>
|
||
<input type="number" id="mra_net_batch" name="external_batch_size"
|
||
value="<?php echo esc_attr( (string) $batch_size ); ?>" min="1" max="100" class="small-text" />
|
||
<p class="description"><?php esc_html_e( 'Images to scan per scheduled batch per site (1–100). Default: 10.', 'robotstxt-mediaaudit' ); ?></p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="mra_net_rl"><?php esc_html_e( 'Rate Limit (req/min)', 'robotstxt-mediaaudit' ); ?></label></th>
|
||
<td>
|
||
<input type="number" id="mra_net_rl" name="rate_limit_per_minute"
|
||
value="<?php echo esc_attr( (string) $rate_limit ); ?>" min="1" max="60" class="small-text" />
|
||
<p class="description"><?php esc_html_e( 'Maximum API requests per minute per provider per site (1–60). Default: 10.', 'robotstxt-mediaaudit' ); ?></p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<?php
|
||
}
|
||
}
|