Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 141
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryLinks
0.00% covered (danger)
0.00%
0 / 140
0.00% covered (danger)
0.00%
0 / 8
1332
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 execute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCacheMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 executeGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 1
702
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\Deferred\LinksUpdate\PageLinksTable;
12use MediaWiki\Deferred\LinksUpdate\TemplateLinksTable;
13use MediaWiki\Linker\LinksMigration;
14use MediaWiki\Page\LinkBatchFactory;
15use MediaWiki\ParamValidator\TypeDef\NamespaceDef;
16use MediaWiki\Title\Title;
17use Wikimedia\ParamValidator\ParamValidator;
18use Wikimedia\ParamValidator\TypeDef\IntegerDef;
19
20/**
21 * A query module to list all wiki links on a given set of pages.
22 *
23 * @ingroup API
24 */
25class ApiQueryLinks extends ApiQueryGeneratorBase {
26
27    private const LINKS = 'links';
28    private const TEMPLATES = 'templates';
29
30    private string $table;
31    private string $prefix;
32    private string $titlesParam;
33    private string $helpUrl;
34    private ?string $virtualdomain;
35
36    public function __construct(
37        ApiQuery $query,
38        string $moduleName,
39        private readonly LinkBatchFactory $linkBatchFactory,
40        private readonly LinksMigration $linksMigration,
41    ) {
42        switch ( $moduleName ) {
43            case self::LINKS:
44                $this->table = 'pagelinks';
45                $this->prefix = 'pl';
46                $this->titlesParam = 'titles';
47                $this->helpUrl = 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Links';
48                $this->virtualdomain = PageLinksTable::VIRTUAL_DOMAIN;
49                break;
50            case self::TEMPLATES:
51                $this->table = 'templatelinks';
52                $this->prefix = 'tl';
53                $this->titlesParam = 'templates';
54                $this->helpUrl = 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Templates';
55                $this->virtualdomain = TemplateLinksTable::VIRTUAL_DOMAIN;
56                break;
57            default:
58                ApiBase::dieDebug( __METHOD__, 'Unknown module name' );
59        }
60
61        parent::__construct( $query, $moduleName, $this->prefix );
62    }
63
64    public function execute() {
65        $this->run();
66    }
67
68    /** @inheritDoc */
69    public function getCacheMode( $params ) {
70        return 'public';
71    }
72
73    /** @inheritDoc */
74    public function executeGenerator( $resultPageSet ) {
75        $this->run( $resultPageSet );
76    }
77
78    /**
79     * @param ApiPageSet|null $resultPageSet
80     */
81    private function run( $resultPageSet = null ) {
82        $pages = $this->getPageSet()->getGoodPages();
83        if ( $pages === [] ) {
84            return; // nothing to do
85        }
86
87        $params = $this->extractRequestParams();
88
89        $this->setVirtualDomain( $this->virtualdomain );
90
91        if ( isset( $this->linksMigration::$mapping[$this->table] ) ) {
92            [ $nsField, $titleField ] = $this->linksMigration->getTitleFields( $this->table );
93            $queryInfo = $this->linksMigration->getQueryInfo( $this->table );
94            $this->addTables( $queryInfo['tables'] );
95            $this->addJoinConds( $queryInfo['joins'] );
96        } else {
97            $this->addTables( $this->table );
98            $nsField = $this->prefix . '_namespace';
99            $titleField = $this->prefix . '_title';
100        }
101
102        $this->addFields( [
103            'pl_from' => $this->prefix . '_from',
104            'pl_namespace' => $nsField,
105            'pl_title' => $titleField,
106        ] );
107
108        $this->addWhereFld( $this->prefix . '_from', array_keys( $pages ) );
109
110        $multiNS = true;
111        $multiTitle = true;
112        if ( $params[$this->titlesParam] ) {
113            // Filter the titles in PHP so our ORDER BY bug avoidance below works right.
114            $filterNS = $params['namespace'] ? array_fill_keys( $params['namespace'], true ) : false;
115
116            $lb = $this->linkBatchFactory->newLinkBatch();
117            foreach ( $params[$this->titlesParam] as $t ) {
118                $title = Title::newFromText( $t );
119                if ( !$title || $title->isExternal() ) {
120                    $this->addWarning( [ 'apiwarn-invalidtitle', wfEscapeWikiText( $t ) ] );
121                } elseif ( !$filterNS || isset( $filterNS[$title->getNamespace()] ) ) {
122                    $lb->addObj( $title );
123                }
124            }
125            if ( $lb->isEmpty() ) {
126                // No titles, no results!
127                return;
128            }
129            $cond = $lb->constructSet( $this->prefix, $this->getDB() );
130            $this->addWhere( $cond );
131            $multiNS = count( $lb->data ) !== 1;
132            $multiTitle = count( array_merge( ...$lb->data ) ) !== 1;
133        } elseif ( $params['namespace'] ) {
134            $this->addWhereFld( $nsField, $params['namespace'] );
135            $multiNS = $params['namespace'] === null || count( $params['namespace'] ) !== 1;
136        }
137
138        if ( $params['continue'] !== null ) {
139            $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int', 'string' ] );
140            $op = $params['dir'] == 'descending' ? '<=' : '>=';
141            $this->addWhere( $this->getDB()->buildComparison( $op, [
142                "{$this->prefix}_from" => $cont[0],
143                $nsField => $cont[1],
144                $titleField => $cont[2],
145            ] ) );
146        }
147
148        $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
149        // Here's some MySQL craziness going on: if you use WHERE foo='bar'
150        // and later ORDER BY foo MySQL doesn't notice the ORDER BY is pointless
151        // but instead goes and filesorts, because the index for foo was used
152        // already. To work around this, we drop constant fields in the WHERE
153        // clause from the ORDER BY clause
154        $order = [];
155        if ( count( $pages ) !== 1 ) {
156            $order[] = $this->prefix . '_from' . $sort;
157        }
158        if ( $multiNS ) {
159            $order[] = $nsField . $sort;
160        }
161        if ( $multiTitle ) {
162            $order[] = $titleField . $sort;
163        }
164        if ( $order ) {
165            $this->addOption( 'ORDER BY', $order );
166        }
167        $this->addOption( 'LIMIT', $params['limit'] + 1 );
168
169        $res = $this->select( __METHOD__ );
170
171        if ( $resultPageSet === null ) {
172            $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'pl' );
173
174            $count = 0;
175            foreach ( $res as $row ) {
176                if ( ++$count > $params['limit'] ) {
177                    // We've reached the one extra which shows that
178                    // there are additional pages to be had. Stop here...
179                    $this->setContinueEnumParameter( 'continue',
180                        "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" );
181                    break;
182                }
183                $vals = [];
184                ApiQueryBase::addTitleInfo( $vals, Title::makeTitle( $row->pl_namespace, $row->pl_title ) );
185                $fit = $this->addPageSubItem( $row->pl_from, $vals );
186                if ( !$fit ) {
187                    $this->setContinueEnumParameter( 'continue',
188                        "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" );
189                    break;
190                }
191            }
192        } else {
193            $titles = [];
194            $count = 0;
195            foreach ( $res as $row ) {
196                if ( ++$count > $params['limit'] ) {
197                    // We've reached the one extra which shows that
198                    // there are additional pages to be had. Stop here...
199                    $this->setContinueEnumParameter( 'continue',
200                        "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" );
201                    break;
202                }
203                $titles[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
204            }
205            $resultPageSet->populateFromTitles( $titles );
206        }
207    }
208
209    /** @inheritDoc */
210    public function getAllowedParams() {
211        return [
212            'namespace' => [
213                ParamValidator::PARAM_TYPE => 'namespace',
214                ParamValidator::PARAM_ISMULTI => true,
215                NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
216            ],
217            'limit' => [
218                ParamValidator::PARAM_DEFAULT => 10,
219                ParamValidator::PARAM_TYPE => 'limit',
220                IntegerDef::PARAM_MIN => 1,
221                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
222                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
223            ],
224            'continue' => [
225                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
226            ],
227            $this->titlesParam => [
228                ParamValidator::PARAM_ISMULTI => true,
229            ],
230            'dir' => [
231                ParamValidator::PARAM_DEFAULT => 'ascending',
232                ParamValidator::PARAM_TYPE => [
233                    'ascending',
234                    'descending'
235                ]
236            ],
237        ];
238    }
239
240    /** @inheritDoc */
241    protected function getExamplesMessages() {
242        $name = $this->getModuleName();
243        $path = $this->getModulePath();
244        $title = Title::newMainPage()->getPrefixedText();
245        $mp = rawurlencode( $title );
246
247        return [
248            "action=query&prop={$name}&titles={$mp}"
249                => "apihelp-{$path}-example-simple",
250            "action=query&generator={$name}&titles={$mp}&prop=info"
251                => "apihelp-{$path}-example-generator",
252            "action=query&prop={$name}&titles={$mp}&{$this->prefix}namespace=2|10"
253                => "apihelp-{$path}-example-namespaces",
254        ];
255    }
256
257    /** @inheritDoc */
258    public function getHelpUrls() {
259        return $this->helpUrl;
260    }
261}
262
263/** @deprecated class alias since 1.43 */
264class_alias( ApiQueryLinks::class, 'ApiQueryLinks' );