Implement extension registration from an extension.json file
[mediawiki.git] / includes / api / ApiParse.php
blob74d1d9a4b2b0f7e76429edc6704b824206f3b799
1 <?php
2 /**
3 * Created on Dec 01, 2007
5 * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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.
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.
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
22 * @file
25 /**
26 * @ingroup API
28 class ApiParse extends ApiBase {
30 /** @var string $section */
31 private $section = null;
33 /** @var Content $content */
34 private $content = null;
36 /** @var Content $pstContent */
37 private $pstContent = null;
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' );
43 // Get parameters
44 $params = $this->extractRequestParams();
45 $text = $params['text'];
46 $title = $params['title'];
47 if ( $title === null ) {
48 $titleProvided = false;
49 // A title is needed for parsing, so arbitrarily choose one
50 $title = 'API';
51 } else {
52 $titleProvided = true;
55 $page = $params['page'];
56 $pageid = $params['pageid'];
57 $oldid = $params['oldid'];
59 $model = $params['contentmodel'];
60 $format = $params['contentformat'];
62 if ( !is_null( $page ) && ( !is_null( $text ) || $titleProvided ) ) {
63 $this->dieUsage(
64 'The page parameter cannot be used together with the text and title parameters',
65 'params'
69 $prop = array_flip( $params['prop'] );
71 if ( isset( $params['section'] ) ) {
72 $this->section = $params['section'];
73 } else {
74 $this->section = false;
77 // The parser needs $wgTitle to be set, apparently the
78 // $title parameter in Parser::parse isn't enough *sigh*
79 // TODO: Does this still need $wgTitle?
80 global $wgParser, $wgTitle;
82 $redirValues = null;
84 // Return result
85 $result = $this->getResult();
87 if ( !is_null( $oldid ) || !is_null( $pageid ) || !is_null( $page ) ) {
88 if ( !is_null( $oldid ) ) {
89 // Don't use the parser cache
90 $rev = Revision::newFromId( $oldid );
91 if ( !$rev ) {
92 $this->dieUsage( "There is no revision ID $oldid", 'missingrev' );
94 if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
95 $this->dieUsage( "You don't have permission to view deleted revisions", 'permissiondenied' );
98 $titleObj = $rev->getTitle();
99 $wgTitle = $titleObj;
100 $pageObj = WikiPage::factory( $titleObj );
101 $popts = $this->makeParserOptions( $pageObj, $params );
103 // If for some reason the "oldid" is actually the current revision, it may be cached
104 if ( $rev->isCurrent() ) {
105 // May get from/save to parser cache
106 $p_result = $this->getParsedContent( $pageObj, $popts,
107 $pageid, isset( $prop['wikitext'] ) );
108 } else { // This is an old revision, so get the text differently
109 $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
111 if ( $this->section !== false ) {
112 $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() );
115 // Should we save old revision parses to the parser cache?
116 $p_result = $this->content->getParserOutput( $titleObj, $rev->getId(), $popts );
118 } else { // Not $oldid, but $pageid or $page
119 if ( $params['redirects'] ) {
120 $reqParams = array(
121 'action' => 'query',
122 'redirects' => '',
124 if ( !is_null( $pageid ) ) {
125 $reqParams['pageids'] = $pageid;
126 } else { // $page
127 $reqParams['titles'] = $page;
129 $req = new FauxRequest( $reqParams );
130 $main = new ApiMain( $req );
131 $main->execute();
132 $data = $main->getResultData();
133 $redirValues = isset( $data['query']['redirects'] )
134 ? $data['query']['redirects']
135 : array();
136 $to = $page;
137 foreach ( (array)$redirValues as $r ) {
138 $to = $r['to'];
140 $pageParams = array( 'title' => $to );
141 } elseif ( !is_null( $pageid ) ) {
142 $pageParams = array( 'pageid' => $pageid );
143 } else { // $page
144 $pageParams = array( 'title' => $page );
147 $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
148 $titleObj = $pageObj->getTitle();
149 if ( !$titleObj || !$titleObj->exists() ) {
150 $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
152 $wgTitle = $titleObj;
154 if ( isset( $prop['revid'] ) ) {
155 $oldid = $pageObj->getLatest();
158 $popts = $this->makeParserOptions( $pageObj, $params );
160 // Potentially cached
161 $p_result = $this->getParsedContent( $pageObj, $popts, $pageid,
162 isset( $prop['wikitext'] ) );
164 } else { // Not $oldid, $pageid, $page. Hence based on $text
165 $titleObj = Title::newFromText( $title );
166 if ( !$titleObj || $titleObj->isExternal() ) {
167 $this->dieUsageMsg( array( 'invalidtitle', $title ) );
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();
178 $popts = $this->makeParserOptions( $pageObj, $params );
179 $textProvided = !is_null( $text );
181 if ( !$textProvided ) {
182 if ( $titleProvided && ( $prop || $params['generatexml'] ) ) {
183 $this->setWarning(
184 "'title' used without 'text', and parsed page properties were requested " .
185 "(did you mean to use 'page' instead of 'title'?)"
188 // Prevent warning from ContentHandler::makeContent()
189 $text = '';
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->setWarning( "No 'title' or 'contentmodel' was given, assuming $model." );
199 try {
200 $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
201 } catch ( MWContentSerializationException $ex ) {
202 $this->dieUsage( $ex->getMessage(), 'parseerror' );
205 if ( $this->section !== false ) {
206 $this->content = $this->getSectionContent( $this->content, $titleObj->getPrefixedText() );
209 if ( $params['pst'] || $params['onlypst'] ) {
210 $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts );
212 if ( $params['onlypst'] ) {
213 // Build a result and bail out
214 $result_array = array();
215 $result_array['text'] = array();
216 ApiResult::setContent( $result_array['text'], $this->pstContent->serialize( $format ) );
217 if ( isset( $prop['wikitext'] ) ) {
218 $result_array['wikitext'] = array();
219 ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
221 $result->addValue( null, $this->getModuleName(), $result_array );
223 return;
226 // Not cached (save or load)
227 if ( $params['pst'] ) {
228 $p_result = $this->pstContent->getParserOutput( $titleObj, null, $popts );
229 } else {
230 $p_result = $this->content->getParserOutput( $titleObj, null, $popts );
234 $result_array = array();
236 $result_array['title'] = $titleObj->getPrefixedText();
238 if ( !is_null( $oldid ) ) {
239 $result_array['revid'] = intval( $oldid );
242 if ( $params['redirects'] && !is_null( $redirValues ) ) {
243 $result_array['redirects'] = $redirValues;
246 if ( $params['disabletoc'] ) {
247 $p_result->setTOCEnabled( false );
250 if ( isset( $prop['text'] ) ) {
251 $result_array['text'] = array();
252 ApiResult::setContent( $result_array['text'], $p_result->getText() );
255 if ( !is_null( $params['summary'] ) ) {
256 $result_array['parsedsummary'] = array();
257 ApiResult::setContent(
258 $result_array['parsedsummary'],
259 Linker::formatComment( $params['summary'], $titleObj )
263 if ( isset( $prop['langlinks'] ) ) {
264 $langlinks = $p_result->getLanguageLinks();
266 if ( $params['effectivelanglinks'] ) {
267 // Link flags are ignored for now, but may in the future be
268 // included in the result.
269 $linkFlags = array();
270 Hooks::run( 'LanguageLinks', array( $titleObj, &$langlinks, &$linkFlags ) );
272 } else {
273 $langlinks = false;
276 if ( isset( $prop['langlinks'] ) ) {
277 $result_array['langlinks'] = $this->formatLangLinks( $langlinks );
279 if ( isset( $prop['categories'] ) ) {
280 $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
282 if ( isset( $prop['categorieshtml'] ) ) {
283 $categoriesHtml = $this->categoriesHtml( $p_result->getCategories() );
284 $result_array['categorieshtml'] = array();
285 ApiResult::setContent( $result_array['categorieshtml'], $categoriesHtml );
287 if ( isset( $prop['links'] ) ) {
288 $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
290 if ( isset( $prop['templates'] ) ) {
291 $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
293 if ( isset( $prop['images'] ) ) {
294 $result_array['images'] = array_keys( $p_result->getImages() );
296 if ( isset( $prop['externallinks'] ) ) {
297 $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
299 if ( isset( $prop['sections'] ) ) {
300 $result_array['sections'] = $p_result->getSections();
303 if ( isset( $prop['displaytitle'] ) ) {
304 $result_array['displaytitle'] = $p_result->getDisplayTitle() ?
305 $p_result->getDisplayTitle() :
306 $titleObj->getPrefixedText();
309 if ( isset( $prop['headitems'] ) || isset( $prop['headhtml'] ) ) {
310 $context = $this->getContext();
311 $context->setTitle( $titleObj );
312 $context->getOutput()->addParserOutputMetadata( $p_result );
314 if ( isset( $prop['headitems'] ) ) {
315 $headItems = $this->formatHeadItems( $p_result->getHeadItems() );
317 $css = $this->formatCss( $context->getOutput()->buildCssLinksArray() );
319 $scripts = array( $context->getOutput()->getHeadScripts() );
321 $result_array['headitems'] = array_merge( $headItems, $css, $scripts );
324 if ( isset( $prop['headhtml'] ) ) {
325 $result_array['headhtml'] = array();
326 ApiResult::setContent(
327 $result_array['headhtml'],
328 $context->getOutput()->headElement( $context->getSkin() )
333 if ( isset( $prop['modules'] ) ) {
334 $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) );
335 $result_array['modulescripts'] = array_values( array_unique( $p_result->getModuleScripts() ) );
336 $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) );
337 $result_array['modulemessages'] = array_values( array_unique( $p_result->getModuleMessages() ) );
340 if ( isset( $prop['indicators'] ) ) {
341 foreach ( $p_result->getIndicators() as $name => $content ) {
342 $indicator = array( 'name' => $name );
343 ApiResult::setContent( $indicator, $content );
344 $result_array['indicators'][] = $indicator;
348 if ( isset( $prop['iwlinks'] ) ) {
349 $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
352 if ( isset( $prop['wikitext'] ) ) {
353 $result_array['wikitext'] = array();
354 ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
355 if ( !is_null( $this->pstContent ) ) {
356 $result_array['psttext'] = array();
357 ApiResult::setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) );
360 if ( isset( $prop['properties'] ) ) {
361 $result_array['properties'] = $this->formatProperties( $p_result->getProperties() );
364 if ( isset( $prop['limitreportdata'] ) ) {
365 $result_array['limitreportdata'] =
366 $this->formatLimitReportData( $p_result->getLimitReportData() );
368 if ( isset( $prop['limitreporthtml'] ) ) {
369 $limitreportHtml = EditPage::getPreviewLimitReport( $p_result );
370 $result_array['limitreporthtml'] = array();
371 ApiResult::setContent( $result_array['limitreporthtml'], $limitreportHtml );
374 if ( $params['generatexml'] ) {
375 if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
376 $this->dieUsage( "generatexml is only supported for wikitext content", "notwikitext" );
379 $wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
380 $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
381 if ( is_callable( array( $dom, 'saveXML' ) ) ) {
382 $xml = $dom->saveXML();
383 } else {
384 $xml = $dom->__toString();
386 $result_array['parsetree'] = array();
387 ApiResult::setContent( $result_array['parsetree'], $xml );
390 $result_mapping = array(
391 'redirects' => 'r',
392 'langlinks' => 'll',
393 'categories' => 'cl',
394 'links' => 'pl',
395 'templates' => 'tl',
396 'images' => 'img',
397 'externallinks' => 'el',
398 'iwlinks' => 'iw',
399 'sections' => 's',
400 'headitems' => 'hi',
401 'modules' => 'm',
402 'indicators' => 'ind',
403 'modulescripts' => 'm',
404 'modulestyles' => 'm',
405 'modulemessages' => 'm',
406 'properties' => 'pp',
407 'limitreportdata' => 'lr',
409 $this->setIndexedTagNames( $result_array, $result_mapping );
410 $result->addValue( null, $this->getModuleName(), $result_array );
414 * Constructs a ParserOptions object
416 * @param WikiPage $pageObj
417 * @param array $params
419 * @return ParserOptions
421 protected function makeParserOptions( WikiPage $pageObj, array $params ) {
423 $popts = $pageObj->makeParserOptions( $this->getContext() );
424 $popts->enableLimitReport( !$params['disablepp'] );
425 $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] );
426 $popts->setIsSectionPreview( $params['sectionpreview'] );
427 $popts->setEditSection( !$params['disableeditsection'] );
430 return $popts;
434 * @param WikiPage $page
435 * @param ParserOptions $popts
436 * @param int $pageId
437 * @param bool $getWikitext
438 * @return ParserOutput
440 private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) {
441 $this->content = $page->getContent( Revision::RAW ); //XXX: really raw?
443 if ( $this->section !== false && $this->content !== null ) {
444 $this->content = $this->getSectionContent(
445 $this->content,
446 !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText()
449 // Not cached (save or load)
450 return $this->content->getParserOutput( $page->getTitle(), null, $popts );
453 // Try the parser cache first
454 // getParserOutput will save to Parser cache if able
455 $pout = $page->getParserOutput( $popts );
456 if ( !$pout ) {
457 $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
459 if ( $getWikitext ) {
460 $this->content = $page->getContent( Revision::RAW );
463 return $pout;
467 * @param Content $content
468 * @param string $what Identifies the content in error messages, e.g. page title.
469 * @return Content|bool
471 private function getSectionContent( Content $content, $what ) {
472 // Not cached (save or load)
473 $section = $content->getSection( $this->section );
474 if ( $section === false ) {
475 $this->dieUsage( "There is no section {$this->section} in " . $what, 'nosuchsection' );
477 if ( $section === null ) {
478 $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' );
479 $section = false;
482 return $section;
485 private function formatLangLinks( $links ) {
486 $result = array();
487 foreach ( $links as $link ) {
488 $entry = array();
489 $bits = explode( ':', $link, 2 );
490 $title = Title::newFromText( $link );
492 $entry['lang'] = $bits[0];
493 if ( $title ) {
494 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
495 // localised language name in 'uselang' language
496 $entry['langname'] = Language::fetchLanguageName(
497 $title->getInterwiki(),
498 $this->getLanguage()->getCode()
501 // native language name
502 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
504 ApiResult::setContent( $entry, $bits[1] );
505 $result[] = $entry;
508 return $result;
511 private function formatCategoryLinks( $links ) {
512 $result = array();
514 if ( !$links ) {
515 return $result;
518 // Fetch hiddencat property
519 $lb = new LinkBatch;
520 $lb->setArray( array( NS_CATEGORY => $links ) );
521 $db = $this->getDB();
522 $res = $db->select( array( 'page', 'page_props' ),
523 array( 'page_title', 'pp_propname' ),
524 $lb->constructSet( 'page', $db ),
525 __METHOD__,
526 array(),
527 array( 'page_props' => array(
528 'LEFT JOIN', array( 'pp_propname' => 'hiddencat', 'pp_page = page_id' )
531 $hiddencats = array();
532 foreach ( $res as $row ) {
533 $hiddencats[$row->page_title] = isset( $row->pp_propname );
536 foreach ( $links as $link => $sortkey ) {
537 $entry = array();
538 $entry['sortkey'] = $sortkey;
539 ApiResult::setContent( $entry, $link );
540 if ( !isset( $hiddencats[$link] ) ) {
541 $entry['missing'] = '';
542 } elseif ( $hiddencats[$link] ) {
543 $entry['hidden'] = '';
545 $result[] = $entry;
548 return $result;
551 private function categoriesHtml( $categories ) {
552 $context = $this->getContext();
553 $context->getOutput()->addCategoryLinks( $categories );
555 return $context->getSkin()->getCategories();
558 private function formatLinks( $links ) {
559 $result = array();
560 foreach ( $links as $ns => $nslinks ) {
561 foreach ( $nslinks as $title => $id ) {
562 $entry = array();
563 $entry['ns'] = $ns;
564 ApiResult::setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() );
565 if ( $id != 0 ) {
566 $entry['exists'] = '';
568 $result[] = $entry;
572 return $result;
575 private function formatIWLinks( $iw ) {
576 $result = array();
577 foreach ( $iw as $prefix => $titles ) {
578 foreach ( array_keys( $titles ) as $title ) {
579 $entry = array();
580 $entry['prefix'] = $prefix;
582 $title = Title::newFromText( "{$prefix}:{$title}" );
583 if ( $title ) {
584 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
587 ApiResult::setContent( $entry, $title->getFullText() );
588 $result[] = $entry;
592 return $result;
595 private function formatHeadItems( $headItems ) {
596 $result = array();
597 foreach ( $headItems as $tag => $content ) {
598 $entry = array();
599 $entry['tag'] = $tag;
600 ApiResult::setContent( $entry, $content );
601 $result[] = $entry;
604 return $result;
607 private function formatProperties( $properties ) {
608 $result = array();
609 foreach ( $properties as $name => $value ) {
610 $entry = array();
611 $entry['name'] = $name;
612 ApiResult::setContent( $entry, $value );
613 $result[] = $entry;
616 return $result;
619 private function formatCss( $css ) {
620 $result = array();
621 foreach ( $css as $file => $link ) {
622 $entry = array();
623 $entry['file'] = $file;
624 ApiResult::setContent( $entry, $link );
625 $result[] = $entry;
628 return $result;
631 private function formatLimitReportData( $limitReportData ) {
632 $result = array();
633 $apiResult = $this->getResult();
635 foreach ( $limitReportData as $name => $value ) {
636 $entry = array();
637 $entry['name'] = $name;
638 if ( !is_array( $value ) ) {
639 $value = array( $value );
641 $apiResult->setIndexedTagName( $value, 'param' );
642 $apiResult->setIndexedTagName_recursive( $value, 'param' );
643 $entry = array_merge( $entry, $value );
644 $result[] = $entry;
647 return $result;
650 private function setIndexedTagNames( &$array, $mapping ) {
651 foreach ( $mapping as $key => $name ) {
652 if ( isset( $array[$key] ) ) {
653 $this->getResult()->setIndexedTagName( $array[$key], $name );
658 public function getAllowedParams() {
659 return array(
660 'title' => null,
661 'text' => null,
662 'summary' => null,
663 'page' => null,
664 'pageid' => array(
665 ApiBase::PARAM_TYPE => 'integer',
667 'redirects' => false,
668 'oldid' => array(
669 ApiBase::PARAM_TYPE => 'integer',
671 'prop' => array(
672 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
673 'images|externallinks|sections|revid|displaytitle|iwlinks|properties',
674 ApiBase::PARAM_ISMULTI => true,
675 ApiBase::PARAM_TYPE => array(
676 'text',
677 'langlinks',
678 'categories',
679 'categorieshtml',
680 'links',
681 'templates',
682 'images',
683 'externallinks',
684 'sections',
685 'revid',
686 'displaytitle',
687 'headitems',
688 'headhtml',
689 'modules',
690 'indicators',
691 'iwlinks',
692 'wikitext',
693 'properties',
694 'limitreportdata',
695 'limitreporthtml',
698 'pst' => false,
699 'onlypst' => false,
700 'effectivelanglinks' => false,
701 'section' => null,
702 'disablepp' => false,
703 'disableeditsection' => false,
704 'generatexml' => array(
705 ApiBase::PARAM_DFLT => false,
706 ApiBase::PARAM_HELP_MSG => array(
707 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
710 'preview' => false,
711 'sectionpreview' => false,
712 'disabletoc' => false,
713 'contentformat' => array(
714 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
716 'contentmodel' => array(
717 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
722 protected function getExamplesMessages() {
723 return array(
724 'action=parse&page=Project:Sandbox'
725 => 'apihelp-parse-example-page',
726 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
727 => 'apihelp-parse-example-text',
728 'action=parse&text={{PAGENAME}}&title=Test'
729 => 'apihelp-parse-example-texttitle',
730 'action=parse&summary=Some+[[link]]&prop='
731 => 'apihelp-parse-example-summary',
735 public function getHelpUrls() {
736 return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#parse';