Merge "Perform a permission check on the title when changing the page language"
[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 }
389
390 if ( isset( $prop['headhtml'] ) ) {
391 $result_array['headhtml'] = $outputPage->headElement( $context->getSkin() );
392 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'headhtml';
393 }
394
395 if ( isset( $prop['modules'] ) ) {
396 if ( $skin ) {
397 $result_array['modules'] = $outputPage->getModules();
398 $result_array['modulescripts'] = $outputPage->getModuleScripts();
399 $result_array['modulestyles'] = $outputPage->getModuleStyles();
400 } else {
401 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
402 $result_array['modulescripts'] = array_values( array_unique( $p_result->getModuleScripts() ) );
403 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
404 }
405 }
406
407 if ( isset( $prop['jsconfigvars'] ) ) {
408 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
409 $result_array['jsconfigvars'] = ApiResult::addMetadataToResultVars( $jsconfigvars );
410 }
411
412 if ( isset( $prop['encodedjsconfigvars'] ) ) {
413 $jsconfigvars = $skin ? $outputPage->getJsConfigVars() : $p_result->getJsConfigVars();
414 $result_array['encodedjsconfigvars'] = FormatJson::encode(
415 $jsconfigvars,
416 false,
417 FormatJson::ALL_OK
418 );
419 $result_array[ApiResult::META_SUBELEMENTS][] = 'encodedjsconfigvars';
420 }
421
422 if ( isset( $prop['modules'] ) &&
423 !isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
424 $this->addWarning( 'apiwarn-moduleswithoutvars' );
425 }
426
427 if ( isset( $prop['indicators'] ) ) {
428 if ( $skin ) {
429 $result_array['indicators'] = (array)$outputPage->getIndicators();
430 } else {
431 $result_array['indicators'] = (array)$p_result->getIndicators();
432 }
433 ApiResult::setArrayType( $result_array['indicators'], 'BCkvp', 'name' );
434 }
435
436 if ( isset( $prop['iwlinks'] ) ) {
437 $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
438 }
439
440 if ( isset( $prop['wikitext'] ) ) {
441 $result_array['wikitext'] = $this->content->serialize( $format );
442 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'wikitext';
443 if ( !is_null( $this->pstContent ) ) {
444 $result_array['psttext'] = $this->pstContent->serialize( $format );
445 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'psttext';
446 }
447 }
448 if ( isset( $prop['properties'] ) ) {
449 $result_array['properties'] = (array)$p_result->getProperties();
450 ApiResult::setArrayType( $result_array['properties'], 'BCkvp', 'name' );
451 }
452
453 if ( isset( $prop['limitreportdata'] ) ) {
454 $result_array['limitreportdata'] =
455 $this->formatLimitReportData( $p_result->getLimitReportData() );
456 }
457 if ( isset( $prop['limitreporthtml'] ) ) {
458 $result_array['limitreporthtml'] = EditPage::getPreviewLimitReport( $p_result );
459 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'limitreporthtml';
460 }
461
462 if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
463 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
464 $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
465 }
466
467 $wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
468 $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
469 if ( is_callable( [ $dom, 'saveXML' ] ) ) {
470 $xml = $dom->saveXML();
471 } else {
472 $xml = $dom->__toString();
473 }
474 $result_array['parsetree'] = $xml;
475 $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
476 }
477
478 $result_mapping = [
479 'redirects' => 'r',
480 'langlinks' => 'll',
481 'categories' => 'cl',
482 'links' => 'pl',
483 'templates' => 'tl',
484 'images' => 'img',
485 'externallinks' => 'el',
486 'iwlinks' => 'iw',
487 'sections' => 's',
488 'headitems' => 'hi',
489 'modules' => 'm',
490 'indicators' => 'ind',
491 'modulescripts' => 'm',
492 'modulestyles' => 'm',
493 'properties' => 'pp',
494 'limitreportdata' => 'lr',
495 'parsewarnings' => 'pw'
496 ];
497 $this->setIndexedTagNames( $result_array, $result_mapping );
498 $result->addValue( null, $this->getModuleName(), $result_array );
499 }
500
501 /**
502 * Constructs a ParserOptions object
503 *
504 * @param WikiPage $pageObj
505 * @param array $params
506 *
507 * @return array [ ParserOptions, ScopedCallback, bool $suppressCache ]
508 */
509 protected function makeParserOptions( WikiPage $pageObj, array $params ) {
510 $popts = $pageObj->makeParserOptions( $this->getContext() );
511 $popts->enableLimitReport( !$params['disablepp'] && !$params['disablelimitreport'] );
512 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
513 $popts->setIsSectionPreview( $params['sectionpreview'] );
514 $popts->setEditSection( !$params['disableeditsection'] );
515 if ( $params['disabletidy'] ) {
516 $popts->setTidy( false );
517 }
518 $popts->setWrapOutputClass(
519 $params['wrapoutputclass'] === '' ? false : $params['wrapoutputclass']
520 );
521
522 $reset = null;
523 $suppressCache = false;
524 Hooks::run( 'ApiMakeParserOptions',
525 [ $popts, $pageObj->getTitle(), $params, $this, &$reset, &$suppressCache ] );
526
527 // Force cache suppression when $popts aren't cacheable.
528 $suppressCache = $suppressCache || !$popts->isSafeToCache();
529
530 return [ $popts, $reset, $suppressCache ];
531 }
532
533 /**
534 * @param WikiPage $page
535 * @param ParserOptions $popts
536 * @param bool $suppressCache
537 * @param int $pageId
538 * @param Revision|null $rev
539 * @param bool $getContent
540 * @return ParserOutput
541 */
542 private function getParsedContent(
543 WikiPage $page, $popts, $suppressCache, $pageId, $rev, $getContent
544 ) {
545 $revId = $rev ? $rev->getId() : null;
546 $isDeleted = $rev && $rev->isDeleted( Revision::DELETED_TEXT );
547
548 if ( $getContent || $this->section !== false || $isDeleted ) {
549 if ( $rev ) {
550 $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
551 if ( !$this->content ) {
552 $this->dieWithError( [ 'apierror-missingcontent-revid', $revId ] );
553 }
554 } else {
555 $this->content = $page->getContent( Revision::FOR_THIS_USER, $this->getUser() );
556 if ( !$this->content ) {
557 $this->dieWithError( [ 'apierror-missingcontent-pageid', $pageId ] );
558 }
559 }
560 $this->contentIsDeleted = $isDeleted;
561 $this->contentIsSuppressed = $rev &&
562 $rev->isDeleted( Revision::DELETED_TEXT | Revision::DELETED_RESTRICTED );
563 }
564
565 if ( $this->section !== false ) {
566 $this->content = $this->getSectionContent(
567 $this->content,
568 $pageId === null ? $page->getTitle()->getPrefixedText() : $this->msg( 'pageid', $pageId )
569 );
570 return $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
571 }
572
573 if ( $isDeleted ) {
574 // getParserOutput can't do revdeled revisions
575 $pout = $this->content->getParserOutput( $page->getTitle(), $revId, $popts );
576 } else {
577 // getParserOutput will save to Parser cache if able
578 $pout = $page->getParserOutput( $popts, $revId, $suppressCache );
579 }
580 if ( !$pout ) {
581 $this->dieWithError( [ 'apierror-nosuchrevid', $revId ?: $page->getLatest() ] );
582 }
583
584 return $pout;
585 }
586
587 /**
588 * Extract the requested section from the given Content
589 *
590 * @param Content $content
591 * @param string|Message $what Identifies the content in error messages, e.g. page title.
592 * @return Content
593 */
594 private function getSectionContent( Content $content, $what ) {
595 // Not cached (save or load)
596 $section = $content->getSection( $this->section );
597 if ( $section === false ) {
598 $this->dieWithError( [ 'apierror-nosuchsection-what', $this->section, $what ], 'nosuchsection' );
599 }
600 if ( $section === null ) {
601 $this->dieWithError( [ 'apierror-sectionsnotsupported-what', $what ], 'nosuchsection' );
602 $section = false;
603 }
604
605 return $section;
606 }
607
608 /**
609 * This mimicks the behavior of EditPage in formatting a summary
610 *
611 * @param Title $title of the page being parsed
612 * @param Array $params the API parameters of the request
613 * @return Content|bool
614 */
615 private function formatSummary( $title, $params ) {
616 global $wgParser;
617 $summary = !is_null( $params['summary'] ) ? $params['summary'] : '';
618 $sectionTitle = !is_null( $params['sectiontitle'] ) ? $params['sectiontitle'] : '';
619
620 if ( $this->section === 'new' && ( $sectionTitle === '' || $summary === '' ) ) {
621 if ( $sectionTitle !== '' ) {
622 $summary = $params['sectiontitle'];
623 }
624 if ( $summary !== '' ) {
625 $summary = wfMessage( 'newsectionsummary' )
626 ->rawParams( $wgParser->stripSectionName( $summary ) )
627 ->inContentLanguage()->text();
628 }
629 }
630 return Linker::formatComment( $summary, $title, $this->section === 'new' );
631 }
632
633 private function formatLangLinks( $links ) {
634 $result = [];
635 foreach ( $links as $link ) {
636 $entry = [];
637 $bits = explode( ':', $link, 2 );
638 $title = Title::newFromText( $link );
639
640 $entry['lang'] = $bits[0];
641 if ( $title ) {
642 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
643 // localised language name in 'uselang' language
644 $entry['langname'] = Language::fetchLanguageName(
645 $title->getInterwiki(),
646 $this->getLanguage()->getCode()
647 );
648
649 // native language name
650 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
651 }
652 ApiResult::setContentValue( $entry, 'title', $bits[1] );
653 $result[] = $entry;
654 }
655
656 return $result;
657 }
658
659 private function formatCategoryLinks( $links ) {
660 $result = [];
661
662 if ( !$links ) {
663 return $result;
664 }
665
666 // Fetch hiddencat property
667 $lb = new LinkBatch;
668 $lb->setArray( [ NS_CATEGORY => $links ] );
669 $db = $this->getDB();
670 $res = $db->select( [ 'page', 'page_props' ],
671 [ 'page_title', 'pp_propname' ],
672 $lb->constructSet( 'page', $db ),
673 __METHOD__,
674 [],
675 [ 'page_props' => [
676 'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = page_id' ]
677 ] ]
678 );
679 $hiddencats = [];
680 foreach ( $res as $row ) {
681 $hiddencats[$row->page_title] = isset( $row->pp_propname );
682 }
683
684 $linkCache = LinkCache::singleton();
685
686 foreach ( $links as $link => $sortkey ) {
687 $entry = [];
688 $entry['sortkey'] = $sortkey;
689 // array keys will cast numeric category names to ints, so cast back to string
690 ApiResult::setContentValue( $entry, 'category', (string)$link );
691 if ( !isset( $hiddencats[$link] ) ) {
692 $entry['missing'] = true;
693
694 // We already know the link doesn't exist in the database, so
695 // tell LinkCache that before calling $title->isKnown().
696 $title = Title::makeTitle( NS_CATEGORY, $link );
697 $linkCache->addBadLinkObj( $title );
698 if ( $title->isKnown() ) {
699 $entry['known'] = true;
700 }
701 } elseif ( $hiddencats[$link] ) {
702 $entry['hidden'] = true;
703 }
704 $result[] = $entry;
705 }
706
707 return $result;
708 }
709
710 private function formatLinks( $links ) {
711 $result = [];
712 foreach ( $links as $ns => $nslinks ) {
713 foreach ( $nslinks as $title => $id ) {
714 $entry = [];
715 $entry['ns'] = $ns;
716 ApiResult::setContentValue( $entry, 'title', Title::makeTitle( $ns, $title )->getFullText() );
717 $entry['exists'] = $id != 0;
718 $result[] = $entry;
719 }
720 }
721
722 return $result;
723 }
724
725 private function formatIWLinks( $iw ) {
726 $result = [];
727 foreach ( $iw as $prefix => $titles ) {
728 foreach ( array_keys( $titles ) as $title ) {
729 $entry = [];
730 $entry['prefix'] = $prefix;
731
732 $title = Title::newFromText( "{$prefix}:{$title}" );
733 if ( $title ) {
734 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
735 }
736
737 ApiResult::setContentValue( $entry, 'title', $title->getFullText() );
738 $result[] = $entry;
739 }
740 }
741
742 return $result;
743 }
744
745 private function formatHeadItems( $headItems ) {
746 $result = [];
747 foreach ( $headItems as $tag => $content ) {
748 $entry = [];
749 $entry['tag'] = $tag;
750 ApiResult::setContentValue( $entry, 'content', $content );
751 $result[] = $entry;
752 }
753
754 return $result;
755 }
756
757 private function formatLimitReportData( $limitReportData ) {
758 $result = [];
759
760 foreach ( $limitReportData as $name => $value ) {
761 $entry = [];
762 $entry['name'] = $name;
763 if ( !is_array( $value ) ) {
764 $value = [ $value ];
765 }
766 ApiResult::setIndexedTagNameRecursive( $value, 'param' );
767 $entry = array_merge( $entry, $value );
768 $result[] = $entry;
769 }
770
771 return $result;
772 }
773
774 private function setIndexedTagNames( &$array, $mapping ) {
775 foreach ( $mapping as $key => $name ) {
776 if ( isset( $array[$key] ) ) {
777 ApiResult::setIndexedTagName( $array[$key], $name );
778 }
779 }
780 }
781
782 public function getAllowedParams() {
783 return [
784 'title' => null,
785 'text' => [
786 ApiBase::PARAM_TYPE => 'text',
787 ],
788 'summary' => null,
789 'page' => null,
790 'pageid' => [
791 ApiBase::PARAM_TYPE => 'integer',
792 ],
793 'redirects' => false,
794 'oldid' => [
795 ApiBase::PARAM_TYPE => 'integer',
796 ],
797 'prop' => [
798 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
799 'images|externallinks|sections|revid|displaytitle|iwlinks|' .
800 'properties|parsewarnings',
801 ApiBase::PARAM_ISMULTI => true,
802 ApiBase::PARAM_TYPE => [
803 'text',
804 'langlinks',
805 'categories',
806 'categorieshtml',
807 'links',
808 'templates',
809 'images',
810 'externallinks',
811 'sections',
812 'revid',
813 'displaytitle',
814 'headhtml',
815 'modules',
816 'jsconfigvars',
817 'encodedjsconfigvars',
818 'indicators',
819 'iwlinks',
820 'wikitext',
821 'properties',
822 'limitreportdata',
823 'limitreporthtml',
824 'parsetree',
825 'parsewarnings',
826 'headitems',
827 ],
828 ApiBase::PARAM_HELP_MSG_PER_VALUE => [
829 'parsetree' => [ 'apihelp-parse-paramvalue-prop-parsetree', CONTENT_MODEL_WIKITEXT ],
830 ],
831 ApiBase::PARAM_DEPRECATED_VALUES => [
832 'headitems' => 'apiwarn-deprecation-parse-headitems',
833 ],
834 ],
835 'wrapoutputclass' => 'mw-parser-output',
836 'pst' => false,
837 'onlypst' => false,
838 'effectivelanglinks' => [
839 ApiBase::PARAM_DFLT => false,
840 ApiBase::PARAM_DEPRECATED => true,
841 ],
842 'section' => null,
843 'sectiontitle' => [
844 ApiBase::PARAM_TYPE => 'string',
845 ],
846 'disablepp' => [
847 ApiBase::PARAM_DFLT => false,
848 ApiBase::PARAM_DEPRECATED => true,
849 ],
850 'disablelimitreport' => false,
851 'disableeditsection' => false,
852 'disabletidy' => false,
853 'generatexml' => [
854 ApiBase::PARAM_DFLT => false,
855 ApiBase::PARAM_HELP_MSG => [
856 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
857 ],
858 ApiBase::PARAM_DEPRECATED => true,
859 ],
860 'preview' => false,
861 'sectionpreview' => false,
862 'disabletoc' => false,
863 'useskin' => [
864 ApiBase::PARAM_TYPE => array_keys( Skin::getAllowedSkins() ),
865 ],
866 'contentformat' => [
867 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
868 ],
869 'contentmodel' => [
870 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
871 ]
872 ];
873 }
874
875 protected function getExamplesMessages() {
876 return [
877 'action=parse&page=Project:Sandbox'
878 => 'apihelp-parse-example-page',
879 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
880 => 'apihelp-parse-example-text',
881 'action=parse&text={{PAGENAME}}&title=Test'
882 => 'apihelp-parse-example-texttitle',
883 'action=parse&summary=Some+[[link]]&prop='
884 => 'apihelp-parse-example-summary',
885 ];
886 }
887
888 public function getHelpUrls() {
889 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse';
890 }
891 }