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\Skin\Skin;
29use MediaWiki\SpecialPage\QueryPage;
30use MediaWiki\Title\TitleValue;
31use MediaWiki\Utils\UrlUtils;
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( array $params ) {
58        $this->mQuery = $params['query'];
59        $this->mNs = $params['namespace'];
60        $this->mProt = $params['protocol'];
61    }
62
63    public function __construct(
64        IConnectionProvider $dbProvider,
65        LinkBatchFactory $linkBatchFactory,
66        UrlUtils $urlUtils
67    ) {
68        parent::__construct( 'LinkSearch' );
69        $this->setDatabaseProvider( $dbProvider );
70        $this->setLinkBatchFactory( $linkBatchFactory );
71        $this->urlUtils = $urlUtils;
72    }
73
74    public function isCacheable() {
75        return false;
76    }
77
78    public function execute( $par ) {
79        $this->setHeaders();
80        $this->outputHeader();
81
82        $out = $this->getOutput();
83        $out->getMetadata()->setPreventClickjacking( false );
84
85        $request = $this->getRequest();
86        $target = $request->getVal( 'target', $par ?? '' );
87        $namespace = $request->getIntOrNull( 'namespace' );
88
89        $protocols_list = [];
90        foreach ( $this->getConfig()->get( MainConfigNames::UrlProtocols ) as $prot ) {
91            if ( $prot !== '//' ) {
92                $protocols_list[] = $prot;
93            }
94        }
95
96        $target2 = Parser::normalizeLinkUrl( $target );
97        $protocol = null;
98        $bits = $this->urlUtils->parse( $target );
99        if ( isset( $bits['scheme'] ) && isset( $bits['delimiter'] ) ) {
100            $protocol = $bits['scheme'] . $bits['delimiter'];
101            // Make sure UrlUtils::parse() didn't make some well-intended correction in the protocol
102            if ( str_starts_with( strtolower( $target ), strtolower( $protocol ) ) ) {
103                $target2 = substr( $target, strlen( $protocol ) );
104            } else {
105                // If it did, let LinkFilter::makeLikeArray() handle this
106                $protocol = '';
107            }
108        }
109
110        $out->addWikiMsg(
111            'linksearch-text',
112            '<nowiki>' . $this->getLanguage()->commaList( $protocols_list ) . '</nowiki>',
113            count( $protocols_list )
114        );
115        $fields = [
116            'target' => [
117                'type' => 'text',
118                'name' => 'target',
119                'id' => 'target',
120                'size' => 50,
121                'label-message' => 'linksearch-pat',
122                'default' => $target,
123                'dir' => 'ltr',
124            ]
125        ];
126        if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
127            $fields += [
128                'namespace' => [
129                    'type' => 'namespaceselect',
130                    'name' => 'namespace',
131                    'label-message' => 'linksearch-ns',
132                    'default' => $namespace,
133                    'id' => 'namespace',
134                    'all' => '',
135                    'cssclass' => 'namespaceselector',
136                ],
137            ];
138        }
139        $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
140        $htmlForm->setSubmitTextMsg( 'linksearch-ok' );
141        $htmlForm->setWrapperLegendMsg( 'linksearch' );
142        $htmlForm->setTitle( $this->getPageTitle() );
143        $htmlForm->setMethod( 'get' );
144        $htmlForm->prepareForm()->displayForm( false );
145        $this->addHelpLink( 'Help:Linksearch' );
146
147        if ( $target != '' ) {
148            $this->setParams( [
149                'query' => $target2,
150                'namespace' => $namespace,
151                'protocol' => $protocol ] );
152            parent::execute( $par );
153            if ( $this->mungedQuery === false ) {
154                $out->addWikiMsg( 'linksearch-error' );
155            }
156        }
157    }
158
159    /**
160     * Disable RSS/Atom feeds
161     * @return bool
162     */
163    public function isSyndicated() {
164        return false;
165    }
166
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    public function getQueryInfo() {
178        $dbr = $this->getDatabaseProvider()->getReplicaDatabase();
179
180        $field = 'el_to_domain_index';
181        $extraFields = [
182            'urldomain' => 'el_to_domain_index',
183            'urlpath' => 'el_to_path'
184        ];
185        if ( $this->mQuery === '*' && $this->mProt !== '' ) {
186            if ( $this->mProt !== null ) {
187                $this->mungedQuery = [
188                    $dbr->expr( $field, IExpression::LIKE, new LikeValue( $this->mProt, $dbr->anyString() ) ),
189                ];
190            } else {
191                $this->mungedQuery = [
192                    $dbr->expr( $field, IExpression::LIKE, new LikeValue( 'http://', $dbr->anyString() ) )
193                        ->or( $field, IExpression::LIKE, new LikeValue( 'https://', $dbr->anyString() ) ),
194                ];
195            }
196        } else {
197            $this->mungedQuery = LinkFilter::getQueryConditions( $this->mQuery, [
198                'protocol' => $this->mProt,
199                'oneWildcard' => true,
200                'db' => $dbr
201            ] );
202            if ( $this->mungedQuery === false ) {
203                // Invalid query; return no results
204                return [ 'tables' => 'page', 'fields' => 'page_id', 'conds' => '0=1' ];
205            }
206        }
207        $orderBy = [ 'el_id' ];
208
209        $retval = [
210            'tables' => [ 'page', 'externallinks' ],
211            'fields' => array_merge( [
212                'namespace' => 'page_namespace',
213                'title' => 'page_title',
214            ], $extraFields ),
215            'conds' => array_merge(
216                [
217                    'page_id = el_from',
218                ],
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 IDatabase $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    protected function getGroupName() {
266        return 'pages';
267    }
268
269    /**
270     * enwiki complained about low limits on this special page
271     *
272     * @see T130058
273     * @todo FIXME This special page should not use LIMIT for paging
274     * @return int
275     */
276    protected function getMaxResults() {
277        return max( parent::getMaxResults(), 60000 );
278    }
279}
280
281/** @deprecated class alias since 1.41 */
282class_alias( SpecialLinkSearch::class, 'SpecialLinkSearch' );