*/ 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 */ 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 */ 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 */ 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'; } /** * 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(), ) ); // --- 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 */ public function sanitize( $input ): array { $raw = get_option( self::OPTION_NAME, array() ); $current = is_array( $raw ) ? $raw : 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; } /** * Sanitises a newline-separated list of hostnames into a deduplicated array. * * Accepts plain hostnames (example.com) and explicit wildcard prefixes * (*.example.com). A plain hostname implicitly covers all its subdomains * at match time. * * @param mixed $raw Raw textarea value. * * @return list */ 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(); ?>
  • PicDefense

1,001 – 5,000,000 $1.50
5,000,001+ $0.60

500 $50
5,000 $200
25,000 $600

get_active_tab(); $tabs = $this->get_tabs(); $page_url = admin_url( 'admin.php?page=robotstxt-mediaaudit-settings' ); ?>