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