false, 'ochered' => array(), // выданная пачка: номер записи => адрес (0 — главная) 'proydeno' => 0, // сколько страниц проверено 'kursor' => 0, // записи с номером до него включительно уже выданы 'potolok' => 0, // записи новее старта в этот обход не входят 'ostatok' => 0, // сколько записей ещё не выдано (пересчитывается с каждой пачкой) 'nahodki' => array(), // схлопнутые: ключ правило+есть+надо 'nachat' => 0, 'obnovlen' => 0, ); } function onsite_a11y_sostoyanie_obhoda() { $s = get_option( ONSITE_A11Y_OPCIYA_OBHODA ); // Состояние старого вида (список адресов) продолжить нельзя — только начать заново. return is_array( $s ) && isset( $s['kursor'] ) ? array_merge( onsite_a11y_pustoy_obhod(), $s ) : onsite_a11y_pustoy_obhod(); } /** * Записи и страницы с номером больше курсора и не больше потолка — первые $skolko * по возрастанию номера и сколько их всего. * * Берём главную, записи и страницы. Служебное не берём намеренно: вложения, * страницы авторов, архивы дат и рубрик — это одна и та же разметка в десятках * вариантов, находки там повторяются, а время обхода растёт кратно. * * Статичная главная страница уже проверена как главная — её не берём второй раз. * * @return array{0: WP_Post[], 1: int} */ function onsite_a11y_zapisi_posle( $kursor, $potolok, $skolko ) { // Номер статичной главной, 0 — главная лентой записей (записи с номером 0 не бывает). // Отсекаем её тем же условием, а не post__not_in: так found_posts сразу без неё. $glavnaya = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_on_front' ) : 0; $uslovie = function ( $where ) use ( $kursor, $potolok, $glavnaya ) { global $wpdb; return $where . $wpdb->prepare( " AND {$wpdb->posts}.ID > %d AND {$wpdb->posts}.ID <= %d AND {$wpdb->posts}.ID <> %d", $kursor, $potolok, $glavnaya ); }; $argumenty = array( 'post_type' => array( 'post', 'page' ), 'post_status' => 'publish', 'posts_per_page' => max( 1, $skolko ), 'orderby' => 'ID', 'order' => 'ASC', 'ignore_sticky_posts' => true, 'suppress_filters' => false, ); add_filter( 'posts_where', $uslovie ); $q = new WP_Query( $argumenty ); remove_filter( 'posts_where', $uslovie, 10 ); return array( $skolko > 0 ? $q->posts : array(), (int) $q->found_posts ); } /** Сколько страниц в обходе: проверенные, выданные и ещё не выданные. */ function onsite_a11y_vsego( $s ) { return (int) $s['proydeno'] + count( $s['ochered'] ) + (int) $s['ostatok']; } /** * Начать обход заново или продолжить прерванный. * * Продолжение — это НЕ новый обход: очередь и находки остаются, поэтому * прерванная проверка не начинается с нуля. */ function onsite_a11y_ajax_nachat() { onsite_a11y_proverit_pravo(); check_ajax_referer( 'onsite_a11y_obhod', 'klyuch' ); $zanovo = isset( $_POST['zanovo'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['zanovo'] ) ); $s = onsite_a11y_sostoyanie_obhoda(); if ( $zanovo || ! $s['ochered'] && ! $s['proydeno'] ) { $s = onsite_a11y_pustoy_obhod(); $posledniy = get_posts( array( 'post_type' => array( 'post', 'page' ), 'post_status' => 'publish', 'numberposts' => 1, 'orderby' => 'ID', 'order' => 'DESC', 'suppress_filters' => false, 'fields' => 'ids', ) ); $s['potolok'] = $posledniy ? (int) $posledniy[0] : 0; list( , $s['ostatok'] ) = onsite_a11y_zapisi_posle( 0, $s['potolok'], 0 ); $s['ochered'] = array( 0 => esc_url_raw( home_url( '/' ) ) ); $s['nachat'] = time(); } $s['idet'] = true; $s['obnovlen'] = time(); update_option( ONSITE_A11Y_OPCIYA_OBHODA, $s, false ); wp_send_json_success( onsite_a11y_kratko( $s ) ); } add_action( 'wp_ajax_onsite_a11y_nachat', 'onsite_a11y_ajax_nachat' ); /** * Отдать следующую пачку адресов. * * Пока выданная пачка не проверена целиком, отдаём её остаток: обход, * остановленный посреди пачки, продолжается с непроверенной страницы, а * проверенные второй раз не выдаются. Новую пачку берём из базы, только когда * старая кончилась. */ function onsite_a11y_ajax_pachka() { onsite_a11y_proverit_pravo(); check_ajax_referer( 'onsite_a11y_obhod', 'klyuch' ); $s = onsite_a11y_sostoyanie_obhoda(); if ( ! $s['idet'] ) { wp_send_json_success( array_merge( onsite_a11y_kratko( $s ), array( 'pachka' => array() ) ) ); } if ( ! $s['ochered'] ) { list( $zapisi, $naydeno ) = onsite_a11y_zapisi_posle( $s['kursor'], $s['potolok'], ONSITE_A11Y_PACHKA ); foreach ( $zapisi as $zapis ) { $s['kursor'] = (int) $zapis->ID; $adres = get_permalink( $zapis ); if ( $adres ) { $s['ochered'][ $s['kursor'] ] = esc_url_raw( $adres ); } } $s['ostatok'] = max( 0, $naydeno - count( $zapisi ) ); if ( ! $s['ochered'] ) { $s['idet'] = false; // в базе больше ничего — обход закончен } $s['obnovlen'] = time(); update_option( ONSITE_A11Y_OPCIYA_OBHODA, $s, false ); } $pachka = array_values( array_slice( $s['ochered'], 0, ONSITE_A11Y_PACHKA, true ) ); wp_send_json_success( array_merge( onsite_a11y_kratko( $s ), array( 'pachka' => $pachka ) ) ); } add_action( 'wp_ajax_onsite_a11y_pachka', 'onsite_a11y_ajax_pachka' ); /** * Принять находки по одной странице и убрать её из очереди. * * Одинаковые проблемы с разных страниц схлопываются в одну запись со счётчиком * страниц: полсотни строк «нет alt» по одной на страницу человек не читает, * а одна строка «встречается на 37 страницах» — читает. */ function onsite_a11y_ajax_prinyat() { onsite_a11y_proverit_pravo(); check_ajax_referer( 'onsite_a11y_obhod', 'klyuch' ); $adres = isset( $_POST['adres'] ) ? esc_url_raw( wp_unslash( $_POST['adres'] ) ) : ''; /* * Находки приходят одной строкой JSON и очищаются сразу на входе. Угловые * скобки и знак процента obhod.js пересылает юникод-кодами JSON (u003c, u003e, u0025): * очистка вырезала бы «