Merge ".mailmap: Correct two contributor names"
[mediawiki.git] / includes / specials / SpecialLinkSearch.php
blob8da3d814737a5b3f20fbcd74c5b9c85c5d17dac5
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.
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.
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
18 * @file
21 namespace MediaWiki\Specials;
23 use MediaWiki\Cache\LinkBatchFactory;
24 use MediaWiki\ExternalLinks\LinkFilter;
25 use MediaWiki\HTMLForm\HTMLForm;
26 use MediaWiki\MainConfigNames;
27 use MediaWiki\Parser\Parser;
28 use MediaWiki\SpecialPage\QueryPage;
29 use MediaWiki\Title\TitleValue;
30 use MediaWiki\Utils\UrlUtils;
31 use Skin;
32 use stdClass;
33 use Wikimedia\Rdbms\IConnectionProvider;
34 use Wikimedia\Rdbms\IDatabase;
35 use Wikimedia\Rdbms\IExpression;
36 use Wikimedia\Rdbms\IResultWrapper;
37 use Wikimedia\Rdbms\LikeValue;
39 /**
40 * Special:LinkSearch to search the external-links table.
42 * @ingroup SpecialPage
43 * @author Brooke Vibber
45 class 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;
55 private UrlUtils $urlUtils;
57 private function setParams( $params ) {
58 $this->mQuery = $params['query'];
59 $this->mNs = $params['namespace'];
60 $this->mProt = $params['protocol'];
63 /**
64 * @param IConnectionProvider $dbProvider
65 * @param LinkBatchFactory $linkBatchFactory
66 * @param UrlUtils $urlUtils
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;
79 public function isCacheable() {
80 return false;
83 public function execute( $par ) {
84 $this->setHeaders();
85 $this->outputHeader();
87 $out = $this->getOutput();
88 $out->getMetadata()->setPreventClickjacking( false );
90 $request = $this->getRequest();
91 $target = $request->getVal( 'target', $par ?? '' );
92 $namespace = $request->getIntOrNull( 'namespace' );
94 $protocols_list = [];
95 foreach ( $this->getConfig()->get( MainConfigNames::UrlProtocols ) as $prot ) {
96 if ( $prot !== '//' ) {
97 $protocols_list[] = $prot;
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 = '';
115 $out->addWikiMsg(
116 'linksearch-text',
117 '<nowiki>' . $this->getLanguage()->commaList( $protocols_list ) . '</nowiki>',
118 count( $protocols_list )
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',
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',
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' );
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' );
165 * Disable RSS/Atom feeds
166 * @return bool
168 public function isSyndicated() {
169 return false;
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;
179 return $params;
182 public function getQueryInfo() {
183 $dbr = $this->getDatabaseProvider()->getReplicaDatabase();
185 $field = 'el_to_domain_index';
186 $extraFields = [
187 'urldomain' => 'el_to_domain_index',
188 'urlpath' => 'el_to_path'
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() ) ),
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() ) ),
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' ];
212 $orderBy = [ 'el_id' ];
214 $retval = [
215 'tables' => [ 'page', 'externallinks' ],
216 'fields' => array_merge( [
217 'namespace' => 'page_namespace',
218 'title' => 'page_title',
219 ], $extraFields ),
220 'conds' => array_merge(
222 'page_id = el_from',
224 $this->mungedQuery
226 'options' => [ 'ORDER BY' => $orderBy ]
229 if ( $this->mNs !== null && !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
230 $retval['conds']['page_namespace'] = $this->mNs;
233 return $retval;
237 * Pre-fill the link cache
239 * @param IDatabase $db
240 * @param IResultWrapper $res
242 public function preprocessResults( $db, $res ) {
243 $this->executeLBFromResultWrapper( $res );
247 * @param Skin $skin
248 * @param stdClass $result Result row
249 * @return string
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;
256 $urlLink = $this->getLinkRenderer()->makeExternalLink( $url, $url, $this->getFullTitle() );
258 return $this->msg( 'linksearch-line' )->rawParams( $urlLink, $pageLink )->escaped();
262 * Override to squash the ORDER BY.
263 * Not much point in descending order here.
264 * @return array
266 protected function getOrderFields() {
267 return [];
270 protected function getGroupName() {
271 return 'pages';
275 * enwiki complained about low limits on this special page
277 * @see T130058
278 * @todo FIXME This special page should not use LIMIT for paging
279 * @return int
281 protected function getMaxResults() {
282 return max( parent::getMaxResults(), 60000 );
286 /** @deprecated class alias since 1.41 */
287 class_alias( SpecialLinkSearch::class, 'SpecialLinkSearch' );