0 );
$public = get_post_types( array( 'public' => true ), 'names' );
unset( $public['attachment'] );
$post_types = apply_filters( 'markproof_post_types', array_values( $public ) );
$ids = get_posts(
array(
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => 400,
'fields' => 'ids',
'orderby' => 'modified',
'order' => 'DESC',
)
);
foreach ( $ids as $id ) {
$url = get_permalink( $id );
if ( $url ) {
$urls[ $url ] = (int) $id;
}
}
/* The core sitemap covers archives and anything else a theme exposes publicly. */
$sitemap = home_url( '/wp-sitemap.xml' );
$cached = get_transient( 'markproof_sitemap' );
if ( false === $cached ) {
$cached = array();
$response = wp_remote_get( $sitemap, array( 'timeout' => 15, 'user-agent' => 'Markproof – Accessibility Checks/' . STOCHASTIC_SCANNER_VERSION ) );
if ( ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
if ( preg_match_all( '#([^<]+)#i', wp_remote_retrieve_body( $response ), $m ) ) {
foreach ( $m[1] as $loc ) {
if ( preg_match( '/wp-sitemap-(posts|pages|taxonomies)-[a-z0-9_-]*-?[0-9]+\.xml$/i', $loc ) ) {
$sub = wp_remote_get( esc_url_raw( $loc ), array( 'timeout' => 15, 'user-agent' => 'Markproof – Accessibility Checks/' . STOCHASTIC_SCANNER_VERSION ) );
if ( ! is_wp_error( $sub ) && preg_match_all( '#([^<]+)#i', wp_remote_retrieve_body( $sub ), $m2 ) ) {
foreach ( $m2[1] as $u ) {
$cached[] = esc_url_raw( $u );
}
}
}
}
}
}
set_transient( 'markproof_sitemap', $cached, HOUR_IN_SECONDS );
}
foreach ( (array) $cached as $u ) {
if ( ! isset( $urls[ $u ] ) ) {
$urls[ $u ] = 0;
}
}
$urls = array_slice( $urls, 0, $limit, true );
return $urls;
}
public static function start_scan( $user_id = 0, $limit = 0 ) {
$list = self::build_url_list( $limit );
$urls = array();
$map = array();
foreach ( $list as $url => $post_id ) {
$urls[] = $url;
$map[ $url ] = $post_id;
}
$scan = Markproof_Store::create_scan( count( $urls ), $user_id );
update_option( self::OPTION_KEY . $scan, $urls, false );
update_option( self::MAP_KEY . $scan, $map, false );
return array(
'scan_id' => $scan,
'total' => count( $urls ),
);
}
/** Fetch and analyse up to $batch pages of an existing scan. */
public static function process_batch( $scan_id, $batch = 5, $time_budget = 20 ) {
$scan = Markproof_Store::get_scan( $scan_id );
if ( ! $scan ) {
return array( 'error' => 'unknown scan' );
}
$queue_key = self::OPTION_KEY . $scan_id;
$queue = get_option( $queue_key, array() );
$started = microtime( true );
$processed = 0;
$failed = 0;
while ( $queue && $processed < $batch && ( microtime( true ) - $started ) < $time_budget ) {
$url = array_shift( $queue );
$result = self::scan_url( $scan_id, $url );
if ( is_wp_error( $result ) ) {
$failed++;
}
$processed++;
}
update_option( $queue_key, $queue, false );
$done = (int) $scan->urls_done + $processed;
$data = array(
'urls_done' => $done,
'urls_failed' => (int) $scan->urls_failed + $failed,
);
if ( ! $queue ) {
$data['status'] = 'finished';
$data['finished_at'] = current_time( 'mysql', true );
delete_option( $queue_key );
self::store_post_counts( $scan_id );
}
Markproof_Store::update_scan( $scan_id, $data );
return array(
'scan_id' => (int) $scan_id,
'done' => $done,
'total' => (int) $scan->urls_total,
'remaining' => count( $queue ),
'failed' => (int) $scan->urls_failed + $failed,
'finished' => ! $queue,
);
}
/** How many findings each post has, so the Posts screen can show it at a glance. */
public static function store_post_counts( $scan_id ) {
global $wpdb;
$map = get_option( self::MAP_KEY . $scan_id, array() );
$counts = array();
if ( $map ) {
$rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- our own table, no core cache applies.
$wpdb->prepare( "SELECT url, SUM(occurrences) AS n FROM {$wpdb->prefix}markproof_issues WHERE scan_id = %d GROUP BY url", (int) $scan_id )
);
foreach ( $rows as $row ) {
$post_id = isset( $map[ $row->url ] ) ? (int) $map[ $row->url ] : 0;
if ( $post_id ) {
$counts[ $post_id ] = (int) $row->n;
}
}
}
update_option( 'markproof_post_counts_' . $scan_id, $counts, false );
update_option( 'markproof_post_counts_latest', array( 'scan_id' => (int) $scan_id, 'counts' => $counts ), false );
}
/** The edit link for a scanned URL, when that URL belongs to a post or page. */
public static function edit_link( $scan_id, $url ) {
$map = get_option( self::MAP_KEY . $scan_id, array() );
if ( empty( $map[ $url ] ) ) {
return '';
}
return get_edit_post_link( (int) $map[ $url ] );
}
/** Fetch one page and store what we found. Only our own host is ever fetched. */
public static function scan_url( $scan_id, $url ) {
$home_host = wp_parse_url( home_url(), PHP_URL_HOST );
$host = wp_parse_url( $url, PHP_URL_HOST );
if ( $host && $home_host && strtolower( $host ) !== strtolower( $home_host ) ) {
return new WP_Error( 'markproof_foreign_host', __( 'Refusing to scan a different host.', 'markproof' ) );
}
$response = wp_remote_get(
$url,
array(
'timeout' => 20,
'redirection' => 3,
'user-agent' => 'Markproof – Accessibility Checks/' . STOCHASTIC_SCANNER_VERSION . '; accessibility audit started by the site owner',
'headers' => array( 'Accept' => 'text/html,application/xhtml+xml' ),
)
);
$html = '';
$noted = false;
if ( ! is_wp_error( $response ) ) {
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code >= 200 && $code < 400 ) {
$html = wp_remote_retrieve_body( $response );
}
}
if ( '' === $html ) {
/* Many hosts cannot fetch their own front end. Fall back to the stored content of the
* post or page, and say so, because that markup can differ from what a visitor sees. */
$map = get_option( self::MAP_KEY . $scan_id, array() );
$post_id = isset( $map[ $url ] ) ? (int) $map[ $url ] : 0;
$post = $post_id ? get_post( $post_id ) : null;
if ( ! $post ) {
return is_wp_error( $response ) ? $response : new WP_Error( 'markproof_http', 'page could not be fetched' );
}
$html = '
' . esc_html( get_the_title( $post ) ) . ''
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- 'the_content' is WordPress's own filter, applied here so we check the page as WordPress renders it.
. apply_filters( 'the_content', $post->post_content ) . '';
$noted = true;
}
/* A page far larger than any real document would eat the host's PHP memory limit. Rather
* than risk taking a site down, we skip it and say so. */
if ( strlen( $html ) > 5000000 ) {
Markproof_Store::add_issue(
$scan_id,
$url,
'page_too_large',
'warning',
__( 'This page is larger than 5 MB of HTML, so it was skipped rather than risk the server running out of memory.', 'markproof' ),
'',
1
);
return 0;
}
$issues = Markproof_Rules::analyse( $html );
if ( $noted ) {
Markproof_Store::add_issue(
$scan_id,
$url,
'scanned_from_stored_content',
'warning',
__( 'This page could not be fetched by the server, so the stored content of the post was checked instead. The live page may differ.', 'markproof' ),
'',
1
);
}
/* Group identical findings on the same page so the report stays readable. */
$counts = array();
foreach ( $issues as $issue ) {
$key = $issue['rule_id'] . '|' . $issue['snippet'];
if ( ! isset( $counts[ $key ] ) ) {
$counts[ $key ] = $issue;
$counts[ $key ]['occurrences'] = 0;
}
$counts[ $key ]['occurrences']++;
}
foreach ( $counts as $issue ) {
Markproof_Store::add_issue(
$scan_id,
$url,
$issue['rule_id'],
$issue['severity'],
$issue['message'],
$issue['snippet'],
$issue['occurrences']
);
}
return count( $issues );
}
/** Cron path: run a whole scan in chunks without blowing up a single request. */
public static function start_background_scan() {
$start = self::start_scan( 0 );
$guard = 0;
do {
$state = self::process_batch( $start['scan_id'], 10, 20 );
$guard++;
} while ( ! $state['finished'] && $guard < 40 );
return $state;
}
}