Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialLinkSearch
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 13
930
0.00% covered (danger)
0.00%
0 / 1
 setParams
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isCacheable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
110
 isSyndicated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 linkParameters
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
56
 preprocessResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatResult
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getOrderFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMaxResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRecacheDB
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Deferred\LinksUpdate\ExternalLinksTable;
10use MediaWiki\ExternalLinks\LinkFilter;
11use MediaWiki\HTMLForm\HTMLForm;
12use MediaWiki\MainConfigNames;
13use MediaWiki\Message\Message;
14use MediaWiki\Page\LinkBatchFactory;
15use MediaWiki\Parser\Parser;
16use MediaWiki\Skin\Skin;
17use MediaWiki\SpecialPage\QueryPage;
18use MediaWiki\Title\TitleValue;
19use MediaWiki\Utils\UrlUtils;
20use stdClass;
21use Wikimedia\Rdbms\IConnectionProvider;
22use Wikimedia\Rdbms\IExpression;
23use Wikimedia\Rdbms\IReadableDatabase;
24use Wikimedia\Rdbms\IResultWrapper;
25use Wikimedia\Rdbms\LikeValue;
26
27/**
28 * Special:LinkSearch to search the external-links table.
29 *
30 * @ingroup SpecialPage
31 * @author Brooke Vibber
32 */
33class SpecialLinkSearch extends QueryPage {
34    /** @var array|bool */
35    private $mungedQuery = false;
36    /** @var string|null */
37    private $mQuery;
38    /** @var int|null */
39    private $mNs;
40    /** @var string|null */
41    private $mProt;
42
43    private function setParams( array $params ) {
44        $this->mQuery = $params['query'];
45        $this->mNs = $params['namespace'];
46        $this->mProt = $params['protocol'];
47    }
48
49    public function __construct(
50        IConnectionProvider $dbProvider,
51        LinkBatchFactory $linkBatchFactory,
52        private readonly UrlUtils $urlUtils,
53    ) {
54        parent::__construct( 'LinkSearch' );
55        $this->setDatabaseProvider( $dbProvider );
56        $this->setLinkBatchFactory( $linkBatchFactory );
57    }
58
59    /** @inheritDoc */
60    public function isCacheable() {
61        return false;
62    }
63
64    /** @inheritDoc */
65    public function execute( $par ) {
66        $this->setHeaders();
67        $this->outputHeader();
68
69        $out = $this->getOutput();
70        $out->getMetadata()->setPreventClickjacking( false );
71
72        $request = $this->getRequest();
73        $target = $request->getVal( 'target', $par ?? '' );
74        $namespace = $request->getIntOrNull( 'namespace' );
75
76        $protocols_list = [];
77        foreach ( $this->getConfig()->get( MainConfigNames::UrlProtocols ) as $prot ) {
78            if ( $prot !== '//' ) {
79                $protocols_list[] = $prot;
80            }
81        }
82
83        $target2 = Parser::normalizeLinkUrl( $target );
84        $protocol = null;
85        $bits = $this->urlUtils->parse( $target );
86        if ( isset( $bits['scheme'] ) && isset( $bits['delimiter'] ) ) {
87            $protocol = $bits['scheme'] . $bits['delimiter'];
88            // Make sure UrlUtils::parse() didn't make some well-intended correction in the protocol
89            if ( str_starts_with( strtolower( $target ), strtolower( $protocol ) ) ) {
90                $target2 = substr( $target, strlen( $protocol ) );
91            } else {
92                // If it did, let LinkFilter::makeLikeArray() handle this
93                $protocol = '';
94            }
95        }
96
97        $out->addWikiMsg(
98            'linksearch-text',
99            '<nowiki>' . $this->getLanguage()->commaList( $protocols_list ) . '</nowiki>',
100            count( $protocols_list )
101        );
102        $fields = [
103            'target' => [
104                'type' => 'text',
105                'name' => 'target',
106                'id' => 'target',
107                'size' => 50,
108                'label-message' => 'linksearch-pat',
109                'default' => $target,
110                'dir' => 'ltr',
111            ]
112        ];
113        if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
114            $fields += [
115                'namespace' => [
116                    'type' => 'namespaceselect',
117                    'name' => 'namespace',
118                    'label-message' => 'linksearch-ns',
119                    'default' => $namespace,
120                    'id' => 'namespace',
121                    'all' => '',
122                    'cssclass' => 'namespaceselector',
123                ],
124            ];
125        }
126        $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
127        $htmlForm->setSubmitTextMsg( 'linksearch-ok' );
128        $htmlForm->setWrapperLegendMsg( 'linksearch' );
129        $htmlForm->setTitle( $this->getPageTitle() );
130        $htmlForm->setMethod( 'get' );
131        $htmlForm->prepareForm()->displayForm( false );
132        $this->addHelpLink( 'Help:Linksearch' );
133
134        if ( $target != '' ) {
135            $this->setParams( [
136                'query' => $target2,
137                'namespace' => $namespace,
138                'protocol' => $protocol ] );
139            parent::execute( $par );
140            if ( $this->mungedQuery === false ) {
141                $out->addWikiMsg( 'linksearch-error' );
142            }
143        }
144        $ignoredDomains = $this->getConfig()->get( MainConfigNames::ExternalLinksIgnoreDomains );
145        if ( $ignoredDomains ) {
146            $out->addWikiMsg(
147                'linksearch-text-ignored-domains',
148                Message::listParam(
149                    array_map( static fn ( $domain ) => "<code>$domain</code>", $ignoredDomains )
150                ),
151                count( $ignoredDomains )
152            );
153        }
154    }
155
156    /**
157     * Disable RSS/Atom feeds
158     * @return bool
159     */
160    public function isSyndicated() {
161        return false;
162    }
163
164    /** @inheritDoc */
165    protected function linkParameters() {
166        $params = [];
167        $params['target'] = $this->mProt . $this->mQuery;
168        if ( $this->mNs !== null && !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
169            $params['namespace'] = $this->mNs;
170        }
171
172        return $params;
173    }
174
175    /** @inheritDoc */
176    public function getQueryInfo() {
177        $dbr = $this->getDatabaseProvider()->getReplicaDatabase( ExternalLinksTable::VIRTUAL_DOMAIN );
178
179        $field = 'el_to_domain_index';
180        $extraFields = [
181            'urldomain' => 'el_to_domain_index',
182            'urlpath' => 'el_to_path'
183        ];
184        if ( $this->mQuery === '*' && $this->mProt !== '' ) {
185            if ( $this->mProt !== null ) {
186                $this->mungedQuery = [
187                    $dbr->expr( $field, IExpression::LIKE, new LikeValue( $this->mProt, $dbr->anyString() ) ),
188                ];
189            } else {
190                $this->mungedQuery = [
191                    $dbr->expr( $field, IExpression::LIKE, new LikeValue( 'http://', $dbr->anyString() ) )
192                        ->or( $field, IExpression::LIKE, new LikeValue( 'https://', $dbr->anyString() ) ),
193                ];
194            }
195        } else {
196            $this->mungedQuery = LinkFilter::getQueryConditions( $this->mQuery, [
197                'protocol' => $this->mProt,
198                'oneWildcard' => true,
199                'db' => $dbr
200            ] );
201            if ( $this->mungedQuery === false ) {
202                // Invalid query; return no results
203                return [ 'tables' => 'page', 'fields' => 'page_id', 'conds' => '0=1' ];
204            }
205        }
206        $orderBy = [ 'el_id' ];
207
208        $retval = [
209            'tables' => [ 'page', 'externallinks' ],
210            'fields' => [
211                'namespace' => 'page_namespace',
212                'title' => 'page_title',
213                ...$extraFields,
214            ],
215            'conds' => [
216                'page_id = el_from',
217                ...$this->mungedQuery,
218            ],
219            'options' => [ 'ORDER BY' => $orderBy ]
220        ];
221
222        if ( $this->mNs !== null && !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
223            $retval['conds']['page_namespace'] = $this->mNs;
224        }
225
226        return $retval;
227    }
228
229    /**
230     * Pre-fill the link cache
231     *
232     * @param IReadableDatabase $db
233     * @param IResultWrapper $res
234     */
235    public function preprocessResults( $db, $res ) {
236        $this->executeLBFromResultWrapper( $res );
237    }
238
239    /**
240     * @param Skin $skin
241     * @param stdClass $result Result row
242     * @return string
243     */
244    public function formatResult( $skin, $result ) {
245        $title = new TitleValue( (int)$result->namespace, $result->title );
246        $pageLink = $this->getLinkRenderer()->makeLink( $title );
247        $url = LinkFilter::reverseIndexes( $result->urldomain ) . $result->urlpath;
248
249        $urlLink = $this->getLinkRenderer()->makeExternalLink( $url, $url, $this->getFullTitle() );
250
251        return $this->msg( 'linksearch-line' )->rawParams( $urlLink, $pageLink )->escaped();
252    }
253
254    /**
255     * Override to squash the ORDER BY.
256     * Not much point in descending order here.
257     * @return array
258     */
259    protected function getOrderFields() {
260        return [];
261    }
262
263    /** @inheritDoc */
264    protected function getGroupName() {
265        return 'pages';
266    }
267
268    /**
269     * enwiki complained about low limits on this special page
270     *
271     * @see T130058
272     * @todo FIXME This special page should not use LIMIT for paging
273     * @return int
274     */
275    protected function getMaxResults() {
276        return max( parent::getMaxResults(), 60000 );
277    }
278
279    /** @inheritDoc */
280    protected function getRecacheDB() {
281        return $this->getDatabaseProvider()->getReplicaDatabase(
282            ExternalLinksTable::VIRTUAL_DOMAIN,
283            'vslow'
284        );
285    }
286}
287
288// @codeCoverageIgnoreStart
289/** @deprecated class alias since 1.41 */
290class_alias( SpecialLinkSearch::class, 'SpecialLinkSearch' );
291// @codeCoverageIgnoreEnd