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