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