*/ public static function run( ?float $deadline = null, ?callable $each = null ): array { $end = min( $deadline ?? INF, microtime( true ) + self::BUDGET ); wp_cache_flush(); // The options the switch brought in, never values cached from the old tables. self::hard_flush(); delete_option( 'rewrite_rules' ); // Rebuilt by the first request below, with the imported site's code. delete_option( self::FLUSH_FLAG ); $targets = self::targets(); $total = count( $targets ); $rows = []; $each && $each( 0, $total ); foreach ( $targets as $t ) { $left = $end - microtime( true ); if ( $left < 0.2 ) { $rows[] = [ 'label' => $t['label'], 'url' => $t['url'], 'status' => 0, 'ok' => null ]; } else { $status = self::status( $t['url'], min( self::TIMEOUT, $left ) ); $away = $status < 0; // Redirected off this site. $status = abs( $status ); $cut = 0 === $status && microtime( true ) >= $end - 0.05; // Stopped by the budget, not answered "no". $rows[] = [ 'label' => $t['label'], 'url' => $t['url'], 'status' => $status, 'ok' => $cut ? null : ( ! $away && ( 404 === $t['expect'] ? 404 === $status : ( $status >= 200 && $status < 400 ) ) ), ]; } $each && $each( count( $rows ), $total ); } return $rows; } /** flush_rewrite_rules( true ) with this site's current permalink settings. */ private static function hard_flush(): void { global $wp_rewrite; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/misc.php'; if ( $wp_rewrite instanceof WP_Rewrite ) { $wp_rewrite->init(); // permalink_structure as stored now, not as read when this request started. } flush_rewrite_rules( true ); } /** @return list */ private static function targets(): array { $list = [ [ 'label' => __( 'Home page', 'mench-migrate' ), 'url' => home_url( '/' ) ] ]; foreach ( [ 'post' => __( 'Latest post', 'mench-migrate' ), 'page' => __( 'Latest page', 'mench-migrate' ) ] as $type => $label ) { $url = self::latest( $type ); if ( '' !== $url ) { $list[] = [ 'label' => $label, 'url' => $url ]; } } $active = array_map( 'strval', (array) get_option( 'active_plugins', [] ) ); $has = static fn( string $folder ): bool => (bool) array_filter( $active, static fn( string $p ): bool => 0 === strpos( $p, $folder ) ); if ( $has( 'woocommerce/' ) && '' !== ( $url = self::latest( 'product' ) ) ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition $list[] = [ 'label' => __( 'A product', 'mench-migrate' ), 'url' => $url ]; } if ( $has( 'easy-digital-downloads' ) && '' !== ( $url = self::latest( 'download' ) ) ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition $list[] = [ 'label' => __( 'A download', 'mench-migrate' ), 'url' => $url ]; } $missing = 'mench-migrate-check-' . Mench_Migrate_Paths::random_suffix( 12 ); $list[] = [ 'label' => __( 'A missing page', 'mench-migrate' ), // Plain permalinks route every path to the front page, so there a missing post id stands in. 'url' => '' === (string) get_option( 'permalink_structure' ) ? add_query_arg( 'p', 2147483000 + wp_rand( 0, 600 ), home_url( '/' ) ) : home_url( '/' . $missing . '/' ), 'expect' => 404, ]; $list = apply_filters( 'mench_migrate_link_check_urls', $list ); $hosts = array_map( 'strtolower', (array) apply_filters( 'mench_migrate_link_check_hosts', [ self::home_host() ] ) ); $out = []; foreach ( is_array( $list ) ? $list : [] as $t ) { if ( ! is_array( $t ) || ! is_string( $t['url'] ?? null ) || ! preg_match( '#^https?://#i', $t['url'] ) ) { continue; } if ( ! in_array( strtolower( (string) wp_parse_url( $t['url'], PHP_URL_HOST ) ), $hosts, true ) ) { continue; // Only this site is asked. } $out[] = [ 'label' => sanitize_text_field( (string) ( $t['label'] ?? $t['url'] ) ), 'url' => esc_url_raw( $t['url'] ), 'expect' => 404 === (int) ( $t['expect'] ?? 200 ) ? 404 : 200, ]; } return $out; } /** Permalink of the latest published item of a post type, or ''. Read from the table: the type may not be registered by this request's code. */ private static function latest( string $type ): string { global $wpdb; $id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish' ORDER BY post_date DESC, ID DESC LIMIT 1", $type ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery if ( $id <= 0 ) { return ''; } if ( post_type_exists( $type ) ) { $url = get_permalink( $id ); return is_string( $url ) ? $url : ''; } return add_query_arg( [ 'post_type' => $type, 'p' => $id ], home_url( '/' ) ); // WordPress redirects it to the pretty URL. } private static function home_host(): string { return strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ); } /** * HTTP status of a URL (HEAD, GET when HEAD is refused), 0 when it cannot be reached in $timeout seconds. * Redirects are followed by hand, up to MAX_REDIRECTS and only to the same host; one that leaves the host is not * followed and makes the URL fail (its status comes back negated). */ private static function status( string $url, float $timeout ): int { $start = microtime( true ); $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); $local = in_array( $host, [ 'localhost', '127.0.0.1', '::1', '[::1]' ], true ) || (bool) preg_match( '/\.(test|local|localhost|invalid|example)$/', $host ); try { for ( $hop = 0; $hop <= self::MAX_REDIRECTS; $hop++ ) { $left = $timeout - ( microtime( true ) - $start ); if ( $left <= 0.05 ) { return 0; } $args = [ 'timeout' => $left, 'redirection' => 0, 'sslverify' => ! $local, 'user-agent' => 'Mench Migrate link check; ' . home_url( '/' ) ]; $probe = add_query_arg( 'doing_wp_cron', sprintf( '%.4F', microtime( true ) ), $url ); // No WP-Cron spawn from this visit. $res = wp_remote_head( $probe, $args ); $code = is_wp_error( $res ) ? 0 : (int) wp_remote_retrieve_response_code( $res ); if ( 405 === $code || 501 === $code ) { $res = wp_remote_get( $probe, $args + [ 'limit_response_size' => 65536 ] ); $code = is_wp_error( $res ) ? 0 : (int) wp_remote_retrieve_response_code( $res ); } $location = $code >= 300 && $code < 400 ? (string) wp_remote_retrieve_header( $res, 'location' ) : ''; if ( '' === $location ) { return $code; } $next = WP_Http::make_absolute_url( $location, $url ); $next = remove_query_arg( 'doing_wp_cron', $next ); if ( strtolower( (string) wp_parse_url( $next, PHP_URL_HOST ) ) !== $host ) { return -$code; // Off this site: not followed. } $url = $next; } return 0; // Too many redirects. } catch ( Throwable $e ) { return 0; } } }