Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.67% covered (warning)
66.67%
38 / 57
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddRedLinks
66.67% covered (warning)
66.67%
38 / 57
0.00% covered (danger)
0.00%
0 / 1
39.93
0.00% covered (danger)
0.00%
0 / 1
 run
66.67% covered (warning)
66.67%
38 / 57
0.00% covered (danger)
0.00%
0 / 1
39.93
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html\PP\Processors;
5
6use Wikimedia\Parsoid\Config\Env;
7use Wikimedia\Parsoid\DOM\DocumentFragment;
8use Wikimedia\Parsoid\DOM\Element;
9use Wikimedia\Parsoid\DOM\Node;
10use Wikimedia\Parsoid\Utils\DOMCompat;
11use Wikimedia\Parsoid\Utils\PHPUtils;
12use Wikimedia\Parsoid\Utils\UrlUtils;
13use Wikimedia\Parsoid\Utils\WTUtils;
14use Wikimedia\Parsoid\Wt2Html\Wt2HtmlDOMProcessor;
15
16class AddRedLinks implements Wt2HtmlDOMProcessor {
17    /**
18     * Add red links to a document.
19     *
20     * @inheritDoc
21     */
22    public function run(
23        Env $env, Node $root, array $options = [], bool $atTopLevel = false
24    ): void {
25        '@phan-var Element|DocumentFragment $root';  // @var Element|DocumentFragment $root
26        $allLinks = PHPUtils::iterable_to_array(
27            DOMCompat::querySelectorAll( $root, 'a[rel~="mw:WikiLink"]' )
28        );
29
30        // Split up processing into chunks of 1000 so that we don't exceed LinkCache::MAX_SIZE
31        $chunks = array_chunk( $allLinks, 1000 );
32        foreach ( $chunks as $links ) {
33            $titles = [];
34            foreach ( $links as $a ) {
35                $t = DOMCompat::getAttribute( $a, 'title' );
36                if ( $t !== null ) {
37                    $titles[$t] = true;
38                }
39            }
40
41            if ( !$titles ) {
42                return;
43            }
44
45            $start = microtime( true );
46            $titleMap = $env->getDataAccess()->getPageInfo( $env->getPageConfig(), array_keys( $titles ) );
47            if ( $env->profiling() ) {
48                $profile = $env->getCurrentProfile();
49                $profile->bumpMWTime( "RedLinks", 1000 * ( microtime( true ) - $start ), "api" );
50                $profile->bumpCount( "RedLinks" );
51            }
52
53            $prefixedTitleText = $env->getContextTitle()->getPrefixedText();
54
55            foreach ( $links as $a ) {
56                $k = DOMCompat::getAttribute( $a, 'title' );
57                if ( $k === null ) {
58                    continue;
59                }
60                if ( empty( $titleMap[$k] ) ) {
61                    // Likely a consequence of T237535; can be removed once
62                    // that is fixed.
63                    $env->log( 'warn', 'We should have data for the title: ' . $k );
64                    continue;
65                }
66                $data = $titleMap[$k];
67                $a->removeAttribute( 'class' ); // Clear all, if we're doing a pb2pb refresh
68
69                $href = DOMCompat::getAttribute( $a, 'href' );
70                $parsedURL = UrlUtils::parseUrl( $href ?? '' );
71
72                $queryElts = [];
73                if ( isset( $parsedURL['query'] ) ) {
74                    parse_str( $parsedURL['query'], $queryElts );
75                }
76
77                if (
78                    !empty( $data['missing'] ) && empty( $data['known'] ) &&
79                    $k !== $prefixedTitleText
80                ) {
81                    DOMCompat::getClassList( $a )->add( 'new' );
82                    WTUtils::addPageContentI18nAttribute( $a, 'title', 'red-link-title', [ $k ] );
83                    $queryElts['action'] = 'edit';
84                    $queryElts['redlink'] = '1';
85                } else {
86                    if ( $k === $prefixedTitleText ) {
87                        if ( isset( $parsedURL['fragment'] ) ) {
88                            DOMCompat::getClassList( $a )->add( 'mw-selflink-fragment' );
89                        } else {
90                            DOMCompat::getClassList( $a )->add( 'mw-selflink', 'selflink' );
91                        }
92                        $a->removeAttribute( 'title' );
93                    }
94                    // Clear a potential redlink, if we're doing a pb2pb refresh
95                    // This is similar to what's happening in Html2Wt/RemoveRedLinks
96                    // and maybe that pass should just run before this one.
97                    if ( isset( $queryElts['action'] ) && $queryElts['action'] === 'edit' ) {
98                        unset( $queryElts['action'] );
99                    }
100                    if ( isset( $queryElts['redlink'] ) && $queryElts['redlink'] === '1' ) {
101                        unset( $queryElts['redlink'] );
102                    }
103                }
104
105                if ( count( $queryElts ) === 0 ) {
106                    // avoids the insertion of ? on empty query string
107                    $parsedURL['query'] = null;
108                } else {
109                    $parsedURL['query'] = http_build_query( $queryElts );
110                }
111                $newHref = UrlUtils::assembleUrl( $parsedURL );
112
113                $a->setAttribute( 'href', $newHref );
114
115                if ( !empty( $data['redirect'] ) ) {
116                    DOMCompat::getClassList( $a )->add( 'mw-redirect' );
117                }
118                foreach ( $data['linkclasses'] ?? [] as $extraClass ) {
119                    DOMCompat::getClassList( $a )->add( $extraClass );
120                }
121            }
122        }
123    }
124}