Merge "Convert Special:DeletedContributions to use OOUI."
[lhc/web/wiklou.git] / includes / api / ApiParse.php
1 <?php
2 /**
3 * Created on Dec 01, 2007
4 *
5 * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * http://www.gnu.org/copyleft/gpl.html
21 *
22 * @file
23 */
24
25 /**
26 * @ingroup API
27 */
28 class ApiParse extends ApiBase {
29
30 /** @var string $section */
31 private $section = null;
32
33 /** @var Content $content */
34 private $content = null;
35
36 /** @var Content $pstContent */
37 private $pstContent = null;
38
39 private function checkReadPermissions( Title $title ) {
40 if ( !$title->userCan( 'read', $this->getUser() ) ) {
41 $this->dieUsage( "You don't have permission to view this page", 'permissiondenied' );
42 }
43 }
44
45 public function execute() {
46 // The data is hot but user-dependent, like page views, so we set vary cookies
47 $this->getMain()->setCacheMode( 'anon-public-user-private' );
48
49 // Get parameters
50 $params = $this->extractRequestParams();
51 $text = $params['text'];
52 $title = $params['title'];
53 if ( $title === null ) {
54 $titleProvided = false;
55 // A title is needed for parsing, so arbitrarily choose one
56 $title = 'API';
57 } else {
58 $titleProvided = true;
59 }
60
61 $page = $params['page'];
62 $pageid = $params['pageid'];
63 $oldid = $params['oldid'];
64
65 $model = $params['contentmodel'];
66 $format = $params['contentformat'];
67
68 if ( !is_null( $page ) && ( !is_null( $text ) || $titleProvided ) ) {
69 $this->dieUsage(
70 'The page parameter cannot be used together with the text and title parameters',
71 'params'
72 );
73 }
74
75 $prop = array_flip( $params['prop'] );
76
77 if ( isset( $params['section'] ) ) {
78 $this->section = $params['section'];
79 if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) {
80 $this->dieUsage(
81 'The section parameter must be a valid section id or "new"', 'invalidsection'
82 );
83 }
84 } else {
85 $this->section = false;
86 }
87
88 // The parser needs $wgTitle to be set, apparently the
89 // $title parameter in Parser::parse isn't enough *sigh*
90 // TODO: Does this still need $wgTitle?
91 global $wgParser, $wgTitle;
92
93 $redirValues = null;
94
95 // Return result
96 $result = $this->getResult();
97
98 if ( !is_null( $oldid ) || !is_null( $pageid ) || !is_null( $page ) ) {
99 if ( $this->section === 'new' ) {
100 $this->dieUsage(
101 'section=new cannot be combined with oldid, pageid or page parameters. ' .
102 'Please use text', 'params'
103 );
104 }
105 if ( !is_null( $oldid ) ) {
106 // Don't use the parser cache
107 $rev = Revision::newFromId( $oldid );
108 if ( !$rev ) {
109 $this->dieUsage( "There is no revision ID $oldid", 'missingrev' );
110 }
111
112 $this->checkReadPermissions( $rev->getTitle() );
113 if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
114 $this->dieUsage( "You don't have permission to view deleted revisions", 'permissiondenied' );
115 }
116
117 $titleObj = $rev->getTitle();
118 $wgTitle = $titleObj;
119 $pageObj = WikiPage::factory( $titleObj );
120 list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params );
121
122 // If for some reason the "oldid" is actually the current revision, it may be cached
123 // Deliberately comparing $pageObj->getLatest() with $rev->getId(), rather than
124 // checking $rev->isCurrent(), because $pageObj is what actually ends up being used,
125 // and if its ->getLatest() is outdated, $rev->isCurrent() won't tell us that.
126 if ( !$suppressCache && $rev->getId() == $pageObj->getLatest() ) {
127 // May get from/save to parser cache
128 $p_result = $this->getParsedContent( $pageObj, $popts,
129 $pageid, isset( $prop['wikitext'] ) );
130 } else { // This is an old revision, so get the text differently
131 $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
132
133 if ( $this->section !== false ) {
134 $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() );
135 }
136
137 // Should we save old revision parses to the parser cache?
138 $p_result = $this->content->getParserOutput( $titleObj, $rev->getId(), $popts );
139 }
140 } else { // Not $oldid, but $pageid or $page
141 if ( $params['redirects'] ) {
142 $reqParams = [
143 'redirects' => '',
144 ];
145 if ( !is_null( $pageid ) ) {
146 $reqParams['pageids'] = $pageid;
147 } else { // $page
148 $reqParams['titles'] = $page;
149 }
150 $req = new FauxRequest( $reqParams );
151 $main = new ApiMain( $req );
152 $pageSet = new ApiPageSet( $main );
153 $pageSet->execute();
154 $redirValues = $pageSet->getRedirectTitlesAsResult( $this->getResult() );
155
156 $to = $page;
157 foreach ( $pageSet->getRedirectTitles() as $title ) {
158 $to = $title->getFullText();
159 }
160 $pageParams = [ 'title' => $to ];
161 } elseif ( !is_null( $pageid ) ) {
162 $pageParams = [ 'pageid' => $pageid ];
163 } else { // $page
164 $pageParams = [ 'title' => $page ];
165 }
166
167 $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
168 $titleObj = $pageObj->getTitle();
169 if ( !$titleObj || !$titleObj->exists() ) {
170 $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
171 }
172
173 $this->checkReadPermissions( $titleObj );
174 $wgTitle = $titleObj;
175
176 if ( isset( $prop['revid'] ) ) {
177 $oldid = $pageObj->getLatest();
178 }
179
180 list( $popts, $reset, $suppressCache ) = $this->makeParserOptions( $pageObj, $params );
181
182 // Don't pollute the parser cache when setting options that aren't
183 // in ParserOptions::optionsHash()
184 /// @todo: This should be handled closer to the actual cache instead of here, see T110269
185 $suppressCache = $suppressCache ||
186 $params['disablepp'] ||
187 $params['disablelimitreport'] ||
188 $params['preview'] ||
189 $params['sectionpreview'] ||
190 $params['disabletidy'];
191
192 if ( $suppressCache ) {
193 $this->content = $this->getContent( $pageObj, $pageid );
194 $p_result = $this->content->getParserOutput( $titleObj, null, $popts );
195 } else {
196 // Potentially cached
197 $p_result = $this->getParsedContent( $pageObj, $popts, $pageid,
198 isset( $prop['wikitext'] ) );
199 }
200 }
201 } else { // Not $oldid, $pageid, $page. Hence based on $text
202 $titleObj = Title::newFromText( $title );
203 if ( !$titleObj || $titleObj->isExternal() ) {
204 $this->dieUsageMsg( [ 'invalidtitle', $title ] );
205 }
206 $wgTitle = $titleObj;
207 if ( $titleObj->canExist() ) {
208 $pageObj = WikiPage::factory( $titleObj );
209 } else {
210 // Do like MediaWiki::initializeArticle()
211 $article = Article::newFromTitle( $titleObj, $this->getContext() );
212 $pageObj = $article->getPage();
213 }
214
215 list( $popts, $reset ) = $this->makeParserOptions( $pageObj, $params );
216 $textProvided = !is_null( $text );
217
218 if ( !$textProvided ) {
219 if ( $titleProvided && ( $prop || $params['generatexml'] ) ) {
220 $this->setWarning(
221 "'title' used without 'text', and parsed page properties were requested " .
222 "(did you mean to use 'page' instead of 'title'?)"
223 );
224 }
225 // Prevent warning from ContentHandler::makeContent()
226 $text = '';
227 }
228
229 // If we are parsing text, do not use the content model of the default
230 // API title, but default to wikitext to keep BC.
231 if ( $textProvided && !$titleProvided && is_null( $model ) ) {
232 $model = CONTENT_MODEL_WIKITEXT;
233 $this->setWarning( "No 'title' or 'contentmodel' was given, assuming $model." );
234 }
235
236 try {
237 $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
238 } catch ( MWContentSerializationException $ex ) {
239 $this->dieUsage( $ex->getMessage(), 'parseerror' );
240 }
241
242 if ( $this->section !== false ) {
243 if ( $this->section === 'new' ) {
244 // Insert the section title above the content.
245 if ( !is_null( $params['sectiontitle'] ) && $params['sectiontitle'] !== '' ) {
246 $this->content = $this->content->addSectionHeader( $params['sectiontitle'] );
247 }
248 } else {
249 $this->content = $this->getSectionContent( $this->content, $titleObj->getPrefixedText() );
250 }
251 }
252
253 if ( $params['pst'] || $params['onlypst'] ) {
254 $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts );
255 }
256 if ( $params['onlypst'] ) {
257 // Build a result and bail out
258 $result_array = [];
259 $result_array['text'] = $this->pstContent->serialize( $format );
260 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
261 if ( isset( $prop['wikitext'] ) ) {
262 $result_array['wikitext'] = $this->content->serialize( $format );
263 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
264 }
265 if ( !is_null( $params['summary'] ) ||
266 ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
267 ) {
268 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
269 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
270 }
271
272 $result->addValue( null, $this->getModuleName(), $result_array );
273
274 return;
275 }
276
277 // Not cached (save or load)
278 if ( $params['pst'] ) {
279 $p_result = $this->pstContent->getParserOutput( $titleObj, null, $popts );
280 } else {
281 $p_result = $this->content->getParserOutput( $titleObj, null, $popts );
282 }
283 }
284
285 $result_array = [];
286
287 $result_array['title'] = $titleObj->getPrefixedText();
288 $result_array['pageid'] = $pageid ?: $pageObj->getId();
289
290 if ( !is_null( $oldid ) ) {
291 $result_array['revid'] = intval( $oldid );
292 }
293
294 if ( $params['redirects'] && !is_null( $redirValues ) ) {
295 $result_array['redirects'] = $redirValues;
296 }
297
298 if ( $params['disabletoc'] ) {
299 $p_result->setTOCEnabled( false );
300 }
301
302 if ( isset( $prop['text'] ) ) {
303 $result_array['text'] = $p_result->getText();
304 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'text';
305 }
306
307 if ( !is_null( $params['summary'] ) ||
308 ( !is_null( $params['sectiontitle'] ) && $this->section === 'new' )
309 ) {
310 $result_array['parsedsummary'] = $this->formatSummary( $titleObj, $params );
311 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsedsummary';
312 }
313
314 if ( isset( $prop['langlinks'] ) ) {
315 $langlinks = $p_result->getLanguageLinks();
316
317 if ( $params['effectivelanglinks'] ) {
318 // Link flags are ignored for now, but may in the future be
319 // included in the result.
320 $linkFlags = [];
321 Hooks::run( 'LanguageLinks', [ $titleObj, &$langlinks, &$linkFlags ] );
322 }
323 } else {
324 $langlinks = false;
325 }
326
327 if ( isset( $prop['langlinks'] ) ) {
328 $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
329 }
330 if ( isset( $prop['categories'] ) ) {
331 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
332 }
333 if ( isset( $prop['categorieshtml'] ) ) {
334 $result_array['categorieshtml'] = $this->categoriesHtml( $p_result->getCategories() );
335 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'categorieshtml';
336 }
337 if ( isset( $prop['links'] ) ) {
338 $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
339 }
340 if ( isset( $prop['templates'] ) ) {
341 $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
342 }
343 if ( isset( $prop['images'] ) ) {
344 $result_array['images'] = array_keys( $p_result->getImages() );
345 }
346 if ( isset( $prop['externallinks'] ) ) {
347 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
348 }
349 if ( isset( $prop['sections'] ) ) {
350 $result_array['sections'] = $p_result->getSections();
351 }
352
353 if ( isset( $prop['displaytitle'] ) ) {
354 $result_array['displaytitle'] = $p_result->getDisplayTitle() ?:
355 $titleObj->getPrefixedText();
356 }
357
358 if ( isset( $prop['headitems'] ) ) {
359 $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
360 $this->logFeatureUsage( 'action=parse&prop=headitems' );
361 $this->setWarning( 'headitems is deprecated since MediaWiki 1.28. '
362 . 'Use prop=headhtml when creating new HTML documents, or '
363 . 'prop=modules|jsconfigvars when updating a document client-side.' );
364 }
365
366 if ( isset( $prop['headhtml'] ) ) {
367 $context = new DerivativeContext( $this->getContext() );
368 $context->setTitle( $titleObj );
369 $context->setWikiPage( $pageObj );
370
371 // We need an OutputPage tied to $context, not to the
372 // RequestContext at the root of the stack.
373 $output = new OutputPage( $context );
374 $output->addParserOutputMetadata( $p_result );
375
376 $result_array['headhtml'] = $output->headElement( $context->getSkin() );
377 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
378 }
379
380 if ( isset( $prop['modules'] ) ) {
381 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
382 $result_array['modulescripts'] = array_values( array_unique( $p_result->getModuleScripts() ) );
383 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
384 }
385
386 if ( isset( $prop['jsconfigvars'] ) ) {
387 $result_array['jsconfigvars'] =
388 ApiResult::addMetadataToResultVars( $p_result->getJsConfigVars() );
389 }
390
391 if ( isset( $prop['encodedjsconfigvars'] ) ) {
392 $result_array['encodedjsconfigvars'] = FormatJson::encode(
393 $p_result->getJsConfigVars(), false, FormatJson::ALL_OK
394 );
395 $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
396 }
397
398 if ( isset( $prop['modules'] ) &&
399 !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
400 $this->setWarning( 'Property "modules" was set but not "jsconfigvars" ' .
401 'or "encodedjsconfigvars". Configuration variables are necessary ' .
402 'for proper module usage.' );
403 }
404
405 if ( isset( $prop['indicators'] ) ) {
406 $result_array['indicators'] = (array)$p_result->getIndicators();
407 ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
408 }
409
410 if ( isset( $prop['iwlinks'] ) ) {
411 $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
412 }
413
414 if ( isset( $prop['wikitext'] ) ) {
415 $result_array['wikitext'] = $this->content->serialize( $format );
416 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
417 if ( !is_null( $this->pstContent ) ) {
418 $result_array['psttext'] = $this->pstContent->serialize( $format );
419 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
420 }
421 }
422 if ( isset( $prop['properties'] ) ) {
423 $result_array['properties'] = (array)$p_result->getProperties();
424 ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
425 }
426
427 if ( isset( $prop['limitreportdata'] ) ) {
428 $result_array['limitreportdata'] =
429 $this->formatLimitReportData( $p_result->getLimitReportData() );
430 }
431 if ( isset( $prop['limitreporthtml'] ) ) {
432 $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
433 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
434 }
435
436 if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
437 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
438 $this->dieUsage( 'parsetree is only supported for wikitext content', 'notwikitext' );
439 }
440
441 $wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
442 $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
443 if ( is_callable( [ $dom, 'saveXML' ] ) ) {
444 $xml = $dom->saveXML();
445 } else {
446 $xml = $dom->__toString();
447 }
448 $result_array['parsetree'] = $xml;
449 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
450 }
451
452 $result_mapping = [
453 'redirects' => 'r',
454 'langlinks' => 'll',
455 'categories' => 'cl',
456 'links' => 'pl',
457 'templates' => 'tl',
458 'images' => 'img',
459 'externallinks' => 'el',
460 'iwlinks' => 'iw',
461 'sections' => 's',
462 'headitems' => 'hi',
463 'modules' => 'm',
464 'indicators' => 'ind',
465 'modulescripts' => 'm',
466 'modulestyles' => 'm',
467 'properties' => 'pp',
468 'limitreportdata' => 'lr',
469 ];
470 $this->setIndexedTagNames( $result_array, $result_mapping );
471 $result->addValue( null, $this->getModuleName(), $result_array );
472 }
473
474 /**
475 * Constructs a ParserOptions object
476 *
477 * @param WikiPage $pageObj
478 * @param array $params
479 *
480 * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
481 */
482 protected function makeParserOptions( WikiPage $pageObj, array $params ) {
483 $popts = $pageObj->makeParserOptions( $this->getContext() );
484 $popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
485 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
486 $popts->setIsSectionPreview( $params['sectionpreview'] );
487 $popts->setEditSection( !$params['disableeditsection'] );
488 if ( $params['disabletidy'] ) {
489 $popts->setTidy( false );
490 }
491
492 $reset = null;
493 $suppressCache = false;
494 Hooks::run( 'ApiMakeParserOptions',
495 [ $popts, $pageObj->getTitle(), $params, $this, &$reset, &$suppressCache ] );
496
497 return [ $popts, $reset, $suppressCache ];
498 }
499
500 /**
501 * @param WikiPage $page
502 * @param ParserOptions $popts
503 * @param int $pageId
504 * @param bool $getWikitext
505 * @return ParserOutput
506 */
507 private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) {
508 $this->content = $this->getContent( $page, $pageId );
509
510 if ( $this->section !== false && $this->content !== null ) {
511 // Not cached (save or load)
512 return $this->content->getParserOutput( $page->getTitle(), null, $popts );
513 }
514
515 // Try the parser cache first
516 // getParserOutput will save to Parser cache if able
517 $pout = $page->getParserOutput( $popts );
518 if ( !$pout ) {
519 $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
520 }
521 if ( $getWikitext ) {
522 $this->content = $page->getContent( Revision::RAW );
523 }
524
525 return $pout;
526 }
527
528 /**
529 * Get the content for the given page and the requested section.
530 *
531 * @param WikiPage $page
532 * @param int $pageId
533 * @return Content
534 */
535 private function getContent( WikiPage $page, $pageId = null ) {
536 $content = $page->getContent( Revision::RAW ); // XXX: really raw?
537
538 if ( $this->section !== false && $content !== null ) {
539 $content = $this->getSectionContent(
540 $content,
541 !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText()
542 );
543 }
544 return $content;
545 }
546
547 /**
548 * Extract the requested section from the given Content
549 *
550 * @param Content $content
551 * @param string $what Identifies the content in error messages, e.g. page title.
552 * @return Content|bool
553 */
554 private function getSectionContent( Content $content, $what ) {
555 // Not cached (save or load)
556 $section = $content->getSection( $this->section );
557 if ( $section === false ) {
558 $this->dieUsage( "There is no section {$this->section} in $what", 'nosuchsection' );
559 }
560 if ( $section === null ) {
561 $this->dieUsage( "Sections are not supported by $what", 'nosuchsection' );
562 $section = false;
563 }
564
565 return $section;
566 }
567
568 /**
569 * This mimicks the behavior of EditPage in formatting a summary
570 *
571 * @param Title $title of the page being parsed
572 * @param Array $params the API parameters of the request
573 * @return Content|bool
574 */
575 private function formatSummary( $title, $params ) {
576 global $wgParser;
577 $summary = !is_null( $params['summary'] ) ? $params['summary'] : '';
578 $sectionTitle = !is_null( $params['sectiontitle'] ) ? $params['sectiontitle'] : '';
579
580 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
581 if ( $sectionTitle !== '' ) {
582 $summary = $params['sectiontitle'];
583 }
584 if ( $summary !== '' ) {
585 $summary = wfMessage( 'newsectionsummary' )
586 ->rawParams( $wgParser->stripSectionName( $summary ) )
587 ->inContentLanguage()->text();
588 }
589 }
590 return Linker::formatComment( $summary, $title, $this->section === 'new' );
591 }
592
593 private function formatLangLinks( $links ) {
594 $result = [];
595 foreach ( $links as $link ) {
596 $entry = [];
597 $bits = explode( ':', $link, 2 );
598 $title = Title::newFromText( $link );
599
600 $entry['lang'] = $bits[0];
601 if ( $title ) {
602 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
603 // localised language name in 'uselang' language
604 $entry['langname'] = Language::fetchLanguageName(
605 $title->getInterwiki(),
606 $this->getLanguage()->getCode()
607 );
608
609 // native language name
610 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
611 }
612 ApiResult::setContentValue( $entry, 'title', $bits[1] );
613 $result[] = $entry;
614 }
615
616 return $result;
617 }
618
619 private function formatCategoryLinks( $links ) {
620 $result = [];
621
622 if ( !$links ) {
623 return $result;
624 }
625
626 // Fetch hiddencat property
627 $lb = new LinkBatch;
628 $lb->setArray( [ NS_CATEGORY => $links ] );
629 $db = $this->getDB();
630 $res = $db->select( [ 'page', 'page_props' ],
631 [ 'page_title', 'pp_propname' ],
632 $lb->constructSet( 'page', $db ),
633 __METHOD__,
634 [],
635 [ 'page_props' => [
636 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
637 ] ]
638 );
639 $hiddencats = [];
640 foreach ( $res as $row ) {
641 $hiddencats[$row->page_title] = isset( $row->pp_propname );
642 }
643
644 $linkCache = LinkCache::singleton();
645
646 foreach ( $links as $link => $sortkey ) {
647 $entry = [];
648 $entry['sortkey'] = $sortkey;
649 // array keys will cast numeric category names to ints, so cast back to string
650 ApiResult::setContentValue( $entry, 'category', (string)$link );
651 if ( !isset( $hiddencats[$link] ) ) {
652 $entry['missing'] = true;
653
654 // We already know the link doesn't exist in the database, so
655 // tell LinkCache that before calling $title->isKnown().
656 $title = Title::makeTitle( NS_CATEGORY, $link );
657 $linkCache->addBadLinkObj( $title );
658 if ( $title->isKnown() ) {
659 $entry['known'] = true;
660 }
661 } elseif ( $hiddencats[$link] ) {
662 $entry['hidden'] = true;
663 }
664 $result[] = $entry;
665 }
666
667 return $result;
668 }
669
670 private function categoriesHtml( $categories ) {
671 $context = $this->getContext();
672 $context->getOutput()->addCategoryLinks( $categories );
673
674 return $context->getSkin()->getCategories();
675 }
676
677 private function formatLinks( $links ) {
678 $result = [];
679 foreach ( $links as $ns => $nslinks ) {
680 foreach ( $nslinks as $title => $id ) {
681 $entry = [];
682 $entry['ns'] = $ns;
683 ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
684 $entry['exists'] = $id != 0;
685 $result[] = $entry;
686 }
687 }
688
689 return $result;
690 }
691
692 private function formatIWLinks( $iw ) {
693 $result = [];
694 foreach ( $iw as $prefix => $titles ) {
695 foreach ( array_keys( $titles ) as $title ) {
696 $entry = [];
697 $entry['prefix'] = $prefix;
698
699 $title = Title::newFromText( "{$prefix}:{$title}" );
700 if ( $title ) {
701 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
702 }
703
704 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
705 $result[] = $entry;
706 }
707 }
708
709 return $result;
710 }
711
712 private function formatHeadItems( $headItems ) {
713 $result = [];
714 foreach ( $headItems as $tag => $content ) {
715 $entry = [];
716 $entry['tag'] = $tag;
717 ApiResult::setContentValue( $entry, 'content', $content );
718 $result[] = $entry;
719 }
720
721 return $result;
722 }
723
724 private function formatLimitReportData( $limitReportData ) {
725 $result = [];
726
727 foreach ( $limitReportData as $name => $value ) {
728 $entry = [];
729 $entry['name'] = $name;
730 if ( !is_array( $value ) ) {
731 $value = [ $value ];
732 }
733 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
734 $entry = array_merge( $entry, $value );
735 $result[] = $entry;
736 }
737
738 return $result;
739 }
740
741 private function setIndexedTagNames( &$array, $mapping ) {
742 foreach ( $mapping as $key => $name ) {
743 if ( isset( $array[$key] ) ) {
744 ApiResult::setIndexedTagName( $array[$key], $name );
745 }
746 }
747 }
748
749 public function getAllowedParams() {
750 return [
751 'title' => null,
752 'text' => [
753 ApiBase::PARAM_TYPE => 'text',
754 ],
755 'summary' => null,
756 'page' => null,
757 'pageid' => [
758 ApiBase::PARAM_TYPE => 'integer',
759 ],
760 'redirects' => false,
761 'oldid' => [
762 ApiBase::PARAM_TYPE => 'integer',
763 ],
764 'prop' => [
765 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
766 'images|externallinks|sections|revid|displaytitle|iwlinks|properties',
767 ApiBase::PARAM_ISMULTI => true,
768 ApiBase::PARAM_TYPE => [
769 'text',
770 'langlinks',
771 'categories',
772 'categorieshtml',
773 'links',
774 'templates',
775 'images',
776 'externallinks',
777 'sections',
778 'revid',
779 'displaytitle',
780 'headitems',
781 'headhtml',
782 'modules',
783 'jsconfigvars',
784 'encodedjsconfigvars',
785 'indicators',
786 'iwlinks',
787 'wikitext',
788 'properties',
789 'limitreportdata',
790 'limitreporthtml',
791 'parsetree',
792 ],
793 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
794 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
795 ],
796 ],
797 'pst' => false,
798 'onlypst' => false,
799 'effectivelanglinks' => false,
800 'section' => null,
801 'sectiontitle' => [
802 ApiBase::PARAM_TYPE => 'string',
803 ],
804 'disablepp' => [
805 ApiBase::PARAM_DFLT => false,
806 ApiBase::PARAM_DEPRECATED => true,
807 ],
808 'disablelimitreport' => false,
809 'disableeditsection' => false,
810 'disabletidy' => false,
811 'generatexml' => [
812 ApiBase::PARAM_DFLT => false,
813 ApiBase::PARAM_HELP_MSG => [
814 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
815 ],
816 ApiBase::PARAM_DEPRECATED => true,
817 ],
818 'preview' => false,
819 'sectionpreview' => false,
820 'disabletoc' => false,
821 'contentformat' => [
822 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
823 ],
824 'contentmodel' => [
825 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
826 ]
827 ];
828 }
829
830 protected function getExamplesMessages() {
831 return [
832 'action=parse&page=Project:Sandbox'
833 => 'apihelp-parse-example-page',
834 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
835 => 'apihelp-parse-example-text',
836 'action=parse&text={{PAGENAME}}&title=Test'
837 => 'apihelp-parse-example-texttitle',
838 'action=parse&summary=Some+[[link]]&prop='
839 => 'apihelp-parse-example-summary',
840 ];
841 }
842
843 public function getHelpUrls() {
844 return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#parse';
845 }
846 }