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