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