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