Merge "DatabaseMssql: Don't duplicate body of makeList()"
[mediawiki.git] / includes / api / ApiParse.php
blob83d2cbc0bdcf110708efe95ffa035894edd3aa09
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'] );
429 return $popts;
433 * @param WikiPage $page
434 * @param ParserOptions $popts
435 * @param int $pageId
436 * @param bool $getWikitext
437 * @return ParserOutput
439 private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) {
440 $this->content = $page->getContent( Revision::RAW ); //XXX: really raw?
442 if ( $this->section !== false && $this->content !== null ) {
443 $this->content = $this->getSectionContent(
444 $this->content,
445 !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText()
448 // Not cached (save or load)
449 return $this->content->getParserOutput( $page->getTitle(), null, $popts );
452 // Try the parser cache first
453 // getParserOutput will save to Parser cache if able
454 $pout = $page->getParserOutput( $popts );
455 if ( !$pout ) {
456 $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
458 if ( $getWikitext ) {
459 $this->content = $page->getContent( Revision::RAW );
462 return $pout;
466 * @param Content $content
467 * @param string $what Identifies the content in error messages, e.g. page title.
468 * @return Content|bool
470 private function getSectionContent( Content $content, $what ) {
471 // Not cached (save or load)
472 $section = $content->getSection( $this->section );
473 if ( $section === false ) {
474 $this->dieUsage( "There is no section {$this->section} in " . $what, 'nosuchsection' );
476 if ( $section === null ) {
477 $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' );
478 $section = false;
481 return $section;
484 private function formatLangLinks( $links ) {
485 $result = array();
486 foreach ( $links as $link ) {
487 $entry = array();
488 $bits = explode( ':', $link, 2 );
489 $title = Title::newFromText( $link );
491 $entry['lang'] = $bits[0];
492 if ( $title ) {
493 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
494 // localised language name in 'uselang' language
495 $entry['langname'] = Language::fetchLanguageName(
496 $title->getInterwiki(),
497 $this->getLanguage()->getCode()
500 // native language name
501 $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() );
503 ApiResult::setContent( $entry, $bits[1] );
504 $result[] = $entry;
507 return $result;
510 private function formatCategoryLinks( $links ) {
511 $result = array();
513 if ( !$links ) {
514 return $result;
517 // Fetch hiddencat property
518 $lb = new LinkBatch;
519 $lb->setArray( array( NS_CATEGORY => $links ) );
520 $db = $this->getDB();
521 $res = $db->select( array( 'page', 'page_props' ),
522 array( 'page_title', 'pp_propname' ),
523 $lb->constructSet( 'page', $db ),
524 __METHOD__,
525 array(),
526 array( 'page_props' => array(
527 'LEFT JOIN', array( 'pp_propname' => 'hiddencat', 'pp_page = page_id' )
530 $hiddencats = array();
531 foreach ( $res as $row ) {
532 $hiddencats[$row->page_title] = isset( $row->pp_propname );
535 foreach ( $links as $link => $sortkey ) {
536 $entry = array();
537 $entry['sortkey'] = $sortkey;
538 ApiResult::setContent( $entry, $link );
539 if ( !isset( $hiddencats[$link] ) ) {
540 $entry['missing'] = '';
541 } elseif ( $hiddencats[$link] ) {
542 $entry['hidden'] = '';
544 $result[] = $entry;
547 return $result;
550 private function categoriesHtml( $categories ) {
551 $context = $this->getContext();
552 $context->getOutput()->addCategoryLinks( $categories );
554 return $context->getSkin()->getCategories();
557 private function formatLinks( $links ) {
558 $result = array();
559 foreach ( $links as $ns => $nslinks ) {
560 foreach ( $nslinks as $title => $id ) {
561 $entry = array();
562 $entry['ns'] = $ns;
563 ApiResult::setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() );
564 if ( $id != 0 ) {
565 $entry['exists'] = '';
567 $result[] = $entry;
571 return $result;
574 private function formatIWLinks( $iw ) {
575 $result = array();
576 foreach ( $iw as $prefix => $titles ) {
577 foreach ( array_keys( $titles ) as $title ) {
578 $entry = array();
579 $entry['prefix'] = $prefix;
581 $title = Title::newFromText( "{$prefix}:{$title}" );
582 if ( $title ) {
583 $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
586 ApiResult::setContent( $entry, $title->getFullText() );
587 $result[] = $entry;
591 return $result;
594 private function formatHeadItems( $headItems ) {
595 $result = array();
596 foreach ( $headItems as $tag => $content ) {
597 $entry = array();
598 $entry['tag'] = $tag;
599 ApiResult::setContent( $entry, $content );
600 $result[] = $entry;
603 return $result;
606 private function formatProperties( $properties ) {
607 $result = array();
608 foreach ( $properties as $name => $value ) {
609 $entry = array();
610 $entry['name'] = $name;
611 ApiResult::setContent( $entry, $value );
612 $result[] = $entry;
615 return $result;
618 private function formatCss( $css ) {
619 $result = array();
620 foreach ( $css as $file => $link ) {
621 $entry = array();
622 $entry['file'] = $file;
623 ApiResult::setContent( $entry, $link );
624 $result[] = $entry;
627 return $result;
630 private function formatLimitReportData( $limitReportData ) {
631 $result = array();
632 $apiResult = $this->getResult();
634 foreach ( $limitReportData as $name => $value ) {
635 $entry = array();
636 $entry['name'] = $name;
637 if ( !is_array( $value ) ) {
638 $value = array( $value );
640 $apiResult->setIndexedTagName( $value, 'param' );
641 $apiResult->setIndexedTagName_recursive( $value, 'param' );
642 $entry = array_merge( $entry, $value );
643 $result[] = $entry;
646 return $result;
649 private function setIndexedTagNames( &$array, $mapping ) {
650 foreach ( $mapping as $key => $name ) {
651 if ( isset( $array[$key] ) ) {
652 $this->getResult()->setIndexedTagName( $array[$key], $name );
657 public function getAllowedParams() {
658 return array(
659 'title' => null,
660 'text' => null,
661 'summary' => null,
662 'page' => null,
663 'pageid' => array(
664 ApiBase::PARAM_TYPE => 'integer',
666 'redirects' => false,
667 'oldid' => array(
668 ApiBase::PARAM_TYPE => 'integer',
670 'prop' => array(
671 ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' .
672 'images|externallinks|sections|revid|displaytitle|iwlinks|properties',
673 ApiBase::PARAM_ISMULTI => true,
674 ApiBase::PARAM_TYPE => array(
675 'text',
676 'langlinks',
677 'categories',
678 'categorieshtml',
679 'links',
680 'templates',
681 'images',
682 'externallinks',
683 'sections',
684 'revid',
685 'displaytitle',
686 'headitems',
687 'headhtml',
688 'modules',
689 'indicators',
690 'iwlinks',
691 'wikitext',
692 'properties',
693 'limitreportdata',
694 'limitreporthtml',
697 'pst' => false,
698 'onlypst' => false,
699 'effectivelanglinks' => false,
700 'section' => null,
701 'disablepp' => false,
702 'disableeditsection' => false,
703 'generatexml' => array(
704 ApiBase::PARAM_DFLT => false,
705 ApiBase::PARAM_HELP_MSG => array(
706 'apihelp-parse-param-generatexml', CONTENT_MODEL_WIKITEXT
709 'preview' => false,
710 'sectionpreview' => false,
711 'disabletoc' => false,
712 'contentformat' => array(
713 ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
715 'contentmodel' => array(
716 ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
721 protected function getExamplesMessages() {
722 return array(
723 'action=parse&page=Project:Sandbox'
724 => 'apihelp-parse-example-page',
725 'action=parse&text={{Project:Sandbox}}&contentmodel=wikitext'
726 => 'apihelp-parse-example-text',
727 'action=parse&text={{PAGENAME}}&title=Test'
728 => 'apihelp-parse-example-texttitle',
729 'action=parse&summary=Some+[[link]]&prop='
730 => 'apihelp-parse-example-summary',
734 public function getHelpUrls() {
735 return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#parse';