page_url() ) );
exit;
}
$site_ids = NetworkActivator::get_active_site_ids();
$count = 0;
foreach ( $site_ids as $site_id ) {
switch_to_blog( $site_id );
switch ( $op ) {
case 'internal':
AttachmentIndexer::schedule();
UsageScanner::schedule();
++$count;
break;
case 'external':
ExternalScanner::schedule();
++$count;
break;
case 'requeue_errors':
ExternalScanner::requeue_errors();
++$count;
break;
}
restore_current_blog();
}
delete_transient( self::STATUS_TRANSIENT );
wp_safe_redirect(
add_query_arg(
array(
'mra_notice' => 'success',
'mra_op' => $op,
'mra_count' => $count,
),
$this->page_url()
)
);
exit;
}
/**
* Returns the canonical URL for the Network Tools page.
*
* @return string
*/
private function page_url(): string {
return network_admin_url( 'admin.php?page=robotstxt-mediaaudit-network-tools' );
}
/**
* Renders the full network Tools page (used in central mode).
*
* @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' ) );
}
$status = $this->get_network_status();
$notice = $this->get_notice_data();
?>
render_notice( $notice ); ?>
render_site_status_table( $status ); ?>
render_network_operations(); ?>
get_network_status();
?>
render_site_status_table( $status ); ?>
> $status Per-site status data keyed by site_id.
*
* @return void
*/
private function render_site_status_table( array $status ): void {
?>
'internal',
'label' => __( 'Schedule Internal Scan — All Sites', 'robotstxt-mediaaudit' ),
'desc' => __( 'Queues indexing and usage scanning jobs on every active site. Safe to run at any time — skips already-indexed attachments.', 'robotstxt-mediaaudit' ),
'class' => 'button button-primary',
),
array(
'op' => 'external',
'label' => __( 'Schedule External Scan — All Sites', 'robotstxt-mediaaudit' ),
'desc' => __( 'Queues reverse-image-search jobs on every active site. Requires API keys configured in Network Settings.', 'robotstxt-mediaaudit' ),
'class' => 'button button-primary',
),
array(
'op' => 'requeue_errors',
'label' => __( 'Requeue Scan Errors — All Sites', 'robotstxt-mediaaudit' ),
'desc' => __( 'Re-queues attachments that failed with an error on the last external scan run across all sites.', 'robotstxt-mediaaudit' ),
'class' => 'button',
),
);
?>
$type,
'op' => $op,
'count' => $count,
);
}
/**
* Renders a WP admin notice for a completed network operation.
*
* @param array{type: string, op: string, count: int}|null $notice Notice data.
*
* @return void
*/
private function render_notice( ?array $notice ): void {
if ( null === $notice || 'success' !== $notice['type'] ) {
return;
}
$op = $notice['op'];
$count = $notice['count'];
?>
>
*/
private function get_network_status(): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$force_refresh = isset( $_GET['mra_refresh'] ) && '1' === $_GET['mra_refresh'];
if ( ! $force_refresh ) {
$cached = get_transient( self::STATUS_TRANSIENT );
if ( is_array( $cached ) ) {
$typed = array();
foreach ( $cached as $k => $v ) {
if ( is_int( $k ) && is_array( $v ) ) {
$typed[ $k ] = $v;
}
}
return $typed;
}
}
global $wpdb;
$site_ids = NetworkActivator::get_active_site_ids();
$result = array();
$as_ok = Scheduler::is_available();
foreach ( $site_ids as $site_id ) {
switch_to_blog( $site_id );
$site_obj = get_site( $site_id );
$site_name = $site_obj instanceof \WP_Site ? $site_obj->blogname : (string) $site_id;
$site_url = get_site_url( $site_id );
$audit_url = get_admin_url( $site_id, 'admin.php?page=robotstxt-mediaaudit' );
$raw_ver = get_option( 'robotstxt_mediaaudit_db_version', '' );
$stored_ver = is_string( $raw_ver ) ? $raw_ver : '';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$total_images = (int) $wpdb->get_var(
"SELECT COUNT(ID) FROM {$wpdb->posts}
WHERE post_type = 'attachment'
AND post_mime_type LIKE 'image/%'
AND post_status != 'trash'"
);
$indexed = AttachmentIndexer::get_indexed_count();
$ext = ExternalScanner::get_status_counts();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$alert_count = (int) $wpdb->get_var(
"SELECT COUNT(DISTINCT er.attachment_id)
FROM {$wpdb->prefix}mra_external_results er
WHERE NOT EXISTS (
SELECT 1 FROM {$wpdb->prefix}mra_dismissed_alerts da
WHERE da.attachment_id = er.attachment_id
)"
);
$result[ $site_id ] = array(
'site_name' => $site_name,
'site_url' => $site_url,
'audit_url' => $audit_url,
'db_ok' => ROBOTSTXT_MEDIAAUDIT_DB_VERSION === $stored_ver,
'total_images' => $total_images,
'indexed' => $indexed,
'external' => $ext,
'total_alert' => $alert_count,
'jobs' => array(
'index' => $as_ok && Scheduler::has_pending( AttachmentIndexer::AS_HOOK ),
'usage' => $as_ok && Scheduler::has_pending( UsageScanner::AS_HOOK ),
'external' => $as_ok && Scheduler::has_pending( ExternalScanner::AS_HOOK ),
),
);
restore_current_blog();
}
set_transient( self::STATUS_TRANSIENT, $result, 5 * MINUTE_IN_SECONDS );
return $result;
}
}