Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 121
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialLinkSearch
0.00% covered (danger)
0.00%
0 / 120
0.00% covered (danger)
0.00%
0 / 12
756
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 / 62
0.00% covered (danger)
0.00%
0 / 1
90
 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 / 35
0.00% covered (danger)
0.00%
0 / 1
42
 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
1<?php
2/**
3 * Implements Special:LinkSearch
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup SpecialPage
22 * @author Brooke Vibber
23 */
24
25namespace MediaWiki\Specials;
26
27use MediaWiki\Cache\LinkBatchFactory;
28use MediaWiki\ExternalLinks\LinkFilter;
29use MediaWiki\HTMLForm\HTMLForm;
30use MediaWiki\Linker\Linker;
31use MediaWiki\MainConfigNames;
32use MediaWiki\Parser\Parser;
33use MediaWiki\SpecialPage\QueryPage;
34use MediaWiki\Title\TitleValue;
35use MediaWiki\Utils\UrlUtils;
36use Skin;
37use stdClass;
38use Wikimedia\Rdbms\IConnectionProvider;
39use Wikimedia\Rdbms\IDatabase;
40use Wikimedia\Rdbms\IResultWrapper;
41
42/**
43 * Special:LinkSearch to search the external-links table.
44 * @ingroup SpecialPage
45 */
46class SpecialLinkSearch extends QueryPage {
47    /** @var array|bool */
48    private $mungedQuery = false;
49    /** @var string|null */
50    private $mQuery;
51    /** @var int|null */
52    private $mNs;
53    /** @var string|null */
54    private $mProt;
55
56    private UrlUtils $urlUtils;
57
58    private function setParams( $params ) {
59        $this->mQuery = $params['query'];
60        $this->mNs = $params['namespace'];
61        $this->mProt = $params['protocol'];
62    }
63
64    /**
65     * @param IConnectionProvider $dbProvider
66     * @param LinkBatchFactory $linkBatchFactory
67     * @param UrlUtils $urlUtils
68     */
69    public function __construct(
70        IConnectionProvider $dbProvider,
71        LinkBatchFactory $linkBatchFactory,
72        UrlUtils $urlUtils
73    ) {
74        parent::__construct( 'LinkSearch' );
75        $this->setDatabaseProvider( $dbProvider );
76        $this->setLinkBatchFactory( $linkBatchFactory );
77        $this->urlUtils = $urlUtils;
78    }
79
80    public function isCacheable() {
81        return false;
82    }
83
84    public function execute( $par ) {
85        $this->setHeaders();
86        $this->outputHeader();
87
88        $out = $this->getOutput();
89        $out->setPreventClickjacking( false );
90
91        $request = $this->getRequest();
92        $target = $request->getVal( 'target', $par ?? '' );
93        $namespace = $request->getIntOrNull( 'namespace' );
94
95        $protocols_list = [];
96        foreach ( $this->getConfig()->get( MainConfigNames::UrlProtocols ) as $prot ) {
97            if ( $prot !== '//' ) {
98                $protocols_list[] = $prot;
99            }
100        }
101
102        $target2 = Parser::normalizeLinkUrl( $target );
103        $protocol = null;
104        $bits = $this->urlUtils->parse( $target );
105        if ( isset( $bits['scheme'] ) && isset( $bits['delimiter'] ) ) {
106            $protocol = $bits['scheme'] . $bits['delimiter'];
107            // Make sure UrlUtils::parse() didn't make some well-intended correction in the protocol
108            if ( str_starts_with( strtolower( $target ), strtolower( $protocol ) ) ) {
109                $target2 = substr( $target, strlen( $protocol ) );
110            } else {
111                // If it did, let LinkFilter::makeLikeArray() handle this
112                $protocol = '';
113            }
114        }
115
116        $out->addWikiMsg(
117            'linksearch-text',
118            '<nowiki>' . $this->getLanguage()->commaList( $protocols_list ) . '</nowiki>',
119            count( $protocols_list )
120        );
121        $fields = [
122            'target' => [
123                'type' => 'text',
124                'name' => 'target',
125                'id' => 'target',
126                'size' => 50,
127                'label-message' => 'linksearch-pat',
128                'default' => $target,
129                'dir' => 'ltr',
130            ]
131        ];
132        if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
133            $fields += [
134                'namespace' => [
135                    'type' => 'namespaceselect',
136                    'name' => 'namespace',
137                    'label-message' => 'linksearch-ns',
138                    'default' => $namespace,
139                    'id' => 'namespace',
140                    'all' => '',
141                    'cssclass' => 'namespaceselector',
142                ],
143            ];
144        }
145        $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
146        $htmlForm->setSubmitTextMsg( 'linksearch-ok' );
147        $htmlForm->setWrapperLegendMsg( 'linksearch' );
148        $htmlForm->setTitle( $this->getPageTitle() );
149        $htmlForm->setMethod( 'get' );
150        $htmlForm->prepareForm()->displayForm( false );
151        $this->addHelpLink( 'Help:Linksearch' );
152
153        if ( $target != '' ) {
154            $this->setParams( [
155                'query' => $target2,
156                'namespace' => $namespace,
157                'protocol' => $protocol ] );
158            parent::execute( $par );
159            if ( $this->mungedQuery === false ) {
160                $out->addWikiMsg( 'linksearch-error' );
161            }
162        }
163    }
164
165    /**
166     * Disable RSS/Atom feeds
167     * @return bool
168     */
169    public function isSyndicated() {
170        return false;
171    }
172
173    protected function linkParameters() {
174        $params = [];
175        $params['target'] = $this->mProt . $this->mQuery;
176        if ( $this->mNs !== null && !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
177            $params['namespace'] = $this->mNs;
178        }
179
180        return $params;
181    }
182
183    public function getQueryInfo() {
184        $dbr = $this->getDatabaseProvider()->getReplicaDatabase();
185
186        $field = 'el_to_domain_index';
187        $extraFields = [
188            'urldomain' => 'el_to_domain_index',
189            'urlpath' => 'el_to_path'
190        ];
191        if ( $this->mQuery === '*' && $this->mProt !== '' ) {
192            $this->mungedQuery = [
193                $field . $dbr->buildLike( $this->mProt, $dbr->anyString() ),
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' => array_merge( [
211                'namespace' => 'page_namespace',
212                'title' => 'page_title',
213            ], $extraFields ),
214            'conds' => array_merge(
215                [
216                    'page_id = el_from',
217                ],
218                $this->mungedQuery
219            ),
220            'options' => [ 'ORDER BY' => $orderBy ]
221        ];
222
223        if ( $this->mNs !== null && !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
224            $retval['conds']['page_namespace'] = $this->mNs;
225        }
226
227        return $retval;
228    }
229
230    /**
231     * Pre-fill the link cache
232     *
233     * @param IDatabase $db
234     * @param IResultWrapper $res
235     */
236    public function preprocessResults( $db, $res ) {
237        $this->executeLBFromResultWrapper( $res );
238    }
239
240    /**
241     * @param Skin $skin
242     * @param stdClass $result Result row
243     * @return string
244     */
245    public function formatResult( $skin, $result ) {
246        $title = new TitleValue( (int)$result->namespace, $result->title );
247        $pageLink = $this->getLinkRenderer()->makeLink( $title );
248        $url = LinkFilter::reverseIndexes( $result->urldomain ) . $result->urlpath;
249
250        $urlLink = Linker::makeExternalLink( $url, $url );
251
252        return $this->msg( 'linksearch-line' )->rawParams( $urlLink, $pageLink )->escaped();
253    }
254
255    /**
256     * Override to squash the ORDER BY.
257     * Not much point in descending order here.
258     * @return array
259     */
260    protected function getOrderFields() {
261        return [];
262    }
263
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
280/** @deprecated class alias since 1.41 */
281class_alias( SpecialLinkSearch::class, 'SpecialLinkSearch' );