PageRenderTime 123ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 3ms

/includes/parser/Parser.php

https://bitbucket.org/ghostfreeman/freeside-wiki
PHP | 5824 lines | 3544 code | 502 blank | 1778 comment | 693 complexity | 9d59990fe5fcb7ecb49bf426312bf069 MD5 | raw file
Possible License(s): GPL-2.0, Apache-2.0, LGPL-3.0
  1. <?php
  2. /**
  3. * PHP parser that converts wiki markup to HTML.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Parser
  22. */
  23. /**
  24. * @defgroup Parser Parser
  25. */
  26. /**
  27. * PHP Parser - Processes wiki markup (which uses a more user-friendly
  28. * syntax, such as "[[link]]" for making links), and provides a one-way
  29. * transformation of that wiki markup it into XHTML output / markup
  30. * (which in turn the browser understands, and can display).
  31. *
  32. * There are seven main entry points into the Parser class:
  33. *
  34. * - Parser::parse()
  35. * produces HTML output
  36. * - Parser::preSaveTransform().
  37. * produces altered wiki markup.
  38. * - Parser::preprocess()
  39. * removes HTML comments and expands templates
  40. * - Parser::cleanSig() and Parser::cleanSigInSig()
  41. * Cleans a signature before saving it to preferences
  42. * - Parser::getSection()
  43. * Return the content of a section from an article for section editing
  44. * - Parser::replaceSection()
  45. * Replaces a section by number inside an article
  46. * - Parser::getPreloadText()
  47. * Removes <noinclude> sections, and <includeonly> tags.
  48. *
  49. * Globals used:
  50. * object: $wgContLang
  51. *
  52. * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
  53. *
  54. * @par Settings:
  55. * $wgLocaltimezone
  56. * $wgNamespacesWithSubpages
  57. *
  58. * @par Settings only within ParserOptions:
  59. * $wgAllowExternalImages
  60. * $wgAllowSpecialInclusion
  61. * $wgInterwikiMagic
  62. * $wgMaxArticleSize
  63. * $wgUseDynamicDates
  64. *
  65. * @ingroup Parser
  66. */
  67. class Parser {
  68. /**
  69. * Update this version number when the ParserOutput format
  70. * changes in an incompatible way, so the parser cache
  71. * can automatically discard old data.
  72. */
  73. const VERSION = '1.6.4';
  74. /**
  75. * Update this version number when the output of serialiseHalfParsedText()
  76. * changes in an incompatible way
  77. */
  78. const HALF_PARSED_VERSION = 2;
  79. # Flags for Parser::setFunctionHook
  80. # Also available as global constants from Defines.php
  81. const SFH_NO_HASH = 1;
  82. const SFH_OBJECT_ARGS = 2;
  83. # Constants needed for external link processing
  84. # Everything except bracket, space, or control characters
  85. # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
  86. # as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
  87. const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
  88. const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
  89. \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
  90. # State constants for the definition list colon extraction
  91. const COLON_STATE_TEXT = 0;
  92. const COLON_STATE_TAG = 1;
  93. const COLON_STATE_TAGSTART = 2;
  94. const COLON_STATE_CLOSETAG = 3;
  95. const COLON_STATE_TAGSLASH = 4;
  96. const COLON_STATE_COMMENT = 5;
  97. const COLON_STATE_COMMENTDASH = 6;
  98. const COLON_STATE_COMMENTDASHDASH = 7;
  99. # Flags for preprocessToDom
  100. const PTD_FOR_INCLUSION = 1;
  101. # Allowed values for $this->mOutputType
  102. # Parameter to startExternalParse().
  103. const OT_HTML = 1; # like parse()
  104. const OT_WIKI = 2; # like preSaveTransform()
  105. const OT_PREPROCESS = 3; # like preprocess()
  106. const OT_MSG = 3;
  107. const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
  108. # Marker Suffix needs to be accessible staticly.
  109. const MARKER_SUFFIX = "-QINU\x7f";
  110. # Persistent:
  111. var $mTagHooks = array();
  112. var $mTransparentTagHooks = array();
  113. var $mFunctionHooks = array();
  114. var $mFunctionSynonyms = array( 0 => array(), 1 => array() );
  115. var $mFunctionTagHooks = array();
  116. var $mStripList = array();
  117. var $mDefaultStripList = array();
  118. var $mVarCache = array();
  119. var $mImageParams = array();
  120. var $mImageParamsMagicArray = array();
  121. var $mMarkerIndex = 0;
  122. var $mFirstCall = true;
  123. # Initialised by initialiseVariables()
  124. /**
  125. * @var MagicWordArray
  126. */
  127. var $mVariables;
  128. /**
  129. * @var MagicWordArray
  130. */
  131. var $mSubstWords;
  132. var $mConf, $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols; # Initialised in constructor
  133. # Cleared with clearState():
  134. /**
  135. * @var ParserOutput
  136. */
  137. var $mOutput;
  138. var $mAutonumber, $mDTopen;
  139. /**
  140. * @var StripState
  141. */
  142. var $mStripState;
  143. var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
  144. /**
  145. * @var LinkHolderArray
  146. */
  147. var $mLinkHolders;
  148. var $mLinkID;
  149. var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
  150. var $mDefaultSort;
  151. var $mTplExpandCache; # empty-frame expansion cache
  152. var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
  153. var $mExpensiveFunctionCount; # number of expensive parser function calls
  154. var $mShowToc, $mForceTocPosition;
  155. /**
  156. * @var User
  157. */
  158. var $mUser; # User object; only used when doing pre-save transform
  159. # Temporary
  160. # These are variables reset at least once per parse regardless of $clearState
  161. /**
  162. * @var ParserOptions
  163. */
  164. var $mOptions;
  165. /**
  166. * @var Title
  167. */
  168. var $mTitle; # Title context, used for self-link rendering and similar things
  169. var $mOutputType; # Output type, one of the OT_xxx constants
  170. var $ot; # Shortcut alias, see setOutputType()
  171. var $mRevisionObject; # The revision object of the specified revision ID
  172. var $mRevisionId; # ID to display in {{REVISIONID}} tags
  173. var $mRevisionTimestamp; # The timestamp of the specified revision ID
  174. var $mRevisionUser; # User to display in {{REVISIONUSER}} tag
  175. var $mRevIdForTs; # The revision ID which was used to fetch the timestamp
  176. /**
  177. * @var string
  178. */
  179. var $mUniqPrefix;
  180. /**
  181. * Constructor
  182. *
  183. * @param $conf array
  184. */
  185. public function __construct( $conf = array() ) {
  186. $this->mConf = $conf;
  187. $this->mUrlProtocols = wfUrlProtocols();
  188. $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')'.
  189. self::EXT_LINK_URL_CLASS.'+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
  190. if ( isset( $conf['preprocessorClass'] ) ) {
  191. $this->mPreprocessorClass = $conf['preprocessorClass'];
  192. } elseif ( defined( 'MW_COMPILED' ) ) {
  193. # Preprocessor_Hash is much faster than Preprocessor_DOM in compiled mode
  194. $this->mPreprocessorClass = 'Preprocessor_Hash';
  195. } elseif ( extension_loaded( 'domxml' ) ) {
  196. # PECL extension that conflicts with the core DOM extension (bug 13770)
  197. wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
  198. $this->mPreprocessorClass = 'Preprocessor_Hash';
  199. } elseif ( extension_loaded( 'dom' ) ) {
  200. $this->mPreprocessorClass = 'Preprocessor_DOM';
  201. } else {
  202. $this->mPreprocessorClass = 'Preprocessor_Hash';
  203. }
  204. wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
  205. }
  206. /**
  207. * Reduce memory usage to reduce the impact of circular references
  208. */
  209. function __destruct() {
  210. if ( isset( $this->mLinkHolders ) ) {
  211. unset( $this->mLinkHolders );
  212. }
  213. foreach ( $this as $name => $value ) {
  214. unset( $this->$name );
  215. }
  216. }
  217. /**
  218. * Do various kinds of initialisation on the first call of the parser
  219. */
  220. function firstCallInit() {
  221. if ( !$this->mFirstCall ) {
  222. return;
  223. }
  224. $this->mFirstCall = false;
  225. wfProfileIn( __METHOD__ );
  226. CoreParserFunctions::register( $this );
  227. CoreTagHooks::register( $this );
  228. $this->initialiseVariables();
  229. wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
  230. wfProfileOut( __METHOD__ );
  231. }
  232. /**
  233. * Clear Parser state
  234. *
  235. * @private
  236. */
  237. function clearState() {
  238. wfProfileIn( __METHOD__ );
  239. if ( $this->mFirstCall ) {
  240. $this->firstCallInit();
  241. }
  242. $this->mOutput = new ParserOutput;
  243. $this->mOptions->registerWatcher( array( $this->mOutput, 'recordOption' ) );
  244. $this->mAutonumber = 0;
  245. $this->mLastSection = '';
  246. $this->mDTopen = false;
  247. $this->mIncludeCount = array();
  248. $this->mArgStack = false;
  249. $this->mInPre = false;
  250. $this->mLinkHolders = new LinkHolderArray( $this );
  251. $this->mLinkID = 0;
  252. $this->mRevisionObject = $this->mRevisionTimestamp =
  253. $this->mRevisionId = $this->mRevisionUser = null;
  254. $this->mVarCache = array();
  255. $this->mUser = null;
  256. /**
  257. * Prefix for temporary replacement strings for the multipass parser.
  258. * \x07 should never appear in input as it's disallowed in XML.
  259. * Using it at the front also gives us a little extra robustness
  260. * since it shouldn't match when butted up against identifier-like
  261. * string constructs.
  262. *
  263. * Must not consist of all title characters, or else it will change
  264. * the behaviour of <nowiki> in a link.
  265. */
  266. $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
  267. $this->mStripState = new StripState( $this->mUniqPrefix );
  268. # Clear these on every parse, bug 4549
  269. $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
  270. $this->mShowToc = true;
  271. $this->mForceTocPosition = false;
  272. $this->mIncludeSizes = array(
  273. 'post-expand' => 0,
  274. 'arg' => 0,
  275. );
  276. $this->mPPNodeCount = 0;
  277. $this->mGeneratedPPNodeCount = 0;
  278. $this->mHighestExpansionDepth = 0;
  279. $this->mDefaultSort = false;
  280. $this->mHeadings = array();
  281. $this->mDoubleUnderscores = array();
  282. $this->mExpensiveFunctionCount = 0;
  283. # Fix cloning
  284. if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
  285. $this->mPreprocessor = null;
  286. }
  287. wfRunHooks( 'ParserClearState', array( &$this ) );
  288. wfProfileOut( __METHOD__ );
  289. }
  290. /**
  291. * Convert wikitext to HTML
  292. * Do not call this function recursively.
  293. *
  294. * @param $text String: text we want to parse
  295. * @param $title Title object
  296. * @param $options ParserOptions
  297. * @param $linestart boolean
  298. * @param $clearState boolean
  299. * @param $revid Int: number to pass in {{REVISIONID}}
  300. * @return ParserOutput a ParserOutput
  301. */
  302. public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) {
  303. /**
  304. * First pass--just handle <nowiki> sections, pass the rest off
  305. * to internalParse() which does all the real work.
  306. */
  307. global $wgUseTidy, $wgAlwaysUseTidy;
  308. $fname = __METHOD__.'-' . wfGetCaller();
  309. wfProfileIn( __METHOD__ );
  310. wfProfileIn( $fname );
  311. $this->startParse( $title, $options, self::OT_HTML, $clearState );
  312. # Remove the strip marker tag prefix from the input, if present.
  313. if ( $clearState ) {
  314. $text = str_replace( $this->mUniqPrefix, '', $text );
  315. }
  316. $oldRevisionId = $this->mRevisionId;
  317. $oldRevisionObject = $this->mRevisionObject;
  318. $oldRevisionTimestamp = $this->mRevisionTimestamp;
  319. $oldRevisionUser = $this->mRevisionUser;
  320. if ( $revid !== null ) {
  321. $this->mRevisionId = $revid;
  322. $this->mRevisionObject = null;
  323. $this->mRevisionTimestamp = null;
  324. $this->mRevisionUser = null;
  325. }
  326. wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
  327. # No more strip!
  328. wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
  329. $text = $this->internalParse( $text );
  330. wfRunHooks( 'ParserAfterParse', array( &$this, &$text, &$this->mStripState ) );
  331. $text = $this->mStripState->unstripGeneral( $text );
  332. # Clean up special characters, only run once, next-to-last before doBlockLevels
  333. $fixtags = array(
  334. # french spaces, last one Guillemet-left
  335. # only if there is something before the space
  336. '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
  337. # french spaces, Guillemet-right
  338. '/(\\302\\253) /' => '\\1&#160;',
  339. '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
  340. );
  341. $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
  342. $text = $this->doBlockLevels( $text, $linestart );
  343. $this->replaceLinkHolders( $text );
  344. /**
  345. * The input doesn't get language converted if
  346. * a) It's disabled
  347. * b) Content isn't converted
  348. * c) It's a conversion table
  349. * d) it is an interface message (which is in the user language)
  350. */
  351. if ( !( $options->getDisableContentConversion()
  352. || isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) )
  353. {
  354. # Run convert unconditionally in 1.18-compatible mode
  355. global $wgBug34832TransitionalRollback;
  356. if ( $wgBug34832TransitionalRollback || !$this->mOptions->getInterfaceMessage() ) {
  357. # The position of the convert() call should not be changed. it
  358. # assumes that the links are all replaced and the only thing left
  359. # is the <nowiki> mark.
  360. $text = $this->getConverterLanguage()->convert( $text );
  361. }
  362. }
  363. /**
  364. * A converted title will be provided in the output object if title and
  365. * content conversion are enabled, the article text does not contain
  366. * a conversion-suppressing double-underscore tag, and no
  367. * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
  368. * automatic link conversion.
  369. */
  370. if ( !( $options->getDisableTitleConversion()
  371. || isset( $this->mDoubleUnderscores['nocontentconvert'] )
  372. || isset( $this->mDoubleUnderscores['notitleconvert'] )
  373. || $this->mOutput->getDisplayTitle() !== false ) )
  374. {
  375. $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
  376. if ( $convruletitle ) {
  377. $this->mOutput->setTitleText( $convruletitle );
  378. } else {
  379. $titleText = $this->getConverterLanguage()->convertTitle( $title );
  380. $this->mOutput->setTitleText( $titleText );
  381. }
  382. }
  383. $text = $this->mStripState->unstripNoWiki( $text );
  384. wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
  385. $text = $this->replaceTransparentTags( $text );
  386. $text = $this->mStripState->unstripGeneral( $text );
  387. $text = Sanitizer::normalizeCharReferences( $text );
  388. if ( ( $wgUseTidy && $this->mOptions->getTidy() ) || $wgAlwaysUseTidy ) {
  389. $text = MWTidy::tidy( $text );
  390. } else {
  391. # attempt to sanitize at least some nesting problems
  392. # (bug #2702 and quite a few others)
  393. $tidyregs = array(
  394. # ''Something [http://www.cool.com cool''] -->
  395. # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
  396. '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
  397. '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
  398. # fix up an anchor inside another anchor, only
  399. # at least for a single single nested link (bug 3695)
  400. '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
  401. '\\1\\2</a>\\3</a>\\1\\4</a>',
  402. # fix div inside inline elements- doBlockLevels won't wrap a line which
  403. # contains a div, so fix it up here; replace
  404. # div with escaped text
  405. '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
  406. '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
  407. # remove empty italic or bold tag pairs, some
  408. # introduced by rules above
  409. '/<([bi])><\/\\1>/' => '',
  410. );
  411. $text = preg_replace(
  412. array_keys( $tidyregs ),
  413. array_values( $tidyregs ),
  414. $text );
  415. }
  416. if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
  417. $this->limitationWarn( 'expensive-parserfunction',
  418. $this->mExpensiveFunctionCount,
  419. $this->mOptions->getExpensiveParserFunctionLimit()
  420. );
  421. }
  422. wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
  423. # Information on include size limits, for the benefit of users who try to skirt them
  424. if ( $this->mOptions->getEnableLimitReport() ) {
  425. $max = $this->mOptions->getMaxIncludeSize();
  426. $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/{$this->mOptions->getExpensiveParserFunctionLimit()}\n";
  427. $limitReport =
  428. "NewPP limit report\n" .
  429. "Preprocessor visited node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\n" .
  430. "Preprocessor generated node count: " .
  431. "{$this->mGeneratedPPNodeCount}/{$this->mOptions->getMaxGeneratedPPNodeCount()}\n" .
  432. "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
  433. "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
  434. "Highest expansion depth: {$this->mHighestExpansionDepth}/{$this->mOptions->getMaxPPExpandDepth()}\n".
  435. $PFreport;
  436. wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
  437. $text .= "\n<!-- \n$limitReport-->\n";
  438. }
  439. $this->mOutput->setText( $text );
  440. $this->mRevisionId = $oldRevisionId;
  441. $this->mRevisionObject = $oldRevisionObject;
  442. $this->mRevisionTimestamp = $oldRevisionTimestamp;
  443. $this->mRevisionUser = $oldRevisionUser;
  444. wfProfileOut( $fname );
  445. wfProfileOut( __METHOD__ );
  446. return $this->mOutput;
  447. }
  448. /**
  449. * Recursive parser entry point that can be called from an extension tag
  450. * hook.
  451. *
  452. * If $frame is not provided, then template variables (e.g., {{{1}}}) within $text are not expanded
  453. *
  454. * @param $text String: text extension wants to have parsed
  455. * @param $frame PPFrame: The frame to use for expanding any template variables
  456. *
  457. * @return string
  458. */
  459. function recursiveTagParse( $text, $frame=false ) {
  460. wfProfileIn( __METHOD__ );
  461. wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
  462. wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
  463. $text = $this->internalParse( $text, false, $frame );
  464. wfProfileOut( __METHOD__ );
  465. return $text;
  466. }
  467. /**
  468. * Expand templates and variables in the text, producing valid, static wikitext.
  469. * Also removes comments.
  470. * @return mixed|string
  471. */
  472. function preprocess( $text, Title $title, ParserOptions $options, $revid = null ) {
  473. wfProfileIn( __METHOD__ );
  474. $this->startParse( $title, $options, self::OT_PREPROCESS, true );
  475. if ( $revid !== null ) {
  476. $this->mRevisionId = $revid;
  477. }
  478. wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
  479. wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
  480. $text = $this->replaceVariables( $text );
  481. $text = $this->mStripState->unstripBoth( $text );
  482. wfProfileOut( __METHOD__ );
  483. return $text;
  484. }
  485. /**
  486. * Recursive parser entry point that can be called from an extension tag
  487. * hook.
  488. *
  489. * @param $text String: text to be expanded
  490. * @param $frame PPFrame: The frame to use for expanding any template variables
  491. * @return String
  492. * @since 1.19
  493. */
  494. public function recursivePreprocess( $text, $frame = false ) {
  495. wfProfileIn( __METHOD__ );
  496. $text = $this->replaceVariables( $text, $frame );
  497. $text = $this->mStripState->unstripBoth( $text );
  498. wfProfileOut( __METHOD__ );
  499. return $text;
  500. }
  501. /**
  502. * Process the wikitext for the "?preload=" feature. (bug 5210)
  503. *
  504. * "<noinclude>", "<includeonly>" etc. are parsed as for template
  505. * transclusion, comments, templates, arguments, tags hooks and parser
  506. * functions are untouched.
  507. *
  508. * @param $text String
  509. * @param $title Title
  510. * @param $options ParserOptions
  511. * @return String
  512. */
  513. public function getPreloadText( $text, Title $title, ParserOptions $options ) {
  514. # Parser (re)initialisation
  515. $this->startParse( $title, $options, self::OT_PLAIN, true );
  516. $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
  517. $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
  518. $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
  519. $text = $this->mStripState->unstripBoth( $text );
  520. return $text;
  521. }
  522. /**
  523. * Get a random string
  524. *
  525. * @return string
  526. */
  527. static public function getRandomString() {
  528. return wfRandomString( 16 );
  529. }
  530. /**
  531. * Set the current user.
  532. * Should only be used when doing pre-save transform.
  533. *
  534. * @param $user Mixed: User object or null (to reset)
  535. */
  536. function setUser( $user ) {
  537. $this->mUser = $user;
  538. }
  539. /**
  540. * Accessor for mUniqPrefix.
  541. *
  542. * @return String
  543. */
  544. public function uniqPrefix() {
  545. if ( !isset( $this->mUniqPrefix ) ) {
  546. # @todo FIXME: This is probably *horribly wrong*
  547. # LanguageConverter seems to want $wgParser's uniqPrefix, however
  548. # if this is called for a parser cache hit, the parser may not
  549. # have ever been initialized in the first place.
  550. # Not really sure what the heck is supposed to be going on here.
  551. return '';
  552. # throw new MWException( "Accessing uninitialized mUniqPrefix" );
  553. }
  554. return $this->mUniqPrefix;
  555. }
  556. /**
  557. * Set the context title
  558. *
  559. * @param $t Title
  560. */
  561. function setTitle( $t ) {
  562. if ( !$t || $t instanceof FakeTitle ) {
  563. $t = Title::newFromText( 'NO TITLE' );
  564. }
  565. if ( strval( $t->getFragment() ) !== '' ) {
  566. # Strip the fragment to avoid various odd effects
  567. $this->mTitle = clone $t;
  568. $this->mTitle->setFragment( '' );
  569. } else {
  570. $this->mTitle = $t;
  571. }
  572. }
  573. /**
  574. * Accessor for the Title object
  575. *
  576. * @return Title object
  577. */
  578. function getTitle() {
  579. return $this->mTitle;
  580. }
  581. /**
  582. * Accessor/mutator for the Title object
  583. *
  584. * @param $x Title object or null to just get the current one
  585. * @return Title object
  586. */
  587. function Title( $x = null ) {
  588. return wfSetVar( $this->mTitle, $x );
  589. }
  590. /**
  591. * Set the output type
  592. *
  593. * @param $ot Integer: new value
  594. */
  595. function setOutputType( $ot ) {
  596. $this->mOutputType = $ot;
  597. # Shortcut alias
  598. $this->ot = array(
  599. 'html' => $ot == self::OT_HTML,
  600. 'wiki' => $ot == self::OT_WIKI,
  601. 'pre' => $ot == self::OT_PREPROCESS,
  602. 'plain' => $ot == self::OT_PLAIN,
  603. );
  604. }
  605. /**
  606. * Accessor/mutator for the output type
  607. *
  608. * @param $x int|null New value or null to just get the current one
  609. * @return Integer
  610. */
  611. function OutputType( $x = null ) {
  612. return wfSetVar( $this->mOutputType, $x );
  613. }
  614. /**
  615. * Get the ParserOutput object
  616. *
  617. * @return ParserOutput object
  618. */
  619. function getOutput() {
  620. return $this->mOutput;
  621. }
  622. /**
  623. * Get the ParserOptions object
  624. *
  625. * @return ParserOptions object
  626. */
  627. function getOptions() {
  628. return $this->mOptions;
  629. }
  630. /**
  631. * Accessor/mutator for the ParserOptions object
  632. *
  633. * @param $x ParserOptions New value or null to just get the current one
  634. * @return ParserOptions Current ParserOptions object
  635. */
  636. function Options( $x = null ) {
  637. return wfSetVar( $this->mOptions, $x );
  638. }
  639. /**
  640. * @return int
  641. */
  642. function nextLinkID() {
  643. return $this->mLinkID++;
  644. }
  645. /**
  646. * @param $id int
  647. */
  648. function setLinkID( $id ) {
  649. $this->mLinkID = $id;
  650. }
  651. /**
  652. * Get a language object for use in parser functions such as {{FORMATNUM:}}
  653. * @return Language
  654. */
  655. function getFunctionLang() {
  656. return $this->getTargetLanguage();
  657. }
  658. /**
  659. * Get the target language for the content being parsed. This is usually the
  660. * language that the content is in.
  661. *
  662. * @since 1.19
  663. *
  664. * @return Language|null
  665. */
  666. public function getTargetLanguage() {
  667. $target = $this->mOptions->getTargetLanguage();
  668. if ( $target !== null ) {
  669. return $target;
  670. } elseif( $this->mOptions->getInterfaceMessage() ) {
  671. return $this->mOptions->getUserLangObj();
  672. } elseif( is_null( $this->mTitle ) ) {
  673. throw new MWException( __METHOD__ . ': $this->mTitle is null' );
  674. }
  675. return $this->mTitle->getPageLanguage();
  676. }
  677. /**
  678. * Get the language object for language conversion
  679. */
  680. function getConverterLanguage() {
  681. global $wgBug34832TransitionalRollback, $wgContLang;
  682. if ( $wgBug34832TransitionalRollback ) {
  683. return $wgContLang;
  684. } else {
  685. return $this->getTargetLanguage();
  686. }
  687. }
  688. /**
  689. * Get a User object either from $this->mUser, if set, or from the
  690. * ParserOptions object otherwise
  691. *
  692. * @return User object
  693. */
  694. function getUser() {
  695. if ( !is_null( $this->mUser ) ) {
  696. return $this->mUser;
  697. }
  698. return $this->mOptions->getUser();
  699. }
  700. /**
  701. * Get a preprocessor object
  702. *
  703. * @return Preprocessor instance
  704. */
  705. function getPreprocessor() {
  706. if ( !isset( $this->mPreprocessor ) ) {
  707. $class = $this->mPreprocessorClass;
  708. $this->mPreprocessor = new $class( $this );
  709. }
  710. return $this->mPreprocessor;
  711. }
  712. /**
  713. * Replaces all occurrences of HTML-style comments and the given tags
  714. * in the text with a random marker and returns the next text. The output
  715. * parameter $matches will be an associative array filled with data in
  716. * the form:
  717. *
  718. * @code
  719. * 'UNIQ-xxxxx' => array(
  720. * 'element',
  721. * 'tag content',
  722. * array( 'param' => 'x' ),
  723. * '<element param="x">tag content</element>' ) )
  724. * @endcode
  725. *
  726. * @param $elements array list of element names. Comments are always extracted.
  727. * @param $text string Source text string.
  728. * @param $matches array Out parameter, Array: extracted tags
  729. * @param $uniq_prefix string
  730. * @return String: stripped text
  731. */
  732. public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = '' ) {
  733. static $n = 1;
  734. $stripped = '';
  735. $matches = array();
  736. $taglist = implode( '|', $elements );
  737. $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
  738. while ( $text != '' ) {
  739. $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
  740. $stripped .= $p[0];
  741. if ( count( $p ) < 5 ) {
  742. break;
  743. }
  744. if ( count( $p ) > 5 ) {
  745. # comment
  746. $element = $p[4];
  747. $attributes = '';
  748. $close = '';
  749. $inside = $p[5];
  750. } else {
  751. # tag
  752. $element = $p[1];
  753. $attributes = $p[2];
  754. $close = $p[3];
  755. $inside = $p[4];
  756. }
  757. $marker = "$uniq_prefix-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
  758. $stripped .= $marker;
  759. if ( $close === '/>' ) {
  760. # Empty element tag, <tag />
  761. $content = null;
  762. $text = $inside;
  763. $tail = null;
  764. } else {
  765. if ( $element === '!--' ) {
  766. $end = '/(-->)/';
  767. } else {
  768. $end = "/(<\\/$element\\s*>)/i";
  769. }
  770. $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
  771. $content = $q[0];
  772. if ( count( $q ) < 3 ) {
  773. # No end tag -- let it run out to the end of the text.
  774. $tail = '';
  775. $text = '';
  776. } else {
  777. $tail = $q[1];
  778. $text = $q[2];
  779. }
  780. }
  781. $matches[$marker] = array( $element,
  782. $content,
  783. Sanitizer::decodeTagAttributes( $attributes ),
  784. "<$element$attributes$close$content$tail" );
  785. }
  786. return $stripped;
  787. }
  788. /**
  789. * Get a list of strippable XML-like elements
  790. *
  791. * @return array
  792. */
  793. function getStripList() {
  794. return $this->mStripList;
  795. }
  796. /**
  797. * Add an item to the strip state
  798. * Returns the unique tag which must be inserted into the stripped text
  799. * The tag will be replaced with the original text in unstrip()
  800. *
  801. * @param $text string
  802. *
  803. * @return string
  804. */
  805. function insertStripItem( $text ) {
  806. $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
  807. $this->mMarkerIndex++;
  808. $this->mStripState->addGeneral( $rnd, $text );
  809. return $rnd;
  810. }
  811. /**
  812. * parse the wiki syntax used to render tables
  813. *
  814. * @private
  815. * @return string
  816. */
  817. function doTableStuff( $text ) {
  818. wfProfileIn( __METHOD__ );
  819. $lines = StringUtils::explode( "\n", $text );
  820. $out = '';
  821. $td_history = array(); # Is currently a td tag open?
  822. $last_tag_history = array(); # Save history of last lag activated (td, th or caption)
  823. $tr_history = array(); # Is currently a tr tag open?
  824. $tr_attributes = array(); # history of tr attributes
  825. $has_opened_tr = array(); # Did this table open a <tr> element?
  826. $indent_level = 0; # indent level of the table
  827. foreach ( $lines as $outLine ) {
  828. $line = trim( $outLine );
  829. if ( $line === '' ) { # empty line, go to next line
  830. $out .= $outLine."\n";
  831. continue;
  832. }
  833. $first_character = $line[0];
  834. $matches = array();
  835. if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) {
  836. # First check if we are starting a new table
  837. $indent_level = strlen( $matches[1] );
  838. $attributes = $this->mStripState->unstripBoth( $matches[2] );
  839. $attributes = Sanitizer::fixTagAttributes( $attributes , 'table' );
  840. $outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
  841. array_push( $td_history , false );
  842. array_push( $last_tag_history , '' );
  843. array_push( $tr_history , false );
  844. array_push( $tr_attributes , '' );
  845. array_push( $has_opened_tr , false );
  846. } elseif ( count( $td_history ) == 0 ) {
  847. # Don't do any of the following
  848. $out .= $outLine."\n";
  849. continue;
  850. } elseif ( substr( $line , 0 , 2 ) === '|}' ) {
  851. # We are ending a table
  852. $line = '</table>' . substr( $line , 2 );
  853. $last_tag = array_pop( $last_tag_history );
  854. if ( !array_pop( $has_opened_tr ) ) {
  855. $line = "<tr><td></td></tr>{$line}";
  856. }
  857. if ( array_pop( $tr_history ) ) {
  858. $line = "</tr>{$line}";
  859. }
  860. if ( array_pop( $td_history ) ) {
  861. $line = "</{$last_tag}>{$line}";
  862. }
  863. array_pop( $tr_attributes );
  864. $outLine = $line . str_repeat( '</dd></dl>' , $indent_level );
  865. } elseif ( substr( $line , 0 , 2 ) === '|-' ) {
  866. # Now we have a table row
  867. $line = preg_replace( '#^\|-+#', '', $line );
  868. # Whats after the tag is now only attributes
  869. $attributes = $this->mStripState->unstripBoth( $line );
  870. $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
  871. array_pop( $tr_attributes );
  872. array_push( $tr_attributes, $attributes );
  873. $line = '';
  874. $last_tag = array_pop( $last_tag_history );
  875. array_pop( $has_opened_tr );
  876. array_push( $has_opened_tr , true );
  877. if ( array_pop( $tr_history ) ) {
  878. $line = '</tr>';
  879. }
  880. if ( array_pop( $td_history ) ) {
  881. $line = "</{$last_tag}>{$line}";
  882. }
  883. $outLine = $line;
  884. array_push( $tr_history , false );
  885. array_push( $td_history , false );
  886. array_push( $last_tag_history , '' );
  887. } elseif ( $first_character === '|' || $first_character === '!' || substr( $line , 0 , 2 ) === '|+' ) {
  888. # This might be cell elements, td, th or captions
  889. if ( substr( $line , 0 , 2 ) === '|+' ) {
  890. $first_character = '+';
  891. $line = substr( $line , 1 );
  892. }
  893. $line = substr( $line , 1 );
  894. if ( $first_character === '!' ) {
  895. $line = str_replace( '!!' , '||' , $line );
  896. }
  897. # Split up multiple cells on the same line.
  898. # FIXME : This can result in improper nesting of tags processed
  899. # by earlier parser steps, but should avoid splitting up eg
  900. # attribute values containing literal "||".
  901. $cells = StringUtils::explodeMarkup( '||' , $line );
  902. $outLine = '';
  903. # Loop through each table cell
  904. foreach ( $cells as $cell ) {
  905. $previous = '';
  906. if ( $first_character !== '+' ) {
  907. $tr_after = array_pop( $tr_attributes );
  908. if ( !array_pop( $tr_history ) ) {
  909. $previous = "<tr{$tr_after}>\n";
  910. }
  911. array_push( $tr_history , true );
  912. array_push( $tr_attributes , '' );
  913. array_pop( $has_opened_tr );
  914. array_push( $has_opened_tr , true );
  915. }
  916. $last_tag = array_pop( $last_tag_history );
  917. if ( array_pop( $td_history ) ) {
  918. $previous = "</{$last_tag}>\n{$previous}";
  919. }
  920. if ( $first_character === '|' ) {
  921. $last_tag = 'td';
  922. } elseif ( $first_character === '!' ) {
  923. $last_tag = 'th';
  924. } elseif ( $first_character === '+' ) {
  925. $last_tag = 'caption';
  926. } else {
  927. $last_tag = '';
  928. }
  929. array_push( $last_tag_history , $last_tag );
  930. # A cell could contain both parameters and data
  931. $cell_data = explode( '|' , $cell , 2 );
  932. # Bug 553: Note that a '|' inside an invalid link should not
  933. # be mistaken as delimiting cell parameters
  934. if ( strpos( $cell_data[0], '[[' ) !== false ) {
  935. $cell = "{$previous}<{$last_tag}>{$cell}";
  936. } elseif ( count( $cell_data ) == 1 ) {
  937. $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
  938. } else {
  939. $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
  940. $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
  941. $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
  942. }
  943. $outLine .= $cell;
  944. array_push( $td_history , true );
  945. }
  946. }
  947. $out .= $outLine . "\n";
  948. }
  949. # Closing open td, tr && table
  950. while ( count( $td_history ) > 0 ) {
  951. if ( array_pop( $td_history ) ) {
  952. $out .= "</td>\n";
  953. }
  954. if ( array_pop( $tr_history ) ) {
  955. $out .= "</tr>\n";
  956. }
  957. if ( !array_pop( $has_opened_tr ) ) {
  958. $out .= "<tr><td></td></tr>\n" ;
  959. }
  960. $out .= "</table>\n";
  961. }
  962. # Remove trailing line-ending (b/c)
  963. if ( substr( $out, -1 ) === "\n" ) {
  964. $out = substr( $out, 0, -1 );
  965. }
  966. # special case: don't return empty table
  967. if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
  968. $out = '';
  969. }
  970. wfProfileOut( __METHOD__ );
  971. return $out;
  972. }
  973. /**
  974. * Helper function for parse() that transforms wiki markup into
  975. * HTML. Only called for $mOutputType == self::OT_HTML.
  976. *
  977. * @private
  978. *
  979. * @param $text string
  980. * @param $isMain bool
  981. * @param $frame bool
  982. *
  983. * @return string
  984. */
  985. function internalParse( $text, $isMain = true, $frame = false ) {
  986. wfProfileIn( __METHOD__ );
  987. $origText = $text;
  988. # Hook to suspend the parser in this state
  989. if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
  990. wfProfileOut( __METHOD__ );
  991. return $text ;
  992. }
  993. # if $frame is provided, then use $frame for replacing any variables
  994. if ( $frame ) {
  995. # use frame depth to infer how include/noinclude tags should be handled
  996. # depth=0 means this is the top-level document; otherwise it's an included document
  997. if ( !$frame->depth ) {
  998. $flag = 0;
  999. } else {
  1000. $flag = Parser::PTD_FOR_INCLUSION;
  1001. }
  1002. $dom = $this->preprocessToDom( $text, $flag );
  1003. $text = $frame->expand( $dom );
  1004. } else {
  1005. # if $frame is not provided, then use old-style replaceVariables
  1006. $text = $this->replaceVariables( $text );
  1007. }
  1008. wfRunHooks( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) );
  1009. $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
  1010. wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
  1011. # Tables need to come after variable replacement for things to work
  1012. # properly; putting them before other transformations should keep
  1013. # exciting things like link expansions from showing up in surprising
  1014. # places.
  1015. $text = $this->doTableStuff( $text );
  1016. $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
  1017. $text = $this->doDoubleUnderscore( $text );
  1018. $text = $this->doHeadings( $text );
  1019. if ( $this->mOptions->getUseDynamicDates() ) {
  1020. $df = DateFormatter::getInstance();
  1021. $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
  1022. }
  1023. $text = $this->replaceInternalLinks( $text );
  1024. $text = $this->doAllQuotes( $text );
  1025. $text = $this->replaceExternalLinks( $text );
  1026. # replaceInternalLinks may sometimes leave behind
  1027. # absolute URLs, which have to be masked to hide them from replaceExternalLinks
  1028. $text = str_replace( $this->mUniqPrefix.'NOPARSE', '', $text );
  1029. $text = $this->doMagicLinks( $text );
  1030. $text = $this->formatHeadings( $text, $origText, $isMain );
  1031. wfProfileOut( __METHOD__ );
  1032. return $text;
  1033. }
  1034. /**
  1035. * Replace special strings like "ISBN xxx" and "RFC xxx" with
  1036. * magic external links.
  1037. *
  1038. * DML
  1039. * @private
  1040. *
  1041. * @param $text string
  1042. *
  1043. * @return string
  1044. */
  1045. function doMagicLinks( $text ) {
  1046. wfProfileIn( __METHOD__ );
  1047. $prots = wfUrlProtocolsWithoutProtRel();
  1048. $urlChar = self::EXT_LINK_URL_CLASS;
  1049. $text = preg_replace_callback(
  1050. '!(?: # Start cases
  1051. (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
  1052. (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
  1053. (\\b(?i:$prots)$urlChar+) | # m[3]: Free external links" . '
  1054. (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number
  1055. ISBN\s+(\b # m[5]: ISBN, capture number
  1056. (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
  1057. (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters
  1058. [0-9Xx] # check digit
  1059. \b)
  1060. )!xu', array( &$this, 'magicLinkCallback' ), $text );
  1061. wfProfileOut( __METHOD__ );
  1062. return $text;
  1063. }
  1064. /**
  1065. * @throws MWException
  1066. * @param $m array
  1067. * @return HTML|string
  1068. */
  1069. function magicLinkCallback( $m ) {
  1070. if ( isset( $m[1] ) && $m[1] !== '' ) {
  1071. # Skip anchor
  1072. return $m[0];
  1073. } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
  1074. # Skip HTML element
  1075. return $m[0];
  1076. } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
  1077. # Free external link
  1078. return $this->makeFreeExternalLink( $m[0] );
  1079. } elseif ( isset( $m[4] ) && $m[4] !== '' ) {
  1080. # RFC or PMID
  1081. if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
  1082. $keyword = 'RFC';
  1083. $urlmsg = 'rfcurl';
  1084. $CssClass = 'mw-magiclink-rfc';
  1085. $id = $m[4];
  1086. } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
  1087. $keyword = 'PMID';
  1088. $urlmsg = 'pubmedurl';
  1089. $CssClass = 'mw-magiclink-pmid';
  1090. $id = $m[4];
  1091. } else {
  1092. throw new MWException( __METHOD__.': unrecognised match type "' .
  1093. substr( $m[0], 0, 20 ) . '"' );
  1094. }
  1095. $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
  1096. return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $CssClass );
  1097. } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
  1098. # ISBN
  1099. $isbn = $m[5];
  1100. $num = strtr( $isbn, array(
  1101. '-' => '',
  1102. ' ' => '',
  1103. 'x' => 'X',
  1104. ));
  1105. $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
  1106. return'<a href="' .
  1107. htmlspecialchars( $titleObj->getLocalUrl() ) .
  1108. "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
  1109. } else {
  1110. return $m[0];
  1111. }
  1112. }
  1113. /**
  1114. * Make a free external link, given a user-supplied URL
  1115. *
  1116. * @param $url string
  1117. *
  1118. * @return string HTML
  1119. * @private
  1120. */
  1121. function makeFreeExternalLink( $url ) {
  1122. wfProfileIn( __METHOD__ );
  1123. $trail = '';
  1124. # The characters '<' and '>' (which were escaped by
  1125. # removeHTMLtags()) should not be included in
  1126. # URLs, per RFC 2396.
  1127. $m2 = array();
  1128. if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
  1129. $trail = substr( $url, $m2[0][1] ) . $trail;
  1130. $url = substr( $url, 0, $m2[0][1] );
  1131. }
  1132. # Move trailing punctuation to $trail
  1133. $sep = ',;\.:!?';
  1134. # If there is no left bracket, then consider right brackets fair game too
  1135. if ( strpos( $url, '(' ) === false ) {
  1136. $sep .= ')';
  1137. }
  1138. $numSepChars = strspn( strrev( $url ), $sep );
  1139. if ( $numSepChars ) {
  1140. $trail = substr( $url, -$numSepChars ) . $trail;
  1141. $url = substr( $url, 0, -$numSepChars );
  1142. }
  1143. $url = Sanitizer::cleanUrl( $url );
  1144. # Is this an external image?
  1145. $text = $this->maybeMakeExternalImage( $url );
  1146. if ( $text === false ) {
  1147. # Not an image, make a link
  1148. $text = Linker::makeExternalLink( $url,
  1149. $this->getConverterLanguage()->markNoConversion($url), true, 'free',
  1150. $this->getExternalLinkAttribs( $url ) );
  1151. # Register it in the output object...
  1152. # Replace unnecessary URL escape codes with their equivalent characters
  1153. $pasteurized = self::replaceUnusualEscapes( $url );
  1154. $this->mOutput->addExternalLink( $pasteurized );
  1155. }
  1156. wfProfileOut( __METHOD__ );
  1157. return $text . $trail;
  1158. }
  1159. /**
  1160. * Parse headers and return html
  1161. *
  1162. * @private
  1163. *
  1164. * @param $text string
  1165. *
  1166. * @return string
  1167. */
  1168. function doHeadings( $text ) {
  1169. wfProfileIn( __METHOD__ );
  1170. for ( $i = 6; $i >= 1; --$i ) {
  1171. $h = str_repeat( '=', $i );
  1172. $text = preg_replace( "/^$h(.+)$h\\s*$/m",
  1173. "<h$i>\\1</h$i>", $text );
  1174. }
  1175. wfProfileOut( __METHOD__ );
  1176. return $text;
  1177. }
  1178. /**
  1179. * Replace single quotes with HTML markup
  1180. * @private
  1181. *
  1182. * @param $text string
  1183. *
  1184. * @return string the altered text
  1185. */
  1186. function doAllQuotes( $text ) {
  1187. wfProfileIn( __METHOD__ );
  1188. $outtext = '';
  1189. $lines = StringUtils::explode( "\n", $text );
  1190. foreach ( $lines as $line ) {
  1191. $outtext .= $this->doQuotes( $line ) . "\n";
  1192. }
  1193. $outtext = substr( $outtext, 0,-1 );
  1194. wfProfileOut( __METHOD__ );
  1195. return $outtext;
  1196. }
  1197. /**
  1198. * Helper function for doAllQuotes()
  1199. *
  1200. * @param $text string
  1201. *
  1202. * @return string
  1203. */
  1204. public function doQuotes( $text ) {
  1205. $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
  1206. if ( count( $arr ) == 1 ) {
  1207. return $text;
  1208. } else {
  1209. # First, do some preliminary work. This may shift some apostrophes from
  1210. # being mark-up to being text. It also counts the number of occurrences
  1211. # of bold and italics mark-ups.
  1212. $numbold = 0;
  1213. $numitalics = 0;
  1214. for ( $i = 0; $i < count( $arr ); $i++ ) {
  1215. if ( ( $i % 2 ) == 1 ) {
  1216. # If there are ever four apostrophes, assume the first is supposed to
  1217. # be text, and the remaining three constitute mark-up for bold text.
  1218. if ( strlen( $arr[$i] ) == 4 ) {
  1219. $arr[$i-1] .= "'";
  1220. $arr[$i] = "'''";
  1221. } elseif ( strlen( $arr[$i] ) > 5 ) {
  1222. # If there are more than 5 apostrophes in a row, assume they're all
  1223. # text except for the last 5.
  1224. $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
  1225. $arr[$i] = "'''''";
  1226. }
  1227. # Count the number of occurrences of bold and italics mark-ups.
  1228. # We are not counting sequences of five apostrophes.
  1229. if ( strlen( $arr[$i] ) == 2 ) {
  1230. $numitalics++;
  1231. } elseif ( strlen( $arr[$i] ) == 3 ) {
  1232. $numbold++;
  1233. } elseif ( strlen( $arr[$i] ) == 5 ) {
  1234. $numitalics++;
  1235. $numbold++;
  1236. }
  1237. }
  1238. }
  1239. # If there is an odd number of both bold and italics, it is likely
  1240. # that one of the bold ones was meant to be an apostrophe followed
  1241. # by italics. Which one we cannot know for certain, but it is more
  1242. # likely to be one that has a single-letter word before it.
  1243. if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
  1244. $i = 0;
  1245. $firstsingleletterword = -1;
  1246. $firstmultiletterword = -1;
  1247. $firstspace = -1;
  1248. foreach ( $arr as $r ) {
  1249. if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) ) {
  1250. $x1 = substr( $arr[$i-1], -1 );
  1251. $x2 = substr( $arr[$i-1], -2, 1 );
  1252. if ( $x1 === ' ' ) {
  1253. if ( $firstspace == -1 ) {
  1254. $firstspace = $i;
  1255. }
  1256. } elseif ( $x2 === ' ') {
  1257. if ( $firstsingleletterword == -1 ) {
  1258. $firstsingleletterword = $i;
  1259. }
  1260. } else {
  1261. if ( $firstmultiletterword == -1 ) {
  1262. $firstmultiletterword = $i;
  1263. }
  1264. }
  1265. }
  1266. $i++;
  1267. }
  1268. # If there is a single-letter word, use it!
  1269. if ( $firstsingleletterword > -1 ) {
  1270. $arr[$firstsingleletterword] = "''";
  1271. $arr[$firstsingleletterword-1] .= "'";
  1272. } elseif ( $firstmultiletterword > -1 ) {
  1273. # If not, but there's a multi-letter word, use that one.
  1274. $arr[$firstmultiletterword] = "''";
  1275. $arr[$firstmultiletterword-1] .= "'";
  1276. } elseif ( $firstspace > -1 ) {
  1277. # ... otherwise use the first one that has neither.
  1278. # (notice that it is possible for all three to be -1 if, for example,
  1279. # there is only one pentuple-apostrophe in the line)
  1280. $arr[$firstspace] = "''";
  1281. $arr[$firstspace-1] .= "'";
  1282. }
  1283. }
  1284. # Now let's actually convert our apostrophic mush to HTML!
  1285. $output = '';
  1286. $buffer = '';
  1287. $state = '';
  1288. $i = 0;
  1289. foreach ( $arr as $r ) {
  1290. if ( ( $i % 2 ) == 0 ) {
  1291. if ( $state === 'both' ) {
  1292. $buffer .= $r;
  1293. } else {
  1294. $output .= $r;
  1295. }
  1296. } else {
  1297. if ( strlen( $r ) == 2 ) {
  1298. if ( $state === 'i' ) {
  1299. $output .= '</i>'; $state = '';
  1300. } elseif ( $state === 'bi' ) {
  1301. $output .= '</i>'; $state = 'b';
  1302. } elseif ( $state === 'ib' ) {
  1303. $output .= '</b></i><b>'; $state = 'b';
  1304. } elseif ( $state === 'both' ) {
  1305. $output .= '<b><i>'.$buffer.'</i>'; $state = 'b';
  1306. } else { # $state can be 'b' or ''
  1307. $output .= '<i>'; $state .= 'i';
  1308. }
  1309. } elseif ( strlen( $r ) == 3 ) {
  1310. if ( $state === 'b' ) {
  1311. $output .= '</b>'; $state = '';
  1312. } elseif ( $state === 'bi' ) {
  1313. $output .= '</i></b><i>'; $state = 'i';
  1314. } elseif ( $state === 'ib' ) {
  1315. $output .= '</b>'; $state = 'i';
  1316. } elseif ( $state === 'both' ) {
  1317. $output .= '<i><b>'.$buffer.'</b>'; $state = 'i';
  1318. } else { # $state can be 'i' or ''
  1319. $output .= '<b>'; $state .= 'b';
  1320. }
  1321. } elseif ( strlen( $r ) == 5 ) {
  1322. if ( $state === 'b' ) {
  1323. $output .= '</b><i>'; $state = 'i';
  1324. } elseif ( $state === 'i' ) {
  1325. $output .= '</i><b>'; $state = 'b';
  1326. } elseif ( $state === 'bi' ) {
  1327. $output .= '</i></b>'; $state = '';
  1328. } elseif ( $state === 'ib' ) {
  1329. $output .= '</b></i>'; $state = '';
  1330. } elseif ( $state === 'both' ) {
  1331. $output .= '<i><b>'.$buffer.'</b></i>'; $state = '';
  1332. } else { # ($state == '')
  1333. $buffer = ''; $state = 'both';
  1334. }
  1335. }
  1336. }
  1337. $i++;
  1338. }
  1339. # Now close all remaining tags. Notice that the order is important.
  1340. if ( $state === 'b' || $state === 'ib' ) {
  1341. $output .= '</b>';
  1342. }
  1343. if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
  1344. $output .= '</i>';
  1345. }
  1346. if ( $state === 'bi' ) {
  1347. $output .= '</b>';
  1348. }
  1349. # There might be lonely ''''', so make sure we have a buffer
  1350. if ( $state === 'both' && $buffer ) {
  1351. $output .= '<b><i>'.$buffer.'</i></b>';
  1352. }
  1353. return $output;
  1354. }
  1355. }
  1356. /**
  1357. * Replace external links (REL)
  1358. *
  1359. * Note: this is all very hackish and the order of execution matters a lot.
  1360. * Make sure to run maintenance/parserTests.php if you change this code.
  1361. *
  1362. * @private
  1363. *
  1364. * @param $text string
  1365. *
  1366. * @return string
  1367. */
  1368. function replaceExternalLinks( $text ) {
  1369. wfProfileIn( __METHOD__ );
  1370. $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
  1371. if ( $bits === false ) {
  1372. throw new MWException( "PCRE needs to be compiled with --enable-unicode-properties in order for MediaWiki to function" );
  1373. }
  1374. $s = array_shift( $bits );
  1375. $i = 0;
  1376. while ( $i<count( $bits ) ) {
  1377. $url = $bits[$i++];
  1378. $protocol = $bits[$i++];
  1379. $text = $bits[$i++];
  1380. $trail = $bits[$i++];
  1381. # The characters '<' and '>' (which were escaped by
  1382. # removeHTMLtags()) should not be included in
  1383. # URLs, per RFC 2396.
  1384. $m2 = array();
  1385. if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
  1386. $text = substr( $url, $m2[0][1] ) . ' ' . $text;
  1387. $url = substr( $url, 0, $m2[0][1] );
  1388. }
  1389. # If the link text is an image URL, replace it with an <img> tag
  1390. # This happened by accident in the original parser, but some people used it extensively
  1391. $img = $this->maybeMakeExternalImage( $text );
  1392. if ( $img !== false ) {
  1393. $text = $img;
  1394. }
  1395. $dtrail = '';
  1396. # Set linktype for CSS - if URL==text, link is essentially free
  1397. $linktype = ( $text === $url ) ? 'free' : 'text';
  1398. # No link text, e.g. [http://domain.tld/some.link]
  1399. if ( $text == '' ) {
  1400. # Autonumber
  1401. $langObj = $this->getTargetLanguage();
  1402. $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
  1403. $linktype = 'autonumber';
  1404. } else {
  1405. # Have link text, e.g. [http://domain.tld/some.link text]s
  1406. # Check for trail
  1407. list( $dtrail, $trail ) = Linker::splitTrail( $trail );
  1408. }
  1409. $text = $this->getConverterLanguage()->markNoConversion( $text );
  1410. $url = Sanitizer::cleanUrl( $url );
  1411. # Use the encoded URL
  1412. # This means that users can paste URLs directly into the text
  1413. # Funny characters like ö aren't valid in URLs anyway
  1414. # This was changed in August 2004
  1415. $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
  1416. $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
  1417. # Register link in the output object.
  1418. # Replace unnecessary URL escape codes with the referenced character
  1419. # This prevents spammers from hiding links from the filters
  1420. $pasteurized = self::replaceUnusualEscapes( $url );
  1421. $this->mOutput->addExternalLink( $pasteurized );
  1422. }
  1423. wfProfileOut( __METHOD__ );
  1424. return $s;
  1425. }
  1426. /**
  1427. * Get an associative array of additional HTML attributes appropriate for a
  1428. * particular external link. This currently may include rel => nofollow
  1429. * (depending on configuration, namespace, and the URL's domain) and/or a
  1430. * target attribute (depending on configuration).
  1431. *
  1432. * @param $url String|bool optional URL, to extract the domain from for rel =>
  1433. * nofollow if appropriate
  1434. * @return Array associative array of HTML attributes
  1435. */
  1436. function getExternalLinkAttribs( $url = false ) {
  1437. $attribs = array();
  1438. global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
  1439. $ns = $this->mTitle->getNamespace();
  1440. if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) &&
  1441. !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions ) )
  1442. {
  1443. $attribs['rel'] = 'nofollow';
  1444. }
  1445. if ( $this->mOptions->getExternalLinkTarget() ) {
  1446. $attribs['target'] = $this->mOptions->getExternalLinkTarget();
  1447. }
  1448. return $attribs;
  1449. }
  1450. /**
  1451. * Replace unusual URL escape codes with their equivalent characters
  1452. *
  1453. * @param $url String
  1454. * @return String
  1455. *
  1456. * @todo This can merge genuinely required bits in the path or query string,
  1457. * breaking legit URLs. A proper fix would treat the various parts of
  1458. * the URL differently; as a workaround, just use the output for
  1459. * statistical records, not for actual linking/output.
  1460. */
  1461. static function replaceUnusualEscapes( $url ) {
  1462. return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
  1463. array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
  1464. }
  1465. /**
  1466. * Callback function used in replaceUnusualEscapes().
  1467. * Replaces unusual URL escape codes with their equivalent character
  1468. *
  1469. * @param $matches array
  1470. *
  1471. * @return string
  1472. */
  1473. private static function replaceUnusualEscapesCallback( $matches ) {
  1474. $char = urldecode( $matches[0] );
  1475. $ord = ord( $char );
  1476. # Is it an unsafe or HTTP reserved character according to RFC 1738?
  1477. if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
  1478. # No, shouldn't be escaped
  1479. return $char;
  1480. } else {
  1481. # Yes, leave it escaped
  1482. return $matches[0];
  1483. }
  1484. }
  1485. /**
  1486. * make an image if it's allowed, either through the global
  1487. * option, through the exception, or through the on-wiki whitelist
  1488. * @private
  1489. *
  1490. * $param $url string
  1491. *
  1492. * @return string
  1493. */
  1494. function maybeMakeExternalImage( $url ) {
  1495. $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
  1496. $imagesexception = !empty( $imagesfrom );
  1497. $text = false;
  1498. # $imagesfrom could be either a single string or an array of strings, parse out the latter
  1499. if ( $imagesexception && is_array( $imagesfrom ) ) {
  1500. $imagematch = false;
  1501. foreach ( $imagesfrom as $match ) {
  1502. if ( strpos( $url, $match ) === 0 ) {
  1503. $imagematch = true;
  1504. break;
  1505. }
  1506. }
  1507. } elseif ( $imagesexception ) {
  1508. $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
  1509. } else {
  1510. $imagematch = false;
  1511. }
  1512. if ( $this->mOptions->getAllowExternalImages()
  1513. || ( $imagesexception && $imagematch ) ) {
  1514. if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
  1515. # Image found
  1516. $text = Linker::makeExternalImage( $url );
  1517. }
  1518. }
  1519. if ( !$text && $this->mOptions->getEnableImageWhitelist()
  1520. && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
  1521. $whitelist = explode( "\n", wfMessage( 'external_image_whitelist' )->inContentLanguage()->text() );
  1522. foreach ( $whitelist as $entry ) {
  1523. # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
  1524. if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
  1525. continue;
  1526. }
  1527. if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
  1528. # Image matches a whitelist entry
  1529. $text = Linker::makeExternalImage( $url );
  1530. break;
  1531. }
  1532. }
  1533. }
  1534. return $text;
  1535. }
  1536. /**
  1537. * Process [[ ]] wikilinks
  1538. *
  1539. * @param $s string
  1540. *
  1541. * @return String: processed text
  1542. *
  1543. * @private
  1544. */
  1545. function replaceInternalLinks( $s ) {
  1546. $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
  1547. return $s;
  1548. }
  1549. /**
  1550. * Process [[ ]] wikilinks (RIL)
  1551. * @return LinkHolderArray
  1552. *
  1553. * @private
  1554. */
  1555. function replaceInternalLinks2( &$s ) {
  1556. wfProfileIn( __METHOD__ );
  1557. wfProfileIn( __METHOD__.'-setup' );
  1558. static $tc = FALSE, $e1, $e1_img;
  1559. # the % is needed to support urlencoded titles as well
  1560. if ( !$tc ) {
  1561. $tc = Title::legalChars() . '#%';
  1562. # Match a link having the form [[namespace:link|alternate]]trail
  1563. $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
  1564. # Match cases where there is no "]]", which might still be images
  1565. $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
  1566. }
  1567. $holders = new LinkHolderArray( $this );
  1568. # split the entire text string on occurrences of [[
  1569. $a = StringUtils::explode( '[[', ' ' . $s );
  1570. # get the first element (all text up to first [[), and remove the space we added
  1571. $s = $a->current();
  1572. $a->next();
  1573. $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
  1574. $s = substr( $s, 1 );
  1575. $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
  1576. $e2 = null;
  1577. if ( $useLinkPrefixExtension ) {
  1578. # Match the end of a line for a word that's not followed by whitespace,
  1579. # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
  1580. $e2 = wfMessage( 'linkprefix' )->inContentLanguage()->text();
  1581. }
  1582. if ( is_null( $this->mTitle ) ) {
  1583. wfProfileOut( __METHOD__.'-setup' );
  1584. wfProfileOut( __METHOD__ );
  1585. throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
  1586. }
  1587. $nottalk = !$this->mTitle->isTalkPage();
  1588. if ( $useLinkPrefixExtension ) {
  1589. $m = array();
  1590. if ( preg_match( $e2, $s, $m ) ) {
  1591. $first_prefix = $m[2];
  1592. } else {
  1593. $first_prefix = false;
  1594. }
  1595. } else {
  1596. $prefix = '';
  1597. }
  1598. if ( $this->getConverterLanguage()->hasVariants() ) {
  1599. $selflink = $this->getConverterLanguage()->autoConvertToAllVariants(
  1600. $this->mTitle->getPrefixedText() );
  1601. } else {
  1602. $selflink = array( $this->mTitle->getPrefixedText() );
  1603. }
  1604. $useSubpages = $this->areSubpagesAllowed();
  1605. wfProfileOut( __METHOD__.'-setup' );
  1606. # Loop for each link
  1607. for ( ; $line !== false && $line !== null ; $a->next(), $line = $a->current() ) {
  1608. # Check for excessive memory usage
  1609. if ( $holders->isBig() ) {
  1610. # Too big
  1611. # Do the existence check, replace the link holders and clear the array
  1612. $holders->replace( $s );
  1613. $holders->clear();
  1614. }
  1615. if ( $useLinkPrefixExtension ) {
  1616. wfProfileIn( __METHOD__.'-prefixhandling' );
  1617. if ( preg_match( $e2, $s, $m ) ) {
  1618. $prefix = $m[2];
  1619. $s = $m[1];
  1620. } else {
  1621. $prefix='';
  1622. }
  1623. # first link
  1624. if ( $first_prefix ) {
  1625. $prefix = $first_prefix;
  1626. $first_prefix = false;
  1627. }
  1628. wfProfileOut( __METHOD__.'-prefixhandling' );
  1629. }
  1630. $might_be_img = false;
  1631. wfProfileIn( __METHOD__."-e1" );
  1632. if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
  1633. $text = $m[2];
  1634. # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
  1635. # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
  1636. # the real problem is with the $e1 regex
  1637. # See bug 1300.
  1638. #
  1639. # Still some problems for cases where the ] is meant to be outside punctuation,
  1640. # and no image is in sight. See bug 2095.
  1641. #
  1642. if ( $text !== '' &&
  1643. substr( $m[3], 0, 1 ) === ']' &&
  1644. strpos( $text, '[' ) !== false
  1645. )
  1646. {
  1647. $text .= ']'; # so that replaceExternalLinks($text) works later
  1648. $m[3] = substr( $m[3], 1 );
  1649. }
  1650. # fix up urlencoded title texts
  1651. if ( strpos( $m[1], '%' ) !== false ) {
  1652. # Should anchors '#' also be rejected?
  1653. $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), rawurldecode( $m[1] ) );
  1654. }
  1655. $trail = $m[3];
  1656. } elseif ( preg_match( $e1_img, $line, $m ) ) { # Invalid, but might be an image with a link in its caption
  1657. $might_be_img = true;
  1658. $text = $m[2];
  1659. if ( strpos( $m[1], '%' ) !== false ) {
  1660. $m[1] = rawurldecode( $m[1] );
  1661. }
  1662. $trail = "";
  1663. } else { # Invalid form; output directly
  1664. $s .= $prefix . '[[' . $line ;
  1665. wfProfileOut( __METHOD__."-e1" );
  1666. continue;
  1667. }
  1668. wfProfileOut( __METHOD__."-e1" );
  1669. wfProfileIn( __METHOD__."-misc" );
  1670. # Don't allow internal links to pages containing
  1671. # PROTO: where PROTO is a valid URL protocol; these
  1672. # should be external links.
  1673. if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $m[1] ) ) {
  1674. $s .= $prefix . '[[' . $line ;
  1675. wfProfileOut( __METHOD__."-misc" );
  1676. continue;
  1677. }
  1678. # Make subpage if necessary
  1679. if ( $useSubpages ) {
  1680. $link = $this->maybeDoSubpageLink( $m[1], $text );
  1681. } else {
  1682. $link = $m[1];
  1683. }
  1684. $noforce = ( substr( $m[1], 0, 1 ) !== ':' );
  1685. if ( !$noforce ) {
  1686. # Strip off leading ':'
  1687. $link = substr( $link, 1 );
  1688. }
  1689. wfProfileOut( __METHOD__."-misc" );
  1690. wfProfileIn( __METHOD__."-title" );
  1691. $nt = Title::newFromText( $this->mStripState->unstripNoWiki( $link ) );
  1692. if ( $nt === null ) {
  1693. $s .= $prefix . '[[' . $line;
  1694. wfProfileOut( __METHOD__."-title" );
  1695. continue;
  1696. }
  1697. $ns = $nt->getNamespace();
  1698. $iw = $nt->getInterWiki();
  1699. wfProfileOut( __METHOD__."-title" );
  1700. if ( $might_be_img ) { # if this is actually an invalid link
  1701. wfProfileIn( __METHOD__."-might_be_img" );
  1702. if ( $ns == NS_FILE && $noforce ) { # but might be an image
  1703. $found = false;
  1704. while ( true ) {
  1705. # look at the next 'line' to see if we can close it there
  1706. $a->next();
  1707. $next_line = $a->current();
  1708. if ( $next_line === false || $next_line === null ) {
  1709. break;
  1710. }
  1711. $m = explode( ']]', $next_line, 3 );
  1712. if ( count( $m ) == 3 ) {
  1713. # the first ]] closes the inner link, the second the image
  1714. $found = true;
  1715. $text .= "[[{$m[0]}]]{$m[1]}";
  1716. $trail = $m[2];
  1717. break;
  1718. } elseif ( count( $m ) == 2 ) {
  1719. # if there's exactly one ]] that's fine, we'll keep looking
  1720. $text .= "[[{$m[0]}]]{$m[1]}";
  1721. } else {
  1722. # if $next_line is invalid too, we need look no further
  1723. $text .= '[[' . $next_line;
  1724. break;
  1725. }
  1726. }
  1727. if ( !$found ) {
  1728. # we couldn't find the end of this imageLink, so output it raw
  1729. # but don't ignore what might be perfectly normal links in the text we've examined
  1730. $holders->merge( $this->replaceInternalLinks2( $text ) );
  1731. $s .= "{$prefix}[[$link|$text";
  1732. # note: no $trail, because without an end, there *is* no trail
  1733. wfProfileOut( __METHOD__."-might_be_img" );
  1734. continue;
  1735. }
  1736. } else { # it's not an image, so output it raw
  1737. $s .= "{$prefix}[[$link|$text";
  1738. # note: no $trail, because without an end, there *is* no trail
  1739. wfProfileOut( __METHOD__."-might_be_img" );
  1740. continue;
  1741. }
  1742. wfProfileOut( __METHOD__."-might_be_img" );
  1743. }
  1744. $wasblank = ( $text == '' );
  1745. if ( $wasblank ) {
  1746. $text = $link;
  1747. } else {
  1748. # Bug 4598 madness. Handle the quotes only if they come from the alternate part
  1749. # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
  1750. # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
  1751. # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
  1752. $text = $this->doQuotes( $text );
  1753. }
  1754. # Link not escaped by : , create the various objects
  1755. if ( $noforce ) {
  1756. # Interwikis
  1757. wfProfileIn( __METHOD__."-interwiki" );
  1758. if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
  1759. $this->mOutput->addLanguageLink( $nt->getFullText() );
  1760. $s = rtrim( $s . $prefix );
  1761. $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
  1762. wfProfileOut( __METHOD__."-interwiki" );
  1763. continue;
  1764. }
  1765. wfProfileOut( __METHOD__."-interwiki" );
  1766. if ( $ns == NS_FILE ) {
  1767. wfProfileIn( __METHOD__."-image" );
  1768. if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
  1769. if ( $wasblank ) {
  1770. # if no parameters were passed, $text
  1771. # becomes something like "File:Foo.png",
  1772. # which we don't want to pass on to the
  1773. # image generator
  1774. $text = '';
  1775. } else {
  1776. # recursively parse links inside the image caption
  1777. # actually, this will parse them in any other parameters, too,
  1778. # but it might be hard to fix that, and it doesn't matter ATM
  1779. $text = $this->replaceExternalLinks( $text );
  1780. $holders->merge( $this->replaceInternalLinks2( $text ) );
  1781. }
  1782. # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
  1783. $s .= $prefix . $this->armorLinks(
  1784. $this->makeImage( $nt, $text, $holders ) ) . $trail;
  1785. } else {
  1786. $s .= $prefix . $trail;
  1787. }
  1788. wfProfileOut( __METHOD__."-image" );
  1789. continue;
  1790. }
  1791. if ( $ns == NS_CATEGORY ) {
  1792. wfProfileIn( __METHOD__."-category" );
  1793. $s = rtrim( $s . "\n" ); # bug 87
  1794. if ( $wasblank ) {
  1795. $sortkey = $this->getDefaultSort();
  1796. } else {
  1797. $sortkey = $text;
  1798. }
  1799. $sortkey = Sanitizer::decodeCharReferences( $sortkey );
  1800. $sortkey = str_replace( "\n", '', $sortkey );
  1801. $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
  1802. $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
  1803. /**
  1804. * Strip the whitespace Category links produce, see bug 87
  1805. * @todo We might want to use trim($tmp, "\n") here.
  1806. */
  1807. $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
  1808. wfProfileOut( __METHOD__."-category" );
  1809. continue;
  1810. }
  1811. }
  1812. # Self-link checking
  1813. if ( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
  1814. if ( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
  1815. $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
  1816. continue;
  1817. }
  1818. }
  1819. # NS_MEDIA is a pseudo-namespace for linking directly to a file
  1820. # @todo FIXME: Should do batch file existence checks, see comment below
  1821. if ( $ns == NS_MEDIA ) {
  1822. wfProfileIn( __METHOD__."-media" );
  1823. # Give extensions a chance to select the file revision for us
  1824. $options = array();
  1825. $descQuery = false;
  1826. wfRunHooks( 'BeforeParserFetchFileAndTitle',
  1827. array( $this, $nt, &$options, &$descQuery ) );
  1828. # Fetch and register the file (file title may be different via hooks)
  1829. list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
  1830. # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
  1831. $s .= $prefix . $this->armorLinks(
  1832. Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
  1833. wfProfileOut( __METHOD__."-media" );
  1834. continue;
  1835. }
  1836. wfProfileIn( __METHOD__."-always_known" );
  1837. # Some titles, such as valid special pages or files in foreign repos, should
  1838. # be shown as bluelinks even though they're not included in the page table
  1839. #
  1840. # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
  1841. # batch file existence checks for NS_FILE and NS_MEDIA
  1842. if ( $iw == '' && $nt->isAlwaysKnown() ) {
  1843. $this->mOutput->addLink( $nt );
  1844. $s .= $this->makeKnownLinkHolder( $nt, $text, array(), $trail, $prefix );
  1845. } else {
  1846. # Links will be added to the output link list after checking
  1847. $s .= $holders->makeHolder( $nt, $text, array(), $trail, $prefix );
  1848. }
  1849. wfProfileOut( __METHOD__."-always_known" );
  1850. }
  1851. wfProfileOut( __METHOD__ );
  1852. return $holders;
  1853. }
  1854. /**
  1855. * Render a forced-blue link inline; protect against double expansion of
  1856. * URLs if we're in a mode that prepends full URL prefixes to internal links.
  1857. * Since this little disaster has to split off the trail text to avoid
  1858. * breaking URLs in the following text without breaking trails on the
  1859. * wiki links, it's been made into a horrible function.
  1860. *
  1861. * @param $nt Title
  1862. * @param $text String
  1863. * @param $query Array or String
  1864. * @param $trail String
  1865. * @param $prefix String
  1866. * @return String: HTML-wikitext mix oh yuck
  1867. */
  1868. function makeKnownLinkHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) {
  1869. list( $inside, $trail ) = Linker::splitTrail( $trail );
  1870. if ( is_string( $query ) ) {
  1871. $query = wfCgiToArray( $query );
  1872. }
  1873. if ( $text == '' ) {
  1874. $text = htmlspecialchars( $nt->getPrefixedText() );
  1875. }
  1876. $link = Linker::linkKnown( $nt, "$prefix$text$inside", array(), $query );
  1877. return $this->armorLinks( $link ) . $trail;
  1878. }
  1879. /**
  1880. * Insert a NOPARSE hacky thing into any inline links in a chunk that's
  1881. * going to go through further parsing steps before inline URL expansion.
  1882. *
  1883. * Not needed quite as much as it used to be since free links are a bit
  1884. * more sensible these days. But bracketed links are still an issue.
  1885. *
  1886. * @param $text String: more-or-less HTML
  1887. * @return String: less-or-more HTML with NOPARSE bits
  1888. */
  1889. function armorLinks( $text ) {
  1890. return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
  1891. "{$this->mUniqPrefix}NOPARSE$1", $text );
  1892. }
  1893. /**
  1894. * Return true if subpage links should be expanded on this page.
  1895. * @return Boolean
  1896. */
  1897. function areSubpagesAllowed() {
  1898. # Some namespaces don't allow subpages
  1899. return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
  1900. }
  1901. /**
  1902. * Handle link to subpage if necessary
  1903. *
  1904. * @param $target String: the source of the link
  1905. * @param &$text String: the link text, modified as necessary
  1906. * @return string the full name of the link
  1907. * @private
  1908. */
  1909. function maybeDoSubpageLink( $target, &$text ) {
  1910. return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
  1911. }
  1912. /**#@+
  1913. * Used by doBlockLevels()
  1914. * @private
  1915. *
  1916. * @return string
  1917. */
  1918. function closeParagraph() {
  1919. $result = '';
  1920. if ( $this->mLastSection != '' ) {
  1921. $result = '</' . $this->mLastSection . ">\n";
  1922. }
  1923. $this->mInPre = false;
  1924. $this->mLastSection = '';
  1925. return $result;
  1926. }
  1927. /**
  1928. * getCommon() returns the length of the longest common substring
  1929. * of both arguments, starting at the beginning of both.
  1930. * @private
  1931. *
  1932. * @param $st1 string
  1933. * @param $st2 string
  1934. *
  1935. * @return int
  1936. */
  1937. function getCommon( $st1, $st2 ) {
  1938. $fl = strlen( $st1 );
  1939. $shorter = strlen( $st2 );
  1940. if ( $fl < $shorter ) {
  1941. $shorter = $fl;
  1942. }
  1943. for ( $i = 0; $i < $shorter; ++$i ) {
  1944. if ( $st1[$i] != $st2[$i] ) {
  1945. break;
  1946. }
  1947. }
  1948. return $i;
  1949. }
  1950. /**
  1951. * These next three functions open, continue, and close the list
  1952. * element appropriate to the prefix character passed into them.
  1953. * @private
  1954. *
  1955. * @param $char string
  1956. *
  1957. * @return string
  1958. */
  1959. function openList( $char ) {
  1960. $result = $this->closeParagraph();
  1961. if ( '*' === $char ) {
  1962. $result .= '<ul><li>';
  1963. } elseif ( '#' === $char ) {
  1964. $result .= '<ol><li>';
  1965. } elseif ( ':' === $char ) {
  1966. $result .= '<dl><dd>';
  1967. } elseif ( ';' === $char ) {
  1968. $result .= '<dl><dt>';
  1969. $this->mDTopen = true;
  1970. } else {
  1971. $result = '<!-- ERR 1 -->';
  1972. }
  1973. return $result;
  1974. }
  1975. /**
  1976. * TODO: document
  1977. * @param $char String
  1978. * @private
  1979. *
  1980. * @return string
  1981. */
  1982. function nextItem( $char ) {
  1983. if ( '*' === $char || '#' === $char ) {
  1984. return '</li><li>';
  1985. } elseif ( ':' === $char || ';' === $char ) {
  1986. $close = '</dd>';
  1987. if ( $this->mDTopen ) {
  1988. $close = '</dt>';
  1989. }
  1990. if ( ';' === $char ) {
  1991. $this->mDTopen = true;
  1992. return $close . '<dt>';
  1993. } else {
  1994. $this->mDTopen = false;
  1995. return $close . '<dd>';
  1996. }
  1997. }
  1998. return '<!-- ERR 2 -->';
  1999. }
  2000. /**
  2001. * TODO: document
  2002. * @param $char String
  2003. * @private
  2004. *
  2005. * @return string
  2006. */
  2007. function closeList( $char ) {
  2008. if ( '*' === $char ) {
  2009. $text = '</li></ul>';
  2010. } elseif ( '#' === $char ) {
  2011. $text = '</li></ol>';
  2012. } elseif ( ':' === $char ) {
  2013. if ( $this->mDTopen ) {
  2014. $this->mDTopen = false;
  2015. $text = '</dt></dl>';
  2016. } else {
  2017. $text = '</dd></dl>';
  2018. }
  2019. } else {
  2020. return '<!-- ERR 3 -->';
  2021. }
  2022. return $text."\n";
  2023. }
  2024. /**#@-*/
  2025. /**
  2026. * Make lists from lines starting with ':', '*', '#', etc. (DBL)
  2027. *
  2028. * @param $text String
  2029. * @param $linestart Boolean: whether or not this is at the start of a line.
  2030. * @private
  2031. * @return string the lists rendered as HTML
  2032. */
  2033. function doBlockLevels( $text, $linestart ) {
  2034. wfProfileIn( __METHOD__ );
  2035. # Parsing through the text line by line. The main thing
  2036. # happening here is handling of block-level elements p, pre,
  2037. # and making lists from lines starting with * # : etc.
  2038. #
  2039. $textLines = StringUtils::explode( "\n", $text );
  2040. $lastPrefix = $output = '';
  2041. $this->mDTopen = $inBlockElem = false;
  2042. $prefixLength = 0;
  2043. $paragraphStack = false;
  2044. foreach ( $textLines as $oLine ) {
  2045. # Fix up $linestart
  2046. if ( !$linestart ) {
  2047. $output .= $oLine;
  2048. $linestart = true;
  2049. continue;
  2050. }
  2051. # * = ul
  2052. # # = ol
  2053. # ; = dt
  2054. # : = dd
  2055. $lastPrefixLength = strlen( $lastPrefix );
  2056. $preCloseMatch = preg_match( '/<\\/pre/i', $oLine );
  2057. $preOpenMatch = preg_match( '/<pre/i', $oLine );
  2058. # If not in a <pre> element, scan for and figure out what prefixes are there.
  2059. if ( !$this->mInPre ) {
  2060. # Multiple prefixes may abut each other for nested lists.
  2061. $prefixLength = strspn( $oLine, '*#:;' );
  2062. $prefix = substr( $oLine, 0, $prefixLength );
  2063. # eh?
  2064. # ; and : are both from definition-lists, so they're equivalent
  2065. # for the purposes of determining whether or not we need to open/close
  2066. # elements.
  2067. $prefix2 = str_replace( ';', ':', $prefix );
  2068. $t = substr( $oLine, $prefixLength );
  2069. $this->mInPre = (bool)$preOpenMatch;
  2070. } else {
  2071. # Don't interpret any other prefixes in preformatted text
  2072. $prefixLength = 0;
  2073. $prefix = $prefix2 = '';
  2074. $t = $oLine;
  2075. }
  2076. # List generation
  2077. if ( $prefixLength && $lastPrefix === $prefix2 ) {
  2078. # Same as the last item, so no need to deal with nesting or opening stuff
  2079. $output .= $this->nextItem( substr( $prefix, -1 ) );
  2080. $paragraphStack = false;
  2081. if ( substr( $prefix, -1 ) === ';') {
  2082. # The one nasty exception: definition lists work like this:
  2083. # ; title : definition text
  2084. # So we check for : in the remainder text to split up the
  2085. # title and definition, without b0rking links.
  2086. $term = $t2 = '';
  2087. if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
  2088. $t = $t2;
  2089. $output .= $term . $this->nextItem( ':' );
  2090. }
  2091. }
  2092. } elseif ( $prefixLength || $lastPrefixLength ) {
  2093. # We need to open or close prefixes, or both.
  2094. # Either open or close a level...
  2095. $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
  2096. $paragraphStack = false;
  2097. # Close all the prefixes which aren't shared.
  2098. while ( $commonPrefixLength < $lastPrefixLength ) {
  2099. $output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
  2100. --$lastPrefixLength;
  2101. }
  2102. # Continue the current prefix if appropriate.
  2103. if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
  2104. $output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
  2105. }
  2106. # Open prefixes where appropriate.
  2107. while ( $prefixLength > $commonPrefixLength ) {
  2108. $char = substr( $prefix, $commonPrefixLength, 1 );
  2109. $output .= $this->openList( $char );
  2110. if ( ';' === $char ) {
  2111. # @todo FIXME: This is dupe of code above
  2112. if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
  2113. $t = $t2;
  2114. $output .= $term . $this->nextItem( ':' );
  2115. }
  2116. }
  2117. ++$commonPrefixLength;
  2118. }
  2119. $lastPrefix = $prefix2;
  2120. }
  2121. # If we have no prefixes, go to paragraph mode.
  2122. if ( 0 == $prefixLength ) {
  2123. wfProfileIn( __METHOD__."-paragraph" );
  2124. # No prefix (not in list)--go to paragraph mode
  2125. # XXX: use a stack for nestable elements like span, table and div
  2126. $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
  2127. $closematch = preg_match(
  2128. '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
  2129. '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
  2130. if ( $openmatch or $closematch ) {
  2131. $paragraphStack = false;
  2132. # TODO bug 5718: paragraph closed
  2133. $output .= $this->closeParagraph();
  2134. if ( $preOpenMatch and !$preCloseMatch ) {
  2135. $this->mInPre = true;
  2136. }
  2137. $inBlockElem = !$closematch;
  2138. } elseif ( !$inBlockElem && !$this->mInPre ) {
  2139. if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' || trim( $t ) != '' ) ) {
  2140. # pre
  2141. if ( $this->mLastSection !== 'pre' ) {
  2142. $paragraphStack = false;
  2143. $output .= $this->closeParagraph().'<pre>';
  2144. $this->mLastSection = 'pre';
  2145. }
  2146. $t = substr( $t, 1 );
  2147. } else {
  2148. # paragraph
  2149. if ( trim( $t ) === '' ) {
  2150. if ( $paragraphStack ) {
  2151. $output .= $paragraphStack.'<br />';
  2152. $paragraphStack = false;
  2153. $this->mLastSection = 'p';
  2154. } else {
  2155. if ( $this->mLastSection !== 'p' ) {
  2156. $output .= $this->closeParagraph();
  2157. $this->mLastSection = '';
  2158. $paragraphStack = '<p>';
  2159. } else {
  2160. $paragraphStack = '</p><p>';
  2161. }
  2162. }
  2163. } else {
  2164. if ( $paragraphStack ) {
  2165. $output .= $paragraphStack;
  2166. $paragraphStack = false;
  2167. $this->mLastSection = 'p';
  2168. } elseif ( $this->mLastSection !== 'p' ) {
  2169. $output .= $this->closeParagraph().'<p>';
  2170. $this->mLastSection = 'p';
  2171. }
  2172. }
  2173. }
  2174. }
  2175. wfProfileOut( __METHOD__."-paragraph" );
  2176. }
  2177. # somewhere above we forget to get out of pre block (bug 785)
  2178. if ( $preCloseMatch && $this->mInPre ) {
  2179. $this->mInPre = false;
  2180. }
  2181. if ( $paragraphStack === false ) {
  2182. $output .= $t."\n";
  2183. }
  2184. }
  2185. while ( $prefixLength ) {
  2186. $output .= $this->closeList( $prefix2[$prefixLength-1] );
  2187. --$prefixLength;
  2188. }
  2189. if ( $this->mLastSection != '' ) {
  2190. $output .= '</' . $this->mLastSection . '>';
  2191. $this->mLastSection = '';
  2192. }
  2193. wfProfileOut( __METHOD__ );
  2194. return $output;
  2195. }
  2196. /**
  2197. * Split up a string on ':', ignoring any occurrences inside tags
  2198. * to prevent illegal overlapping.
  2199. *
  2200. * @param $str String the string to split
  2201. * @param &$before String set to everything before the ':'
  2202. * @param &$after String set to everything after the ':'
  2203. * @return String the position of the ':', or false if none found
  2204. */
  2205. function findColonNoLinks( $str, &$before, &$after ) {
  2206. wfProfileIn( __METHOD__ );
  2207. $pos = strpos( $str, ':' );
  2208. if ( $pos === false ) {
  2209. # Nothing to find!
  2210. wfProfileOut( __METHOD__ );
  2211. return false;
  2212. }
  2213. $lt = strpos( $str, '<' );
  2214. if ( $lt === false || $lt > $pos ) {
  2215. # Easy; no tag nesting to worry about
  2216. $before = substr( $str, 0, $pos );
  2217. $after = substr( $str, $pos+1 );
  2218. wfProfileOut( __METHOD__ );
  2219. return $pos;
  2220. }
  2221. # Ugly state machine to walk through avoiding tags.
  2222. $state = self::COLON_STATE_TEXT;
  2223. $stack = 0;
  2224. $len = strlen( $str );
  2225. for( $i = 0; $i < $len; $i++ ) {
  2226. $c = $str[$i];
  2227. switch( $state ) {
  2228. # (Using the number is a performance hack for common cases)
  2229. case 0: # self::COLON_STATE_TEXT:
  2230. switch( $c ) {
  2231. case "<":
  2232. # Could be either a <start> tag or an </end> tag
  2233. $state = self::COLON_STATE_TAGSTART;
  2234. break;
  2235. case ":":
  2236. if ( $stack == 0 ) {
  2237. # We found it!
  2238. $before = substr( $str, 0, $i );
  2239. $after = substr( $str, $i + 1 );
  2240. wfProfileOut( __METHOD__ );
  2241. return $i;
  2242. }
  2243. # Embedded in a tag; don't break it.
  2244. break;
  2245. default:
  2246. # Skip ahead looking for something interesting
  2247. $colon = strpos( $str, ':', $i );
  2248. if ( $colon === false ) {
  2249. # Nothing else interesting
  2250. wfProfileOut( __METHOD__ );
  2251. return false;
  2252. }
  2253. $lt = strpos( $str, '<', $i );
  2254. if ( $stack === 0 ) {
  2255. if ( $lt === false || $colon < $lt ) {
  2256. # We found it!
  2257. $before = substr( $str, 0, $colon );
  2258. $after = substr( $str, $colon + 1 );
  2259. wfProfileOut( __METHOD__ );
  2260. return $i;
  2261. }
  2262. }
  2263. if ( $lt === false ) {
  2264. # Nothing else interesting to find; abort!
  2265. # We're nested, but there's no close tags left. Abort!
  2266. break 2;
  2267. }
  2268. # Skip ahead to next tag start
  2269. $i = $lt;
  2270. $state = self::COLON_STATE_TAGSTART;
  2271. }
  2272. break;
  2273. case 1: # self::COLON_STATE_TAG:
  2274. # In a <tag>
  2275. switch( $c ) {
  2276. case ">":
  2277. $stack++;
  2278. $state = self::COLON_STATE_TEXT;
  2279. break;
  2280. case "/":
  2281. # Slash may be followed by >?
  2282. $state = self::COLON_STATE_TAGSLASH;
  2283. break;
  2284. default:
  2285. # ignore
  2286. }
  2287. break;
  2288. case 2: # self::COLON_STATE_TAGSTART:
  2289. switch( $c ) {
  2290. case "/":
  2291. $state = self::COLON_STATE_CLOSETAG;
  2292. break;
  2293. case "!":
  2294. $state = self::COLON_STATE_COMMENT;
  2295. break;
  2296. case ">":
  2297. # Illegal early close? This shouldn't happen D:
  2298. $state = self::COLON_STATE_TEXT;
  2299. break;
  2300. default:
  2301. $state = self::COLON_STATE_TAG;
  2302. }
  2303. break;
  2304. case 3: # self::COLON_STATE_CLOSETAG:
  2305. # In a </tag>
  2306. if ( $c === ">" ) {
  2307. $stack--;
  2308. if ( $stack < 0 ) {
  2309. wfDebug( __METHOD__.": Invalid input; too many close tags\n" );
  2310. wfProfileOut( __METHOD__ );
  2311. return false;
  2312. }
  2313. $state = self::COLON_STATE_TEXT;
  2314. }
  2315. break;
  2316. case self::COLON_STATE_TAGSLASH:
  2317. if ( $c === ">" ) {
  2318. # Yes, a self-closed tag <blah/>
  2319. $state = self::COLON_STATE_TEXT;
  2320. } else {
  2321. # Probably we're jumping the gun, and this is an attribute
  2322. $state = self::COLON_STATE_TAG;
  2323. }
  2324. break;
  2325. case 5: # self::COLON_STATE_COMMENT:
  2326. if ( $c === "-" ) {
  2327. $state = self::COLON_STATE_COMMENTDASH;
  2328. }
  2329. break;
  2330. case self::COLON_STATE_COMMENTDASH:
  2331. if ( $c === "-" ) {
  2332. $state = self::COLON_STATE_COMMENTDASHDASH;
  2333. } else {
  2334. $state = self::COLON_STATE_COMMENT;
  2335. }
  2336. break;
  2337. case self::COLON_STATE_COMMENTDASHDASH:
  2338. if ( $c === ">" ) {
  2339. $state = self::COLON_STATE_TEXT;
  2340. } else {
  2341. $state = self::COLON_STATE_COMMENT;
  2342. }
  2343. break;
  2344. default:
  2345. throw new MWException( "State machine error in " . __METHOD__ );
  2346. }
  2347. }
  2348. if ( $stack > 0 ) {
  2349. wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" );
  2350. wfProfileOut( __METHOD__ );
  2351. return false;
  2352. }
  2353. wfProfileOut( __METHOD__ );
  2354. return false;
  2355. }
  2356. /**
  2357. * Return value of a magic variable (like PAGENAME)
  2358. *
  2359. * @private
  2360. *
  2361. * @param $index integer
  2362. * @param $frame PPFrame
  2363. *
  2364. * @return string
  2365. */
  2366. function getVariableValue( $index, $frame = false ) {
  2367. global $wgContLang, $wgSitename, $wgServer;
  2368. global $wgArticlePath, $wgScriptPath, $wgStylePath;
  2369. if ( is_null( $this->mTitle ) ) {
  2370. // If no title set, bad things are going to happen
  2371. // later. Title should always be set since this
  2372. // should only be called in the middle of a parse
  2373. // operation (but the unit-tests do funky stuff)
  2374. throw new MWException( __METHOD__ . ' Should only be '
  2375. . ' called while parsing (no title set)' );
  2376. }
  2377. /**
  2378. * Some of these require message or data lookups and can be
  2379. * expensive to check many times.
  2380. */
  2381. if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
  2382. if ( isset( $this->mVarCache[$index] ) ) {
  2383. return $this->mVarCache[$index];
  2384. }
  2385. }
  2386. $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
  2387. wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
  2388. # Use the time zone
  2389. global $wgLocaltimezone;
  2390. if ( isset( $wgLocaltimezone ) ) {
  2391. $oldtz = date_default_timezone_get();
  2392. date_default_timezone_set( $wgLocaltimezone );
  2393. }
  2394. $localTimestamp = date( 'YmdHis', $ts );
  2395. $localMonth = date( 'm', $ts );
  2396. $localMonth1 = date( 'n', $ts );
  2397. $localMonthName = date( 'n', $ts );
  2398. $localDay = date( 'j', $ts );
  2399. $localDay2 = date( 'd', $ts );
  2400. $localDayOfWeek = date( 'w', $ts );
  2401. $localWeek = date( 'W', $ts );
  2402. $localYear = date( 'Y', $ts );
  2403. $localHour = date( 'H', $ts );
  2404. if ( isset( $wgLocaltimezone ) ) {
  2405. date_default_timezone_set( $oldtz );
  2406. }
  2407. $pageLang = $this->getFunctionLang();
  2408. switch ( $index ) {
  2409. case 'currentmonth':
  2410. $value = $pageLang->formatNum( gmdate( 'm', $ts ) );
  2411. break;
  2412. case 'currentmonth1':
  2413. $value = $pageLang->formatNum( gmdate( 'n', $ts ) );
  2414. break;
  2415. case 'currentmonthname':
  2416. $value = $pageLang->getMonthName( gmdate( 'n', $ts ) );
  2417. break;
  2418. case 'currentmonthnamegen':
  2419. $value = $pageLang->getMonthNameGen( gmdate( 'n', $ts ) );
  2420. break;
  2421. case 'currentmonthabbrev':
  2422. $value = $pageLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
  2423. break;
  2424. case 'currentday':
  2425. $value = $pageLang->formatNum( gmdate( 'j', $ts ) );
  2426. break;
  2427. case 'currentday2':
  2428. $value = $pageLang->formatNum( gmdate( 'd', $ts ) );
  2429. break;
  2430. case 'localmonth':
  2431. $value = $pageLang->formatNum( $localMonth );
  2432. break;
  2433. case 'localmonth1':
  2434. $value = $pageLang->formatNum( $localMonth1 );
  2435. break;
  2436. case 'localmonthname':
  2437. $value = $pageLang->getMonthName( $localMonthName );
  2438. break;
  2439. case 'localmonthnamegen':
  2440. $value = $pageLang->getMonthNameGen( $localMonthName );
  2441. break;
  2442. case 'localmonthabbrev':
  2443. $value = $pageLang->getMonthAbbreviation( $localMonthName );
  2444. break;
  2445. case 'localday':
  2446. $value = $pageLang->formatNum( $localDay );
  2447. break;
  2448. case 'localday2':
  2449. $value = $pageLang->formatNum( $localDay2 );
  2450. break;
  2451. case 'pagename':
  2452. $value = wfEscapeWikiText( $this->mTitle->getText() );
  2453. break;
  2454. case 'pagenamee':
  2455. $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
  2456. break;
  2457. case 'fullpagename':
  2458. $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
  2459. break;
  2460. case 'fullpagenamee':
  2461. $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
  2462. break;
  2463. case 'subpagename':
  2464. $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
  2465. break;
  2466. case 'subpagenamee':
  2467. $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
  2468. break;
  2469. case 'basepagename':
  2470. $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
  2471. break;
  2472. case 'basepagenamee':
  2473. $value = wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ) );
  2474. break;
  2475. case 'talkpagename':
  2476. if ( $this->mTitle->canTalk() ) {
  2477. $talkPage = $this->mTitle->getTalkPage();
  2478. $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
  2479. } else {
  2480. $value = '';
  2481. }
  2482. break;
  2483. case 'talkpagenamee':
  2484. if ( $this->mTitle->canTalk() ) {
  2485. $talkPage = $this->mTitle->getTalkPage();
  2486. $value = wfEscapeWikiText( $talkPage->getPrefixedUrl() );
  2487. } else {
  2488. $value = '';
  2489. }
  2490. break;
  2491. case 'subjectpagename':
  2492. $subjPage = $this->mTitle->getSubjectPage();
  2493. $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
  2494. break;
  2495. case 'subjectpagenamee':
  2496. $subjPage = $this->mTitle->getSubjectPage();
  2497. $value = wfEscapeWikiText( $subjPage->getPrefixedUrl() );
  2498. break;
  2499. case 'pageid': // requested in bug 23427
  2500. $pageid = $this->getTitle()->getArticleId();
  2501. if( $pageid == 0 ) {
  2502. # 0 means the page doesn't exist in the database,
  2503. # which means the user is previewing a new page.
  2504. # The vary-revision flag must be set, because the magic word
  2505. # will have a different value once the page is saved.
  2506. $this->mOutput->setFlag( 'vary-revision' );
  2507. wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
  2508. }
  2509. $value = $pageid ? $pageid : null;
  2510. break;
  2511. case 'revisionid':
  2512. # Let the edit saving system know we should parse the page
  2513. # *after* a revision ID has been assigned.
  2514. $this->mOutput->setFlag( 'vary-revision' );
  2515. wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
  2516. $value = $this->mRevisionId;
  2517. break;
  2518. case 'revisionday':
  2519. # Let the edit saving system know we should parse the page
  2520. # *after* a revision ID has been assigned. This is for null edits.
  2521. $this->mOutput->setFlag( 'vary-revision' );
  2522. wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
  2523. $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
  2524. break;
  2525. case 'revisionday2':
  2526. # Let the edit saving system know we should parse the page
  2527. # *after* a revision ID has been assigned. This is for null edits.
  2528. $this->mOutput->setFlag( 'vary-revision' );
  2529. wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
  2530. $value = substr( $this->getRevisionTimestamp(), 6, 2 );
  2531. break;
  2532. case 'revisionmonth':
  2533. # Let the edit saving system know we should parse the page
  2534. # *after* a revision ID has been assigned. This is for null edits.
  2535. $this->mOutput->setFlag( 'vary-revision' );
  2536. wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
  2537. $value = substr( $this->getRevisionTimestamp(), 4, 2 );
  2538. break;
  2539. case 'revisionmonth1':
  2540. # Let the edit saving system know we should parse the page
  2541. # *after* a revision ID has been assigned. This is for null edits.
  2542. $this->mOutput->setFlag( 'vary-revision' );
  2543. wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
  2544. $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
  2545. break;
  2546. case 'revisionyear':
  2547. # Let the edit saving system know we should parse the page
  2548. # *after* a revision ID has been assigned. This is for null edits.
  2549. $this->mOutput->setFlag( 'vary-revision' );
  2550. wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
  2551. $value = substr( $this->getRevisionTimestamp(), 0, 4 );
  2552. break;
  2553. case 'revisiontimestamp':
  2554. # Let the edit saving system know we should parse the page
  2555. # *after* a revision ID has been assigned. This is for null edits.
  2556. $this->mOutput->setFlag( 'vary-revision' );
  2557. wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
  2558. $value = $this->getRevisionTimestamp();
  2559. break;
  2560. case 'revisionuser':
  2561. # Let the edit saving system know we should parse the page
  2562. # *after* a revision ID has been assigned. This is for null edits.
  2563. $this->mOutput->setFlag( 'vary-revision' );
  2564. wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
  2565. $value = $this->getRevisionUser();
  2566. break;
  2567. case 'namespace':
  2568. $value = str_replace( '_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
  2569. break;
  2570. case 'namespacee':
  2571. $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
  2572. break;
  2573. case 'namespacenumber':
  2574. $value = $this->mTitle->getNamespace();
  2575. break;
  2576. case 'talkspace':
  2577. $value = $this->mTitle->canTalk() ? str_replace( '_',' ',$this->mTitle->getTalkNsText() ) : '';
  2578. break;
  2579. case 'talkspacee':
  2580. $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
  2581. break;
  2582. case 'subjectspace':
  2583. $value = $this->mTitle->getSubjectNsText();
  2584. break;
  2585. case 'subjectspacee':
  2586. $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
  2587. break;
  2588. case 'currentdayname':
  2589. $value = $pageLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
  2590. break;
  2591. case 'currentyear':
  2592. $value = $pageLang->formatNum( gmdate( 'Y', $ts ), true );
  2593. break;
  2594. case 'currenttime':
  2595. $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
  2596. break;
  2597. case 'currenthour':
  2598. $value = $pageLang->formatNum( gmdate( 'H', $ts ), true );
  2599. break;
  2600. case 'currentweek':
  2601. # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
  2602. # int to remove the padding
  2603. $value = $pageLang->formatNum( (int)gmdate( 'W', $ts ) );
  2604. break;
  2605. case 'currentdow':
  2606. $value = $pageLang->formatNum( gmdate( 'w', $ts ) );
  2607. break;
  2608. case 'localdayname':
  2609. $value = $pageLang->getWeekdayName( $localDayOfWeek + 1 );
  2610. break;
  2611. case 'localyear':
  2612. $value = $pageLang->formatNum( $localYear, true );
  2613. break;
  2614. case 'localtime':
  2615. $value = $pageLang->time( $localTimestamp, false, false );
  2616. break;
  2617. case 'localhour':
  2618. $value = $pageLang->formatNum( $localHour, true );
  2619. break;
  2620. case 'localweek':
  2621. # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
  2622. # int to remove the padding
  2623. $value = $pageLang->formatNum( (int)$localWeek );
  2624. break;
  2625. case 'localdow':
  2626. $value = $pageLang->formatNum( $localDayOfWeek );
  2627. break;
  2628. case 'numberofarticles':
  2629. $value = $pageLang->formatNum( SiteStats::articles() );
  2630. break;
  2631. case 'numberoffiles':
  2632. $value = $pageLang->formatNum( SiteStats::images() );
  2633. break;
  2634. case 'numberofusers':
  2635. $value = $pageLang->formatNum( SiteStats::users() );
  2636. break;
  2637. case 'numberofactiveusers':
  2638. $value = $pageLang->formatNum( SiteStats::activeUsers() );
  2639. break;
  2640. case 'numberofpages':
  2641. $value = $pageLang->formatNum( SiteStats::pages() );
  2642. break;
  2643. case 'numberofadmins':
  2644. $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
  2645. break;
  2646. case 'numberofedits':
  2647. $value = $pageLang->formatNum( SiteStats::edits() );
  2648. break;
  2649. case 'numberofviews':
  2650. global $wgDisableCounters;
  2651. $value = !$wgDisableCounters ? $pageLang->formatNum( SiteStats::views() ) : '';
  2652. break;
  2653. case 'currenttimestamp':
  2654. $value = wfTimestamp( TS_MW, $ts );
  2655. break;
  2656. case 'localtimestamp':
  2657. $value = $localTimestamp;
  2658. break;
  2659. case 'currentversion':
  2660. $value = SpecialVersion::getVersion();
  2661. break;
  2662. case 'articlepath':
  2663. return $wgArticlePath;
  2664. case 'sitename':
  2665. return $wgSitename;
  2666. case 'server':
  2667. return $wgServer;
  2668. case 'servername':
  2669. $serverParts = wfParseUrl( $wgServer );
  2670. return $serverParts && isset( $serverParts['host'] ) ? $serverParts['host'] : $wgServer;
  2671. case 'scriptpath':
  2672. return $wgScriptPath;
  2673. case 'stylepath':
  2674. return $wgStylePath;
  2675. case 'directionmark':
  2676. return $pageLang->getDirMark();
  2677. case 'contentlanguage':
  2678. global $wgLanguageCode;
  2679. return $wgLanguageCode;
  2680. default:
  2681. $ret = null;
  2682. if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret, &$frame ) ) ) {
  2683. return $ret;
  2684. } else {
  2685. return null;
  2686. }
  2687. }
  2688. if ( $index ) {
  2689. $this->mVarCache[$index] = $value;
  2690. }
  2691. return $value;
  2692. }
  2693. /**
  2694. * initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
  2695. *
  2696. * @private
  2697. */
  2698. function initialiseVariables() {
  2699. wfProfileIn( __METHOD__ );
  2700. $variableIDs = MagicWord::getVariableIDs();
  2701. $substIDs = MagicWord::getSubstIDs();
  2702. $this->mVariables = new MagicWordArray( $variableIDs );
  2703. $this->mSubstWords = new MagicWordArray( $substIDs );
  2704. wfProfileOut( __METHOD__ );
  2705. }
  2706. /**
  2707. * Preprocess some wikitext and return the document tree.
  2708. * This is the ghost of replace_variables().
  2709. *
  2710. * @param $text String: The text to parse
  2711. * @param $flags Integer: bitwise combination of:
  2712. * self::PTD_FOR_INCLUSION Handle "<noinclude>" and "<includeonly>" as if the text is being
  2713. * included. Default is to assume a direct page view.
  2714. *
  2715. * The generated DOM tree must depend only on the input text and the flags.
  2716. * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
  2717. *
  2718. * Any flag added to the $flags parameter here, or any other parameter liable to cause a
  2719. * change in the DOM tree for a given text, must be passed through the section identifier
  2720. * in the section edit link and thus back to extractSections().
  2721. *
  2722. * The output of this function is currently only cached in process memory, but a persistent
  2723. * cache may be implemented at a later date which takes further advantage of these strict
  2724. * dependency requirements.
  2725. *
  2726. * @private
  2727. *
  2728. * @return PPNode
  2729. */
  2730. function preprocessToDom( $text, $flags = 0 ) {
  2731. $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
  2732. return $dom;
  2733. }
  2734. /**
  2735. * Return a three-element array: leading whitespace, string contents, trailing whitespace
  2736. *
  2737. * @param $s string
  2738. *
  2739. * @return array
  2740. */
  2741. public static function splitWhitespace( $s ) {
  2742. $ltrimmed = ltrim( $s );
  2743. $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
  2744. $trimmed = rtrim( $ltrimmed );
  2745. $diff = strlen( $ltrimmed ) - strlen( $trimmed );
  2746. if ( $diff > 0 ) {
  2747. $w2 = substr( $ltrimmed, -$diff );
  2748. } else {
  2749. $w2 = '';
  2750. }
  2751. return array( $w1, $trimmed, $w2 );
  2752. }
  2753. /**
  2754. * Replace magic variables, templates, and template arguments
  2755. * with the appropriate text. Templates are substituted recursively,
  2756. * taking care to avoid infinite loops.
  2757. *
  2758. * Note that the substitution depends on value of $mOutputType:
  2759. * self::OT_WIKI: only {{subst:}} templates
  2760. * self::OT_PREPROCESS: templates but not extension tags
  2761. * self::OT_HTML: all templates and extension tags
  2762. *
  2763. * @param $text String the text to transform
  2764. * @param $frame PPFrame Object describing the arguments passed to the template.
  2765. * Arguments may also be provided as an associative array, as was the usual case before MW1.12.
  2766. * Providing arguments this way may be useful for extensions wishing to perform variable replacement explicitly.
  2767. * @param $argsOnly Boolean only do argument (triple-brace) expansion, not double-brace expansion
  2768. * @private
  2769. *
  2770. * @return string
  2771. */
  2772. function replaceVariables( $text, $frame = false, $argsOnly = false ) {
  2773. # Is there any text? Also, Prevent too big inclusions!
  2774. if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
  2775. return $text;
  2776. }
  2777. wfProfileIn( __METHOD__ );
  2778. if ( $frame === false ) {
  2779. $frame = $this->getPreprocessor()->newFrame();
  2780. } elseif ( !( $frame instanceof PPFrame ) ) {
  2781. wfDebug( __METHOD__." called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
  2782. $frame = $this->getPreprocessor()->newCustomFrame( $frame );
  2783. }
  2784. $dom = $this->preprocessToDom( $text );
  2785. $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
  2786. $text = $frame->expand( $dom, $flags );
  2787. wfProfileOut( __METHOD__ );
  2788. return $text;
  2789. }
  2790. /**
  2791. * Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
  2792. *
  2793. * @param $args array
  2794. *
  2795. * @return array
  2796. */
  2797. static function createAssocArgs( $args ) {
  2798. $assocArgs = array();
  2799. $index = 1;
  2800. foreach ( $args as $arg ) {
  2801. $eqpos = strpos( $arg, '=' );
  2802. if ( $eqpos === false ) {
  2803. $assocArgs[$index++] = $arg;
  2804. } else {
  2805. $name = trim( substr( $arg, 0, $eqpos ) );
  2806. $value = trim( substr( $arg, $eqpos+1 ) );
  2807. if ( $value === false ) {
  2808. $value = '';
  2809. }
  2810. if ( $name !== false ) {
  2811. $assocArgs[$name] = $value;
  2812. }
  2813. }
  2814. }
  2815. return $assocArgs;
  2816. }
  2817. /**
  2818. * Warn the user when a parser limitation is reached
  2819. * Will warn at most once the user per limitation type
  2820. *
  2821. * @param $limitationType String: should be one of:
  2822. * 'expensive-parserfunction' (corresponding messages:
  2823. * 'expensive-parserfunction-warning',
  2824. * 'expensive-parserfunction-category')
  2825. * 'post-expand-template-argument' (corresponding messages:
  2826. * 'post-expand-template-argument-warning',
  2827. * 'post-expand-template-argument-category')
  2828. * 'post-expand-template-inclusion' (corresponding messages:
  2829. * 'post-expand-template-inclusion-warning',
  2830. * 'post-expand-template-inclusion-category')
  2831. * @param $current int|null Current value
  2832. * @param $max int|null Maximum allowed, when an explicit limit has been
  2833. * exceeded, provide the values (optional)
  2834. */
  2835. function limitationWarn( $limitationType, $current = '', $max = '' ) {
  2836. # does no harm if $current and $max are present but are unnecessary for the message
  2837. $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
  2838. ->inContentLanguage()->escaped();
  2839. $this->mOutput->addWarning( $warning );
  2840. $this->addTrackingCategory( "$limitationType-category" );
  2841. }
  2842. /**
  2843. * Return the text of a template, after recursively
  2844. * replacing any variables or templates within the template.
  2845. *
  2846. * @param $piece Array: the parts of the template
  2847. * $piece['title']: the title, i.e. the part before the |
  2848. * $piece['parts']: the parameter array
  2849. * $piece['lineStart']: whether the brace was at the start of a line
  2850. * @param $frame PPFrame The current frame, contains template arguments
  2851. * @return String: the text of the template
  2852. * @private
  2853. */
  2854. function braceSubstitution( $piece, $frame ) {
  2855. global $wgContLang;
  2856. wfProfileIn( __METHOD__ );
  2857. wfProfileIn( __METHOD__.'-setup' );
  2858. # Flags
  2859. $found = false; # $text has been filled
  2860. $nowiki = false; # wiki markup in $text should be escaped
  2861. $isHTML = false; # $text is HTML, armour it against wikitext transformation
  2862. $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
  2863. $isChildObj = false; # $text is a DOM node needing expansion in a child frame
  2864. $isLocalObj = false; # $text is a DOM node needing expansion in the current frame
  2865. # Title object, where $text came from
  2866. $title = false;
  2867. # $part1 is the bit before the first |, and must contain only title characters.
  2868. # Various prefixes will be stripped from it later.
  2869. $titleWithSpaces = $frame->expand( $piece['title'] );
  2870. $part1 = trim( $titleWithSpaces );
  2871. $titleText = false;
  2872. # Original title text preserved for various purposes
  2873. $originalTitle = $part1;
  2874. # $args is a list of argument nodes, starting from index 0, not including $part1
  2875. # @todo FIXME: If piece['parts'] is null then the call to getLength() below won't work b/c this $args isn't an object
  2876. $args = ( null == $piece['parts'] ) ? array() : $piece['parts'];
  2877. wfProfileOut( __METHOD__.'-setup' );
  2878. $titleProfileIn = null; // profile templates
  2879. # SUBST
  2880. wfProfileIn( __METHOD__.'-modifiers' );
  2881. if ( !$found ) {
  2882. $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
  2883. # Possibilities for substMatch: "subst", "safesubst" or FALSE
  2884. # Decide whether to expand template or keep wikitext as-is.
  2885. if ( $this->ot['wiki'] ) {
  2886. if ( $substMatch === false ) {
  2887. $literal = true; # literal when in PST with no prefix
  2888. } else {
  2889. $literal = false; # expand when in PST with subst: or safesubst:
  2890. }
  2891. } else {
  2892. if ( $substMatch == 'subst' ) {
  2893. $literal = true; # literal when not in PST with plain subst:
  2894. } else {
  2895. $literal = false; # expand when not in PST with safesubst: or no prefix
  2896. }
  2897. }
  2898. if ( $literal ) {
  2899. $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
  2900. $isLocalObj = true;
  2901. $found = true;
  2902. }
  2903. }
  2904. # Variables
  2905. if ( !$found && $args->getLength() == 0 ) {
  2906. $id = $this->mVariables->matchStartToEnd( $part1 );
  2907. if ( $id !== false ) {
  2908. $text = $this->getVariableValue( $id, $frame );
  2909. if ( MagicWord::getCacheTTL( $id ) > -1 ) {
  2910. $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
  2911. }
  2912. $found = true;
  2913. }
  2914. }
  2915. # MSG, MSGNW and RAW
  2916. if ( !$found ) {
  2917. # Check for MSGNW:
  2918. $mwMsgnw = MagicWord::get( 'msgnw' );
  2919. if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
  2920. $nowiki = true;
  2921. } else {
  2922. # Remove obsolete MSG:
  2923. $mwMsg = MagicWord::get( 'msg' );
  2924. $mwMsg->matchStartAndRemove( $part1 );
  2925. }
  2926. # Check for RAW:
  2927. $mwRaw = MagicWord::get( 'raw' );
  2928. if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
  2929. $forceRawInterwiki = true;
  2930. }
  2931. }
  2932. wfProfileOut( __METHOD__.'-modifiers' );
  2933. # Parser functions
  2934. if ( !$found ) {
  2935. wfProfileIn( __METHOD__ . '-pfunc' );
  2936. $colonPos = strpos( $part1, ':' );
  2937. if ( $colonPos !== false ) {
  2938. # Case sensitive functions
  2939. $function = substr( $part1, 0, $colonPos );
  2940. if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
  2941. $function = $this->mFunctionSynonyms[1][$function];
  2942. } else {
  2943. # Case insensitive functions
  2944. $function = $wgContLang->lc( $function );
  2945. if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
  2946. $function = $this->mFunctionSynonyms[0][$function];
  2947. } else {
  2948. $function = false;
  2949. }
  2950. }
  2951. if ( $function ) {
  2952. wfProfileIn( __METHOD__ . '-pfunc-' . $function );
  2953. list( $callback, $flags ) = $this->mFunctionHooks[$function];
  2954. $initialArgs = array( &$this );
  2955. $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
  2956. if ( $flags & SFH_OBJECT_ARGS ) {
  2957. # Add a frame parameter, and pass the arguments as an array
  2958. $allArgs = $initialArgs;
  2959. $allArgs[] = $frame;
  2960. for ( $i = 0; $i < $args->getLength(); $i++ ) {
  2961. $funcArgs[] = $args->item( $i );
  2962. }
  2963. $allArgs[] = $funcArgs;
  2964. } else {
  2965. # Convert arguments to plain text
  2966. for ( $i = 0; $i < $args->getLength(); $i++ ) {
  2967. $funcArgs[] = trim( $frame->expand( $args->item( $i ) ) );
  2968. }
  2969. $allArgs = array_merge( $initialArgs, $funcArgs );
  2970. }
  2971. # Workaround for PHP bug 35229 and similar
  2972. if ( !is_callable( $callback ) ) {
  2973. wfProfileOut( __METHOD__ . '-pfunc-' . $function );
  2974. wfProfileOut( __METHOD__ . '-pfunc' );
  2975. wfProfileOut( __METHOD__ );
  2976. throw new MWException( "Tag hook for $function is not callable\n" );
  2977. }
  2978. $result = call_user_func_array( $callback, $allArgs );
  2979. $found = true;
  2980. $noparse = true;
  2981. $preprocessFlags = 0;
  2982. if ( is_array( $result ) ) {
  2983. if ( isset( $result[0] ) ) {
  2984. $text = $result[0];
  2985. unset( $result[0] );
  2986. }
  2987. # Extract flags into the local scope
  2988. # This allows callers to set flags such as nowiki, found, etc.
  2989. extract( $result );
  2990. } else {
  2991. $text = $result;
  2992. }
  2993. if ( !$noparse ) {
  2994. $text = $this->preprocessToDom( $text, $preprocessFlags );
  2995. $isChildObj = true;
  2996. }
  2997. wfProfileOut( __METHOD__ . '-pfunc-' . $function );
  2998. }
  2999. }
  3000. wfProfileOut( __METHOD__ . '-pfunc' );
  3001. }
  3002. # Finish mangling title and then check for loops.
  3003. # Set $title to a Title object and $titleText to the PDBK
  3004. if ( !$found ) {
  3005. $ns = NS_TEMPLATE;
  3006. # Split the title into page and subpage
  3007. $subpage = '';
  3008. $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
  3009. if ( $subpage !== '' ) {
  3010. $ns = $this->mTitle->getNamespace();
  3011. }
  3012. $title = Title::newFromText( $part1, $ns );
  3013. if ( $title ) {
  3014. $titleText = $title->getPrefixedText();
  3015. # Check for language variants if the template is not found
  3016. if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
  3017. $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
  3018. }
  3019. # Do recursion depth check
  3020. $limit = $this->mOptions->getMaxTemplateDepth();
  3021. if ( $frame->depth >= $limit ) {
  3022. $found = true;
  3023. $text = '<span class="error">'
  3024. . wfMessage( 'parser-template-recursion-depth-warning' )
  3025. ->numParams( $limit )->inContentLanguage()->text()
  3026. . '</span>';
  3027. }
  3028. }
  3029. }
  3030. # Load from database
  3031. if ( !$found && $title ) {
  3032. if ( !Profiler::instance()->isPersistent() ) {
  3033. # Too many unique items can kill profiling DBs/collectors
  3034. $titleProfileIn = __METHOD__ . "-title-" . $title->getDBKey();
  3035. wfProfileIn( $titleProfileIn ); // template in
  3036. }
  3037. wfProfileIn( __METHOD__ . '-loadtpl' );
  3038. if ( !$title->isExternal() ) {
  3039. if ( $title->isSpecialPage()
  3040. && $this->mOptions->getAllowSpecialInclusion()
  3041. && $this->ot['html'] )
  3042. {
  3043. // Pass the template arguments as URL parameters.
  3044. // "uselang" will have no effect since the Language object
  3045. // is forced to the one defined in ParserOptions.
  3046. $pageArgs = array();
  3047. for ( $i = 0; $i < $args->getLength(); $i++ ) {
  3048. $bits = $args->item( $i )->splitArg();
  3049. if ( strval( $bits['index'] ) === '' ) {
  3050. $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
  3051. $value = trim( $frame->expand( $bits['value'] ) );
  3052. $pageArgs[$name] = $value;
  3053. }
  3054. }
  3055. // Create a new context to execute the special page
  3056. $context = new RequestContext;
  3057. $context->setTitle( $title );
  3058. $context->setRequest( new FauxRequest( $pageArgs ) );
  3059. $context->setUser( $this->getUser() );
  3060. $context->setLanguage( $this->mOptions->getUserLangObj() );
  3061. $ret = SpecialPageFactory::capturePath( $title, $context );
  3062. if ( $ret ) {
  3063. $text = $context->getOutput()->getHTML();
  3064. $this->mOutput->addOutputPageMetadata( $context->getOutput() );
  3065. $found = true;
  3066. $isHTML = true;
  3067. $this->disableCache();
  3068. }
  3069. } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
  3070. $found = false; # access denied
  3071. wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() );
  3072. } else {
  3073. list( $text, $title ) = $this->getTemplateDom( $title );
  3074. if ( $text !== false ) {
  3075. $found = true;
  3076. $isChildObj = true;
  3077. }
  3078. }
  3079. # If the title is valid but undisplayable, make a link to it
  3080. if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
  3081. $text = "[[:$titleText]]";
  3082. $found = true;
  3083. }
  3084. } elseif ( $title->isTrans() ) {
  3085. # Interwiki transclusion
  3086. if ( $this->ot['html'] && !$forceRawInterwiki ) {
  3087. $text = $this->interwikiTransclude( $title, 'render' );
  3088. $isHTML = true;
  3089. } else {
  3090. $text = $this->interwikiTransclude( $title, 'raw' );
  3091. # Preprocess it like a template
  3092. $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
  3093. $isChildObj = true;
  3094. }
  3095. $found = true;
  3096. }
  3097. # Do infinite loop check
  3098. # This has to be done after redirect resolution to avoid infinite loops via redirects
  3099. if ( !$frame->loopCheck( $title ) ) {
  3100. $found = true;
  3101. $text = '<span class="error">'
  3102. . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
  3103. . '</span>';
  3104. wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
  3105. }
  3106. wfProfileOut( __METHOD__ . '-loadtpl' );
  3107. }
  3108. # If we haven't found text to substitute by now, we're done
  3109. # Recover the source wikitext and return it
  3110. if ( !$found ) {
  3111. $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
  3112. if ( $titleProfileIn ) {
  3113. wfProfileOut( $titleProfileIn ); // template out
  3114. }
  3115. wfProfileOut( __METHOD__ );
  3116. return array( 'object' => $text );
  3117. }
  3118. # Expand DOM-style return values in a child frame
  3119. if ( $isChildObj ) {
  3120. # Clean up argument array
  3121. $newFrame = $frame->newChild( $args, $title );
  3122. if ( $nowiki ) {
  3123. $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
  3124. } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
  3125. # Expansion is eligible for the empty-frame cache
  3126. if ( isset( $this->mTplExpandCache[$titleText] ) ) {
  3127. $text = $this->mTplExpandCache[$titleText];
  3128. } else {
  3129. $text = $newFrame->expand( $text );
  3130. $this->mTplExpandCache[$titleText] = $text;
  3131. }
  3132. } else {
  3133. # Uncached expansion
  3134. $text = $newFrame->expand( $text );
  3135. }
  3136. }
  3137. if ( $isLocalObj && $nowiki ) {
  3138. $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
  3139. $isLocalObj = false;
  3140. }
  3141. if ( $titleProfileIn ) {
  3142. wfProfileOut( $titleProfileIn ); // template out
  3143. }
  3144. # Replace raw HTML by a placeholder
  3145. if ( $isHTML ) {
  3146. $text = $this->insertStripItem( $text );
  3147. } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
  3148. # Escape nowiki-style return values
  3149. $text = wfEscapeWikiText( $text );
  3150. } elseif ( is_string( $text )
  3151. && !$piece['lineStart']
  3152. && preg_match( '/^(?:{\\||:|;|#|\*)/', $text ) )
  3153. {
  3154. # Bug 529: if the template begins with a table or block-level
  3155. # element, it should be treated as beginning a new line.
  3156. # This behaviour is somewhat controversial.
  3157. $text = "\n" . $text;
  3158. }
  3159. if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
  3160. # Error, oversize inclusion
  3161. if ( $titleText !== false ) {
  3162. # Make a working, properly escaped link if possible (bug 23588)
  3163. $text = "[[:$titleText]]";
  3164. } else {
  3165. # This will probably not be a working link, but at least it may
  3166. # provide some hint of where the problem is
  3167. preg_replace( '/^:/', '', $originalTitle );
  3168. $text = "[[:$originalTitle]]";
  3169. }
  3170. $text .= $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' );
  3171. $this->limitationWarn( 'post-expand-template-inclusion' );
  3172. }
  3173. if ( $isLocalObj ) {
  3174. $ret = array( 'object' => $text );
  3175. } else {
  3176. $ret = array( 'text' => $text );
  3177. }
  3178. wfProfileOut( __METHOD__ );
  3179. return $ret;
  3180. }
  3181. /**
  3182. * Get the semi-parsed DOM representation of a template with a given title,
  3183. * and its redirect destination title. Cached.
  3184. *
  3185. * @param $title Title
  3186. *
  3187. * @return array
  3188. */
  3189. function getTemplateDom( $title ) {
  3190. $cacheTitle = $title;
  3191. $titleText = $title->getPrefixedDBkey();
  3192. if ( isset( $this->mTplRedirCache[$titleText] ) ) {
  3193. list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
  3194. $title = Title::makeTitle( $ns, $dbk );
  3195. $titleText = $title->getPrefixedDBkey();
  3196. }
  3197. if ( isset( $this->mTplDomCache[$titleText] ) ) {
  3198. return array( $this->mTplDomCache[$titleText], $title );
  3199. }
  3200. # Cache miss, go to the database
  3201. list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
  3202. if ( $text === false ) {
  3203. $this->mTplDomCache[$titleText] = false;
  3204. return array( false, $title );
  3205. }
  3206. $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
  3207. $this->mTplDomCache[ $titleText ] = $dom;
  3208. if ( !$title->equals( $cacheTitle ) ) {
  3209. $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
  3210. array( $title->getNamespace(), $cdb = $title->getDBkey() );
  3211. }
  3212. return array( $dom, $title );
  3213. }
  3214. /**
  3215. * Fetch the unparsed text of a template and register a reference to it.
  3216. * @param Title $title
  3217. * @return Array ( string or false, Title )
  3218. */
  3219. function fetchTemplateAndTitle( $title ) {
  3220. $templateCb = $this->mOptions->getTemplateCallback(); # Defaults to Parser::statelessFetchTemplate()
  3221. $stuff = call_user_func( $templateCb, $title, $this );
  3222. $text = $stuff['text'];
  3223. $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
  3224. if ( isset( $stuff['deps'] ) ) {
  3225. foreach ( $stuff['deps'] as $dep ) {
  3226. $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
  3227. }
  3228. }
  3229. return array( $text, $finalTitle );
  3230. }
  3231. /**
  3232. * Fetch the unparsed text of a template and register a reference to it.
  3233. * @param Title $title
  3234. * @return mixed string or false
  3235. */
  3236. function fetchTemplate( $title ) {
  3237. $rv = $this->fetchTemplateAndTitle( $title );
  3238. return $rv[0];
  3239. }
  3240. /**
  3241. * Static function to get a template
  3242. * Can be overridden via ParserOptions::setTemplateCallback().
  3243. *
  3244. * @param $title Title
  3245. * @param $parser Parser
  3246. *
  3247. * @return array
  3248. */
  3249. static function statelessFetchTemplate( $title, $parser = false ) {
  3250. $text = $skip = false;
  3251. $finalTitle = $title;
  3252. $deps = array();
  3253. # Loop to fetch the article, with up to 1 redirect
  3254. for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
  3255. # Give extensions a chance to select the revision instead
  3256. $id = false; # Assume current
  3257. wfRunHooks( 'BeforeParserFetchTemplateAndtitle',
  3258. array( $parser, $title, &$skip, &$id ) );
  3259. if ( $skip ) {
  3260. $text = false;
  3261. $deps[] = array(
  3262. 'title' => $title,
  3263. 'page_id' => $title->getArticleID(),
  3264. 'rev_id' => null
  3265. );
  3266. break;
  3267. }
  3268. # Get the revision
  3269. $rev = $id
  3270. ? Revision::newFromId( $id )
  3271. : Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
  3272. $rev_id = $rev ? $rev->getId() : 0;
  3273. # If there is no current revision, there is no page
  3274. if ( $id === false && !$rev ) {
  3275. $linkCache = LinkCache::singleton();
  3276. $linkCache->addBadLinkObj( $title );
  3277. }
  3278. $deps[] = array(
  3279. 'title' => $title,
  3280. 'page_id' => $title->getArticleID(),
  3281. 'rev_id' => $rev_id );
  3282. if ( $rev && !$title->equals( $rev->getTitle() ) ) {
  3283. # We fetched a rev from a different title; register it too...
  3284. $deps[] = array(
  3285. 'title' => $rev->getTitle(),
  3286. 'page_id' => $rev->getPage(),
  3287. 'rev_id' => $rev_id );
  3288. }
  3289. if ( $rev ) {
  3290. $text = $rev->getText();
  3291. } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
  3292. global $wgContLang;
  3293. $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
  3294. if ( !$message->exists() ) {
  3295. $text = false;
  3296. break;
  3297. }
  3298. $text = $message->plain();
  3299. } else {
  3300. break;
  3301. }
  3302. if ( $text === false ) {
  3303. break;
  3304. }
  3305. # Redirect?
  3306. $finalTitle = $title;
  3307. $title = Title::newFromRedirect( $text );
  3308. }
  3309. return array(
  3310. 'text' => $text,
  3311. 'finalTitle' => $finalTitle,
  3312. 'deps' => $deps );
  3313. }
  3314. /**
  3315. * Fetch a file and its title and register a reference to it.
  3316. * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
  3317. * @param Title $title
  3318. * @param Array $options Array of options to RepoGroup::findFile
  3319. * @return File|bool
  3320. */
  3321. function fetchFile( $title, $options = array() ) {
  3322. $res = $this->fetchFileAndTitle( $title, $options );
  3323. return $res[0];
  3324. }
  3325. /**
  3326. * Fetch a file and its title and register a reference to it.
  3327. * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
  3328. * @param Title $title
  3329. * @param Array $options Array of options to RepoGroup::findFile
  3330. * @return Array ( File or false, Title of file )
  3331. */
  3332. function fetchFileAndTitle( $title, $options = array() ) {
  3333. if ( isset( $options['broken'] ) ) {
  3334. $file = false; // broken thumbnail forced by hook
  3335. } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
  3336. $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
  3337. } else { // get by (name,timestamp)
  3338. $file = wfFindFile( $title, $options );
  3339. }
  3340. $time = $file ? $file->getTimestamp() : false;
  3341. $sha1 = $file ? $file->getSha1() : false;
  3342. # Register the file as a dependency...
  3343. $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
  3344. if ( $file && !$title->equals( $file->getTitle() ) ) {
  3345. # Update fetched file title
  3346. $title = $file->getTitle();
  3347. if ( is_null( $file->getRedirectedTitle() ) ) {
  3348. # This file was not a redirect, but the title does not match.
  3349. # Register under the new name because otherwise the link will
  3350. # get lost.
  3351. $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
  3352. }
  3353. }
  3354. return array( $file, $title );
  3355. }
  3356. /**
  3357. * Transclude an interwiki link.
  3358. *
  3359. * @param $title Title
  3360. * @param $action
  3361. *
  3362. * @return string
  3363. */
  3364. function interwikiTransclude( $title, $action ) {
  3365. global $wgEnableScaryTranscluding;
  3366. if ( !$wgEnableScaryTranscluding ) {
  3367. return wfMessage('scarytranscludedisabled')->inContentLanguage()->text();
  3368. }
  3369. $url = $title->getFullUrl( "action=$action" );
  3370. if ( strlen( $url ) > 255 ) {
  3371. return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
  3372. }
  3373. return $this->fetchScaryTemplateMaybeFromCache( $url );
  3374. }
  3375. /**
  3376. * @param $url string
  3377. * @return Mixed|String
  3378. */
  3379. function fetchScaryTemplateMaybeFromCache( $url ) {
  3380. global $wgTranscludeCacheExpiry;
  3381. $dbr = wfGetDB( DB_SLAVE );
  3382. $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
  3383. $obj = $dbr->selectRow( 'transcache', array('tc_time', 'tc_contents' ),
  3384. array( 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) );
  3385. if ( $obj ) {
  3386. return $obj->tc_contents;
  3387. }
  3388. $text = Http::get( $url );
  3389. if ( !$text ) {
  3390. return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
  3391. }
  3392. $dbw = wfGetDB( DB_MASTER );
  3393. $dbw->replace( 'transcache', array('tc_url'), array(
  3394. 'tc_url' => $url,
  3395. 'tc_time' => $dbw->timestamp( time() ),
  3396. 'tc_contents' => $text)
  3397. );
  3398. return $text;
  3399. }
  3400. /**
  3401. * Triple brace replacement -- used for template arguments
  3402. * @private
  3403. *
  3404. * @param $piece array
  3405. * @param $frame PPFrame
  3406. *
  3407. * @return array
  3408. */
  3409. function argSubstitution( $piece, $frame ) {
  3410. wfProfileIn( __METHOD__ );
  3411. $error = false;
  3412. $parts = $piece['parts'];
  3413. $nameWithSpaces = $frame->expand( $piece['title'] );
  3414. $argName = trim( $nameWithSpaces );
  3415. $object = false;
  3416. $text = $frame->getArgument( $argName );
  3417. if ( $text === false && $parts->getLength() > 0
  3418. && (
  3419. $this->ot['html']
  3420. || $this->ot['pre']
  3421. || ( $this->ot['wiki'] && $frame->isTemplate() )
  3422. )
  3423. ) {
  3424. # No match in frame, use the supplied default
  3425. $object = $parts->item( 0 )->getChildren();
  3426. }
  3427. if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
  3428. $error = '<!-- WARNING: argument omitted, expansion size too large -->';
  3429. $this->limitationWarn( 'post-expand-template-argument' );
  3430. }
  3431. if ( $text === false && $object === false ) {
  3432. # No match anywhere
  3433. $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
  3434. }
  3435. if ( $error !== false ) {
  3436. $text .= $error;
  3437. }
  3438. if ( $object !== false ) {
  3439. $ret = array( 'object' => $object );
  3440. } else {
  3441. $ret = array( 'text' => $text );
  3442. }
  3443. wfProfileOut( __METHOD__ );
  3444. return $ret;
  3445. }
  3446. /**
  3447. * Return the text to be used for a given extension tag.
  3448. * This is the ghost of strip().
  3449. *
  3450. * @param $params array Associative array of parameters:
  3451. * name PPNode for the tag name
  3452. * attr PPNode for unparsed text where tag attributes are thought to be
  3453. * attributes Optional associative array of parsed attributes
  3454. * inner Contents of extension element
  3455. * noClose Original text did not have a close tag
  3456. * @param $frame PPFrame
  3457. *
  3458. * @return string
  3459. */
  3460. function extensionSubstitution( $params, $frame ) {
  3461. $name = $frame->expand( $params['name'] );
  3462. $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
  3463. $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
  3464. $marker = "{$this->mUniqPrefix}-$name-" . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
  3465. $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower($name)] ) &&
  3466. ( $this->ot['html'] || $this->ot['pre'] );
  3467. if ( $isFunctionTag ) {
  3468. $markerType = 'none';
  3469. } else {
  3470. $markerType = 'general';
  3471. }
  3472. if ( $this->ot['html'] || $isFunctionTag ) {
  3473. $name = strtolower( $name );
  3474. $attributes = Sanitizer::decodeTagAttributes( $attrText );
  3475. if ( isset( $params['attributes'] ) ) {
  3476. $attributes = $attributes + $params['attributes'];
  3477. }
  3478. if ( isset( $this->mTagHooks[$name] ) ) {
  3479. # Workaround for PHP bug 35229 and similar
  3480. if ( !is_callable( $this->mTagHooks[$name] ) ) {
  3481. throw new MWException( "Tag hook for $name is not callable\n" );
  3482. }
  3483. $output = call_user_func_array( $this->mTagHooks[$name],
  3484. array( $content, $attributes, $this, $frame ) );
  3485. } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
  3486. list( $callback, $flags ) = $this->mFunctionTagHooks[$name];
  3487. if ( !is_callable( $callback ) ) {
  3488. throw new MWException( "Tag hook for $name is not callable\n" );
  3489. }
  3490. $output = call_user_func_array( $callback, array( &$this, $frame, $content, $attributes ) );
  3491. } else {
  3492. $output = '<span class="error">Invalid tag extension name: ' .
  3493. htmlspecialchars( $name ) . '</span>';
  3494. }
  3495. if ( is_array( $output ) ) {
  3496. # Extract flags to local scope (to override $markerType)
  3497. $flags = $output;
  3498. $output = $flags[0];
  3499. unset( $flags[0] );
  3500. extract( $flags );
  3501. }
  3502. } else {
  3503. if ( is_null( $attrText ) ) {
  3504. $attrText = '';
  3505. }
  3506. if ( isset( $params['attributes'] ) ) {
  3507. foreach ( $params['attributes'] as $attrName => $attrValue ) {
  3508. $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
  3509. htmlspecialchars( $attrValue ) . '"';
  3510. }
  3511. }
  3512. if ( $content === null ) {
  3513. $output = "<$name$attrText/>";
  3514. } else {
  3515. $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
  3516. $output = "<$name$attrText>$content$close";
  3517. }
  3518. }
  3519. if ( $markerType === 'none' ) {
  3520. return $output;
  3521. } elseif ( $markerType === 'nowiki' ) {
  3522. $this->mStripState->addNoWiki( $marker, $output );
  3523. } elseif ( $markerType === 'general' ) {
  3524. $this->mStripState->addGeneral( $marker, $output );
  3525. } else {
  3526. throw new MWException( __METHOD__.': invalid marker type' );
  3527. }
  3528. return $marker;
  3529. }
  3530. /**
  3531. * Increment an include size counter
  3532. *
  3533. * @param $type String: the type of expansion
  3534. * @param $size Integer: the size of the text
  3535. * @return Boolean: false if this inclusion would take it over the maximum, true otherwise
  3536. */
  3537. function incrementIncludeSize( $type, $size ) {
  3538. if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
  3539. return false;
  3540. } else {
  3541. $this->mIncludeSizes[$type] += $size;
  3542. return true;
  3543. }
  3544. }
  3545. /**
  3546. * Increment the expensive function count
  3547. *
  3548. * @return Boolean: false if the limit has been exceeded
  3549. */
  3550. function incrementExpensiveFunctionCount() {
  3551. $this->mExpensiveFunctionCount++;
  3552. return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
  3553. }
  3554. /**
  3555. * Strip double-underscore items like __NOGALLERY__ and __NOTOC__
  3556. * Fills $this->mDoubleUnderscores, returns the modified text
  3557. *
  3558. * @param $text string
  3559. *
  3560. * @return string
  3561. */
  3562. function doDoubleUnderscore( $text ) {
  3563. wfProfileIn( __METHOD__ );
  3564. # The position of __TOC__ needs to be recorded
  3565. $mw = MagicWord::get( 'toc' );
  3566. if ( $mw->match( $text ) ) {
  3567. $this->mShowToc = true;
  3568. $this->mForceTocPosition = true;
  3569. # Set a placeholder. At the end we'll fill it in with the TOC.
  3570. $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
  3571. # Only keep the first one.
  3572. $text = $mw->replace( '', $text );
  3573. }
  3574. # Now match and remove the rest of them
  3575. $mwa = MagicWord::getDoubleUnderscoreArray();
  3576. $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
  3577. if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
  3578. $this->mOutput->mNoGallery = true;
  3579. }
  3580. if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
  3581. $this->mShowToc = false;
  3582. }
  3583. if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) {
  3584. $this->addTrackingCategory( 'hidden-category-category' );
  3585. }
  3586. # (bug 8068) Allow control over whether robots index a page.
  3587. #
  3588. # @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This
  3589. # is not desirable, the last one on the page should win.
  3590. if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
  3591. $this->mOutput->setIndexPolicy( 'noindex' );
  3592. $this->addTrackingCategory( 'noindex-category' );
  3593. }
  3594. if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
  3595. $this->mOutput->setIndexPolicy( 'index' );
  3596. $this->addTrackingCategory( 'index-category' );
  3597. }
  3598. # Cache all double underscores in the database
  3599. foreach ( $this->mDoubleUnderscores as $key => $val ) {
  3600. $this->mOutput->setProperty( $key, '' );
  3601. }
  3602. wfProfileOut( __METHOD__ );
  3603. return $text;
  3604. }
  3605. /**
  3606. * Add a tracking category, getting the title from a system message,
  3607. * or print a debug message if the title is invalid.
  3608. *
  3609. * @param $msg String: message key
  3610. * @return Boolean: whether the addition was successful
  3611. */
  3612. public function addTrackingCategory( $msg ) {
  3613. if ( $this->mTitle->getNamespace() === NS_SPECIAL ) {
  3614. wfDebug( __METHOD__.": Not adding tracking category $msg to special page!\n" );
  3615. return false;
  3616. }
  3617. // Important to parse with correct title (bug 31469)
  3618. $cat = wfMessage( $msg )
  3619. ->title( $this->getTitle() )
  3620. ->inContentLanguage()
  3621. ->text();
  3622. # Allow tracking categories to be disabled by setting them to "-"
  3623. if ( $cat === '-' ) {
  3624. return false;
  3625. }
  3626. $containerCategory = Title::makeTitleSafe( NS_CATEGORY, $cat );
  3627. if ( $containerCategory ) {
  3628. $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
  3629. return true;
  3630. } else {
  3631. wfDebug( __METHOD__.": [[MediaWiki:$msg]] is not a valid title!\n" );
  3632. return false;
  3633. }
  3634. }
  3635. /**
  3636. * This function accomplishes several tasks:
  3637. * 1) Auto-number headings if that option is enabled
  3638. * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
  3639. * 3) Add a Table of contents on the top for users who have enabled the option
  3640. * 4) Auto-anchor headings
  3641. *
  3642. * It loops through all headlines, collects the necessary data, then splits up the
  3643. * string and re-inserts the newly formatted headlines.
  3644. *
  3645. * @param $text String
  3646. * @param $origText String: original, untouched wikitext
  3647. * @param $isMain Boolean
  3648. * @return mixed|string
  3649. * @private
  3650. */
  3651. function formatHeadings( $text, $origText, $isMain=true ) {
  3652. global $wgMaxTocLevel, $wgHtml5, $wgExperimentalHtmlIds;
  3653. # Inhibit editsection links if requested in the page
  3654. if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
  3655. $maybeShowEditLink = $showEditLink = false;
  3656. } else {
  3657. $maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
  3658. $showEditLink = $this->mOptions->getEditSection();
  3659. }
  3660. if ( $showEditLink ) {
  3661. $this->mOutput->setEditSectionTokens( true );
  3662. }
  3663. # Get all headlines for numbering them and adding funky stuff like [edit]
  3664. # links - this is for later, but we need the number of headlines right now
  3665. $matches = array();
  3666. $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches );
  3667. # if there are fewer than 4 headlines in the article, do not show TOC
  3668. # unless it's been explicitly enabled.
  3669. $enoughToc = $this->mShowToc &&
  3670. ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
  3671. # Allow user to stipulate that a page should have a "new section"
  3672. # link added via __NEWSECTIONLINK__
  3673. if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
  3674. $this->mOutput->setNewSection( true );
  3675. }
  3676. # Allow user to remove the "new section"
  3677. # link via __NONEWSECTIONLINK__
  3678. if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
  3679. $this->mOutput->hideNewSection( true );
  3680. }
  3681. # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
  3682. # override above conditions and always show TOC above first header
  3683. if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
  3684. $this->mShowToc = true;
  3685. $enoughToc = true;
  3686. }
  3687. # headline counter
  3688. $headlineCount = 0;
  3689. $numVisible = 0;
  3690. # Ugh .. the TOC should have neat indentation levels which can be
  3691. # passed to the skin functions. These are determined here
  3692. $toc = '';
  3693. $full = '';
  3694. $head = array();
  3695. $sublevelCount = array();
  3696. $levelCount = array();
  3697. $level = 0;
  3698. $prevlevel = 0;
  3699. $toclevel = 0;
  3700. $prevtoclevel = 0;
  3701. $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
  3702. $baseTitleText = $this->mTitle->getPrefixedDBkey();
  3703. $oldType = $this->mOutputType;
  3704. $this->setOutputType( self::OT_WIKI );
  3705. $frame = $this->getPreprocessor()->newFrame();
  3706. $root = $this->preprocessToDom( $origText );
  3707. $node = $root->getFirstChild();
  3708. $byteOffset = 0;
  3709. $tocraw = array();
  3710. $refers = array();
  3711. foreach ( $matches[3] as $headline ) {
  3712. $isTemplate = false;
  3713. $titleText = false;
  3714. $sectionIndex = false;
  3715. $numbering = '';
  3716. $markerMatches = array();
  3717. if ( preg_match("/^$markerRegex/", $headline, $markerMatches ) ) {
  3718. $serial = $markerMatches[1];
  3719. list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
  3720. $isTemplate = ( $titleText != $baseTitleText );
  3721. $headline = preg_replace( "/^$markerRegex/", "", $headline );
  3722. }
  3723. if ( $toclevel ) {
  3724. $prevlevel = $level;
  3725. }
  3726. $level = $matches[1][$headlineCount];
  3727. if ( $level > $prevlevel ) {
  3728. # Increase TOC level
  3729. $toclevel++;
  3730. $sublevelCount[$toclevel] = 0;
  3731. if ( $toclevel<$wgMaxTocLevel ) {
  3732. $prevtoclevel = $toclevel;
  3733. $toc .= Linker::tocIndent();
  3734. $numVisible++;
  3735. }
  3736. } elseif ( $level < $prevlevel && $toclevel > 1 ) {
  3737. # Decrease TOC level, find level to jump to
  3738. for ( $i = $toclevel; $i > 0; $i-- ) {
  3739. if ( $levelCount[$i] == $level ) {
  3740. # Found last matching level
  3741. $toclevel = $i;
  3742. break;
  3743. } elseif ( $levelCount[$i] < $level ) {
  3744. # Found first matching level below current level
  3745. $toclevel = $i + 1;
  3746. break;
  3747. }
  3748. }
  3749. if ( $i == 0 ) {
  3750. $toclevel = 1;
  3751. }
  3752. if ( $toclevel<$wgMaxTocLevel ) {
  3753. if ( $prevtoclevel < $wgMaxTocLevel ) {
  3754. # Unindent only if the previous toc level was shown :p
  3755. $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
  3756. $prevtoclevel = $toclevel;
  3757. } else {
  3758. $toc .= Linker::tocLineEnd();
  3759. }
  3760. }
  3761. } else {
  3762. # No change in level, end TOC line
  3763. if ( $toclevel<$wgMaxTocLevel ) {
  3764. $toc .= Linker::tocLineEnd();
  3765. }
  3766. }
  3767. $levelCount[$toclevel] = $level;
  3768. # count number of headlines for each level
  3769. @$sublevelCount[$toclevel]++;
  3770. $dot = 0;
  3771. for( $i = 1; $i <= $toclevel; $i++ ) {
  3772. if ( !empty( $sublevelCount[$i] ) ) {
  3773. if ( $dot ) {
  3774. $numbering .= '.';
  3775. }
  3776. $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
  3777. $dot = 1;
  3778. }
  3779. }
  3780. # The safe header is a version of the header text safe to use for links
  3781. # Remove link placeholders by the link text.
  3782. # <!--LINK number-->
  3783. # turns into
  3784. # link text with suffix
  3785. # Do this before unstrip since link text can contain strip markers
  3786. $safeHeadline = $this->replaceLinkHoldersText( $headline );
  3787. # Avoid insertion of weird stuff like <math> by expanding the relevant sections
  3788. $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
  3789. # Strip out HTML (first regex removes any tag not allowed)
  3790. # Allowed tags are <sup> and <sub> (bug 8393), <i> (bug 26375) and <b> (r105284)
  3791. # We strip any parameter from accepted tags (second regex)
  3792. $tocline = preg_replace(
  3793. array( '#<(?!/?(sup|sub|i|b)(?: [^>]*)?>).*?'.'>#', '#<(/?(sup|sub|i|b))(?: .*?)?'.'>#' ),
  3794. array( '', '<$1>' ),
  3795. $safeHeadline
  3796. );
  3797. $tocline = trim( $tocline );
  3798. # For the anchor, strip out HTML-y stuff period
  3799. $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
  3800. $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
  3801. # Save headline for section edit hint before it's escaped
  3802. $headlineHint = $safeHeadline;
  3803. if ( $wgHtml5 && $wgExperimentalHtmlIds ) {
  3804. # For reverse compatibility, provide an id that's
  3805. # HTML4-compatible, like we used to.
  3806. #
  3807. # It may be worth noting, academically, that it's possible for
  3808. # the legacy anchor to conflict with a non-legacy headline
  3809. # anchor on the page. In this case likely the "correct" thing
  3810. # would be to either drop the legacy anchors or make sure
  3811. # they're numbered first. However, this would require people
  3812. # to type in section names like "abc_.D7.93.D7.90.D7.A4"
  3813. # manually, so let's not bother worrying about it.
  3814. $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
  3815. array( 'noninitial', 'legacy' ) );
  3816. $safeHeadline = Sanitizer::escapeId( $safeHeadline );
  3817. if ( $legacyHeadline == $safeHeadline ) {
  3818. # No reason to have both (in fact, we can't)
  3819. $legacyHeadline = false;
  3820. }
  3821. } else {
  3822. $legacyHeadline = false;
  3823. $safeHeadline = Sanitizer::escapeId( $safeHeadline,
  3824. 'noninitial' );
  3825. }
  3826. # HTML names must be case-insensitively unique (bug 10721).
  3827. # This does not apply to Unicode characters per
  3828. # http://dev.w3.org/html5/spec/infrastructure.html#case-sensitivity-and-string-comparison
  3829. # @todo FIXME: We may be changing them depending on the current locale.
  3830. $arrayKey = strtolower( $safeHeadline );
  3831. if ( $legacyHeadline === false ) {
  3832. $legacyArrayKey = false;
  3833. } else {
  3834. $legacyArrayKey = strtolower( $legacyHeadline );
  3835. }
  3836. # count how many in assoc. array so we can track dupes in anchors
  3837. if ( isset( $refers[$arrayKey] ) ) {
  3838. $refers[$arrayKey]++;
  3839. } else {
  3840. $refers[$arrayKey] = 1;
  3841. }
  3842. if ( isset( $refers[$legacyArrayKey] ) ) {
  3843. $refers[$legacyArrayKey]++;
  3844. } else {
  3845. $refers[$legacyArrayKey] = 1;
  3846. }
  3847. # Don't number the heading if it is the only one (looks silly)
  3848. if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
  3849. # the two are different if the line contains a link
  3850. $headline = Html::element( 'span', array( 'class' => 'mw-headline-number' ), $numbering ) . ' ' . $headline;
  3851. }
  3852. # Create the anchor for linking from the TOC to the section
  3853. $anchor = $safeHeadline;
  3854. $legacyAnchor = $legacyHeadline;
  3855. if ( $refers[$arrayKey] > 1 ) {
  3856. $anchor .= '_' . $refers[$arrayKey];
  3857. }
  3858. if ( $legacyHeadline !== false && $refers[$legacyArrayKey] > 1 ) {
  3859. $legacyAnchor .= '_' . $refers[$legacyArrayKey];
  3860. }
  3861. if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
  3862. $toc .= Linker::tocLine( $anchor, $tocline,
  3863. $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
  3864. }
  3865. # Add the section to the section tree
  3866. # Find the DOM node for this header
  3867. while ( $node && !$isTemplate ) {
  3868. if ( $node->getName() === 'h' ) {
  3869. $bits = $node->splitHeading();
  3870. if ( $bits['i'] == $sectionIndex ) {
  3871. break;
  3872. }
  3873. }
  3874. $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
  3875. $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
  3876. $node = $node->getNextSibling();
  3877. }
  3878. $tocraw[] = array(
  3879. 'toclevel' => $toclevel,
  3880. 'level' => $level,
  3881. 'line' => $tocline,
  3882. 'number' => $numbering,
  3883. 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
  3884. 'fromtitle' => $titleText,
  3885. 'byteoffset' => ( $isTemplate ? null : $byteOffset ),
  3886. 'anchor' => $anchor,
  3887. );
  3888. # give headline the correct <h#> tag
  3889. if ( $maybeShowEditLink && $sectionIndex !== false ) {
  3890. // Output edit section links as markers with styles that can be customized by skins
  3891. if ( $isTemplate ) {
  3892. # Put a T flag in the section identifier, to indicate to extractSections()
  3893. # that sections inside <includeonly> should be counted.
  3894. $editlinkArgs = array( $titleText, "T-$sectionIndex"/*, null */ );
  3895. } else {
  3896. $editlinkArgs = array( $this->mTitle->getPrefixedText(), $sectionIndex, $headlineHint );
  3897. }
  3898. // We use a bit of pesudo-xml for editsection markers. The language converter is run later on
  3899. // Using a UNIQ style marker leads to the converter screwing up the tokens when it converts stuff
  3900. // And trying to insert strip tags fails too. At this point all real inputted tags have already been escaped
  3901. // so we don't have to worry about a user trying to input one of these markers directly.
  3902. // We use a page and section attribute to stop the language converter from converting these important bits
  3903. // of data, but put the headline hint inside a content block because the language converter is supposed to
  3904. // be able to convert that piece of data.
  3905. $editlink = '<mw:editsection page="' . htmlspecialchars($editlinkArgs[0]);
  3906. $editlink .= '" section="' . htmlspecialchars($editlinkArgs[1]) .'"';
  3907. if ( isset($editlinkArgs[2]) ) {
  3908. $editlink .= '>' . $editlinkArgs[2] . '</mw:editsection>';
  3909. } else {
  3910. $editlink .= '/>';
  3911. }
  3912. } else {
  3913. $editlink = '';
  3914. }
  3915. $head[$headlineCount] = Linker::makeHeadline( $level,
  3916. $matches['attrib'][$headlineCount], $anchor, $headline,
  3917. $editlink, $legacyAnchor );
  3918. $headlineCount++;
  3919. }
  3920. $this->setOutputType( $oldType );
  3921. # Never ever show TOC if no headers
  3922. if ( $numVisible < 1 ) {
  3923. $enoughToc = false;
  3924. }
  3925. if ( $enoughToc ) {
  3926. if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
  3927. $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
  3928. }
  3929. $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
  3930. $this->mOutput->setTOCHTML( $toc );
  3931. }
  3932. if ( $isMain ) {
  3933. $this->mOutput->setSections( $tocraw );
  3934. }
  3935. # split up and insert constructed headlines
  3936. $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
  3937. $i = 0;
  3938. // build an array of document sections
  3939. $sections = array();
  3940. foreach ( $blocks as $block ) {
  3941. // $head is zero-based, sections aren't.
  3942. if ( empty( $head[$i - 1] ) ) {
  3943. $sections[$i] = $block;
  3944. } else {
  3945. $sections[$i] = $head[$i - 1] . $block;
  3946. }
  3947. /**
  3948. * Send a hook, one per section.
  3949. * The idea here is to be able to make section-level DIVs, but to do so in a
  3950. * lower-impact, more correct way than r50769
  3951. *
  3952. * $this : caller
  3953. * $section : the section number
  3954. * &$sectionContent : ref to the content of the section
  3955. * $showEditLinks : boolean describing whether this section has an edit link
  3956. */
  3957. wfRunHooks( 'ParserSectionCreate', array( $this, $i, &$sections[$i], $showEditLink ) );
  3958. $i++;
  3959. }
  3960. if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
  3961. // append the TOC at the beginning
  3962. // Top anchor now in skin
  3963. $sections[0] = $sections[0] . $toc . "\n";
  3964. }
  3965. $full .= join( '', $sections );
  3966. if ( $this->mForceTocPosition ) {
  3967. return str_replace( '<!--MWTOC-->', $toc, $full );
  3968. } else {
  3969. return $full;
  3970. }
  3971. }
  3972. /**
  3973. * Transform wiki markup when saving a page by doing "\r\n" -> "\n"
  3974. * conversion, substitting signatures, {{subst:}} templates, etc.
  3975. *
  3976. * @param $text String: the text to transform
  3977. * @param $title Title: the Title object for the current article
  3978. * @param $user User: the User object describing the current user
  3979. * @param $options ParserOptions: parsing options
  3980. * @param $clearState Boolean: whether to clear the parser state first
  3981. * @return String: the altered wiki markup
  3982. */
  3983. public function preSaveTransform( $text, Title $title, User $user, ParserOptions $options, $clearState = true ) {
  3984. $this->startParse( $title, $options, self::OT_WIKI, $clearState );
  3985. $this->setUser( $user );
  3986. $pairs = array(
  3987. "\r\n" => "\n",
  3988. );
  3989. $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
  3990. if( $options->getPreSaveTransform() ) {
  3991. $text = $this->pstPass2( $text, $user );
  3992. }
  3993. $text = $this->mStripState->unstripBoth( $text );
  3994. $this->setUser( null ); #Reset
  3995. return $text;
  3996. }
  3997. /**
  3998. * Pre-save transform helper function
  3999. * @private
  4000. *
  4001. * @param $text string
  4002. * @param $user User
  4003. *
  4004. * @return string
  4005. */
  4006. function pstPass2( $text, $user ) {
  4007. global $wgContLang, $wgLocaltimezone;
  4008. # Note: This is the timestamp saved as hardcoded wikitext to
  4009. # the database, we use $wgContLang here in order to give
  4010. # everyone the same signature and use the default one rather
  4011. # than the one selected in each user's preferences.
  4012. # (see also bug 12815)
  4013. $ts = $this->mOptions->getTimestamp();
  4014. if ( isset( $wgLocaltimezone ) ) {
  4015. $tz = $wgLocaltimezone;
  4016. } else {
  4017. $tz = date_default_timezone_get();
  4018. }
  4019. $unixts = wfTimestamp( TS_UNIX, $ts );
  4020. $oldtz = date_default_timezone_get();
  4021. date_default_timezone_set( $tz );
  4022. $ts = date( 'YmdHis', $unixts );
  4023. $tzMsg = date( 'T', $unixts ); # might vary on DST changeover!
  4024. # Allow translation of timezones through wiki. date() can return
  4025. # whatever crap the system uses, localised or not, so we cannot
  4026. # ship premade translations.
  4027. $key = 'timezone-' . strtolower( trim( $tzMsg ) );
  4028. $msg = wfMessage( $key )->inContentLanguage();
  4029. if ( $msg->exists() ) {
  4030. $tzMsg = $msg->text();
  4031. }
  4032. date_default_timezone_set( $oldtz );
  4033. $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
  4034. # Variable replacement
  4035. # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
  4036. $text = $this->replaceVariables( $text );
  4037. # This works almost by chance, as the replaceVariables are done before the getUserSig(),
  4038. # which may corrupt this parser instance via its wfMessage()->text() call-
  4039. # Signatures
  4040. $sigText = $this->getUserSig( $user );
  4041. $text = strtr( $text, array(
  4042. '~~~~~' => $d,
  4043. '~~~~' => "$sigText $d",
  4044. '~~~' => $sigText
  4045. ) );
  4046. # Context links: [[|name]] and [[name (context)|]]
  4047. $tc = '[' . Title::legalChars() . ']';
  4048. $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
  4049. $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; # [[ns:page (context)|]]
  4050. $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; # [[ns:page(context)|]]
  4051. $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:page (context), context|]]
  4052. $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]]
  4053. # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
  4054. $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
  4055. $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
  4056. $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
  4057. $t = $this->mTitle->getText();
  4058. $m = array();
  4059. if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
  4060. $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
  4061. } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
  4062. $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
  4063. } else {
  4064. # if there's no context, don't bother duplicating the title
  4065. $text = preg_replace( $p2, '[[\\1]]', $text );
  4066. }
  4067. # Trim trailing whitespace
  4068. $text = rtrim( $text );
  4069. return $text;
  4070. }
  4071. /**
  4072. * Fetch the user's signature text, if any, and normalize to
  4073. * validated, ready-to-insert wikitext.
  4074. * If you have pre-fetched the nickname or the fancySig option, you can
  4075. * specify them here to save a database query.
  4076. * Do not reuse this parser instance after calling getUserSig(),
  4077. * as it may have changed if it's the $wgParser.
  4078. *
  4079. * @param $user User
  4080. * @param $nickname String|bool nickname to use or false to use user's default nickname
  4081. * @param $fancySig Boolean|null whether the nicknname is the complete signature
  4082. * or null to use default value
  4083. * @return string
  4084. */
  4085. function getUserSig( &$user, $nickname = false, $fancySig = null ) {
  4086. global $wgMaxSigChars;
  4087. $username = $user->getName();
  4088. # If not given, retrieve from the user object.
  4089. if ( $nickname === false )
  4090. $nickname = $user->getOption( 'nickname' );
  4091. if ( is_null( $fancySig ) ) {
  4092. $fancySig = $user->getBoolOption( 'fancysig' );
  4093. }
  4094. $nickname = $nickname == null ? $username : $nickname;
  4095. if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
  4096. $nickname = $username;
  4097. wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
  4098. } elseif ( $fancySig !== false ) {
  4099. # Sig. might contain markup; validate this
  4100. if ( $this->validateSig( $nickname ) !== false ) {
  4101. # Validated; clean up (if needed) and return it
  4102. return $this->cleanSig( $nickname, true );
  4103. } else {
  4104. # Failed to validate; fall back to the default
  4105. $nickname = $username;
  4106. wfDebug( __METHOD__.": $username has bad XML tags in signature.\n" );
  4107. }
  4108. }
  4109. # Make sure nickname doesnt get a sig in a sig
  4110. $nickname = self::cleanSigInSig( $nickname );
  4111. # If we're still here, make it a link to the user page
  4112. $userText = wfEscapeWikiText( $username );
  4113. $nickText = wfEscapeWikiText( $nickname );
  4114. $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
  4115. return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()->title( $this->getTitle() )->text();
  4116. }
  4117. /**
  4118. * Check that the user's signature contains no bad XML
  4119. *
  4120. * @param $text String
  4121. * @return mixed An expanded string, or false if invalid.
  4122. */
  4123. function validateSig( $text ) {
  4124. return( Xml::isWellFormedXmlFragment( $text ) ? $text : false );
  4125. }
  4126. /**
  4127. * Clean up signature text
  4128. *
  4129. * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
  4130. * 2) Substitute all transclusions
  4131. *
  4132. * @param $text String
  4133. * @param $parsing bool Whether we're cleaning (preferences save) or parsing
  4134. * @return String: signature text
  4135. */
  4136. public function cleanSig( $text, $parsing = false ) {
  4137. if ( !$parsing ) {
  4138. global $wgTitle;
  4139. $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
  4140. }
  4141. # Option to disable this feature
  4142. if ( !$this->mOptions->getCleanSignatures() ) {
  4143. return $text;
  4144. }
  4145. # @todo FIXME: Regex doesn't respect extension tags or nowiki
  4146. # => Move this logic to braceSubstitution()
  4147. $substWord = MagicWord::get( 'subst' );
  4148. $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
  4149. $substText = '{{' . $substWord->getSynonym( 0 );
  4150. $text = preg_replace( $substRegex, $substText, $text );
  4151. $text = self::cleanSigInSig( $text );
  4152. $dom = $this->preprocessToDom( $text );
  4153. $frame = $this->getPreprocessor()->newFrame();
  4154. $text = $frame->expand( $dom );
  4155. if ( !$parsing ) {
  4156. $text = $this->mStripState->unstripBoth( $text );
  4157. }
  4158. return $text;
  4159. }
  4160. /**
  4161. * Strip ~~~, ~~~~ and ~~~~~ out of signatures
  4162. *
  4163. * @param $text String
  4164. * @return String: signature text with /~{3,5}/ removed
  4165. */
  4166. public static function cleanSigInSig( $text ) {
  4167. $text = preg_replace( '/~{3,5}/', '', $text );
  4168. return $text;
  4169. }
  4170. /**
  4171. * Set up some variables which are usually set up in parse()
  4172. * so that an external function can call some class members with confidence
  4173. *
  4174. * @param $title Title|null
  4175. * @param $options ParserOptions
  4176. * @param $outputType
  4177. * @param $clearState bool
  4178. */
  4179. public function startExternalParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) {
  4180. $this->startParse( $title, $options, $outputType, $clearState );
  4181. }
  4182. /**
  4183. * @param $title Title|null
  4184. * @param $options ParserOptions
  4185. * @param $outputType
  4186. * @param $clearState bool
  4187. */
  4188. private function startParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) {
  4189. $this->setTitle( $title );
  4190. $this->mOptions = $options;
  4191. $this->setOutputType( $outputType );
  4192. if ( $clearState ) {
  4193. $this->clearState();
  4194. }
  4195. }
  4196. /**
  4197. * Wrapper for preprocess()
  4198. *
  4199. * @param $text String: the text to preprocess
  4200. * @param $options ParserOptions: options
  4201. * @param $title Title object or null to use $wgTitle
  4202. * @return String
  4203. */
  4204. public function transformMsg( $text, $options, $title = null ) {
  4205. static $executing = false;
  4206. # Guard against infinite recursion
  4207. if ( $executing ) {
  4208. return $text;
  4209. }
  4210. $executing = true;
  4211. wfProfileIn( __METHOD__ );
  4212. if ( !$title ) {
  4213. global $wgTitle;
  4214. $title = $wgTitle;
  4215. }
  4216. if ( !$title ) {
  4217. # It's not uncommon having a null $wgTitle in scripts. See r80898
  4218. # Create a ghost title in such case
  4219. $title = Title::newFromText( 'Dwimmerlaik' );
  4220. }
  4221. $text = $this->preprocess( $text, $title, $options );
  4222. $executing = false;
  4223. wfProfileOut( __METHOD__ );
  4224. return $text;
  4225. }
  4226. /**
  4227. * Create an HTML-style tag, e.g. "<yourtag>special text</yourtag>"
  4228. * The callback should have the following form:
  4229. * function myParserHook( $text, $params, $parser, $frame ) { ... }
  4230. *
  4231. * Transform and return $text. Use $parser for any required context, e.g. use
  4232. * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
  4233. *
  4234. * Hooks may return extended information by returning an array, of which the
  4235. * first numbered element (index 0) must be the return string, and all other
  4236. * entries are extracted into local variables within an internal function
  4237. * in the Parser class.
  4238. *
  4239. * This interface (introduced r61913) appears to be undocumented, but
  4240. * 'markerName' is used by some core tag hooks to override which strip
  4241. * array their results are placed in. **Use great caution if attempting
  4242. * this interface, as it is not documented and injudicious use could smash
  4243. * private variables.**
  4244. *
  4245. * @param $tag Mixed: the tag to use, e.g. 'hook' for "<hook>"
  4246. * @param $callback Mixed: the callback function (and object) to use for the tag
  4247. * @return Mixed|null The old value of the mTagHooks array associated with the hook
  4248. */
  4249. public function setHook( $tag, $callback ) {
  4250. $tag = strtolower( $tag );
  4251. if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
  4252. throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
  4253. }
  4254. $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
  4255. $this->mTagHooks[$tag] = $callback;
  4256. if ( !in_array( $tag, $this->mStripList ) ) {
  4257. $this->mStripList[] = $tag;
  4258. }
  4259. return $oldVal;
  4260. }
  4261. /**
  4262. * As setHook(), but letting the contents be parsed.
  4263. *
  4264. * Transparent tag hooks are like regular XML-style tag hooks, except they
  4265. * operate late in the transformation sequence, on HTML instead of wikitext.
  4266. *
  4267. * This is probably obsoleted by things dealing with parser frames?
  4268. * The only extension currently using it is geoserver.
  4269. *
  4270. * @since 1.10
  4271. * @todo better document or deprecate this
  4272. *
  4273. * @param $tag Mixed: the tag to use, e.g. 'hook' for "<hook>"
  4274. * @param $callback Mixed: the callback function (and object) to use for the tag
  4275. * @return Mixed|null The old value of the mTagHooks array associated with the hook
  4276. */
  4277. function setTransparentTagHook( $tag, $callback ) {
  4278. $tag = strtolower( $tag );
  4279. if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
  4280. throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
  4281. }
  4282. $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
  4283. $this->mTransparentTagHooks[$tag] = $callback;
  4284. return $oldVal;
  4285. }
  4286. /**
  4287. * Remove all tag hooks
  4288. */
  4289. function clearTagHooks() {
  4290. $this->mTagHooks = array();
  4291. $this->mFunctionTagHooks = array();
  4292. $this->mStripList = $this->mDefaultStripList;
  4293. }
  4294. /**
  4295. * Create a function, e.g. {{sum:1|2|3}}
  4296. * The callback function should have the form:
  4297. * function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
  4298. *
  4299. * Or with SFH_OBJECT_ARGS:
  4300. * function myParserFunction( $parser, $frame, $args ) { ... }
  4301. *
  4302. * The callback may either return the text result of the function, or an array with the text
  4303. * in element 0, and a number of flags in the other elements. The names of the flags are
  4304. * specified in the keys. Valid flags are:
  4305. * found The text returned is valid, stop processing the template. This
  4306. * is on by default.
  4307. * nowiki Wiki markup in the return value should be escaped
  4308. * isHTML The returned text is HTML, armour it against wikitext transformation
  4309. *
  4310. * @param $id String: The magic word ID
  4311. * @param $callback Mixed: the callback function (and object) to use
  4312. * @param $flags Integer: a combination of the following flags:
  4313. * SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
  4314. *
  4315. * SFH_OBJECT_ARGS Pass the template arguments as PPNode objects instead of text. This
  4316. * allows for conditional expansion of the parse tree, allowing you to eliminate dead
  4317. * branches and thus speed up parsing. It is also possible to analyse the parse tree of
  4318. * the arguments, and to control the way they are expanded.
  4319. *
  4320. * The $frame parameter is a PPFrame. This can be used to produce expanded text from the
  4321. * arguments, for instance:
  4322. * $text = isset( $args[0] ) ? $frame->expand( $args[0] ) : '';
  4323. *
  4324. * For technical reasons, $args[0] is pre-expanded and will be a string. This may change in
  4325. * future versions. Please call $frame->expand() on it anyway so that your code keeps
  4326. * working if/when this is changed.
  4327. *
  4328. * If you want whitespace to be trimmed from $args, you need to do it yourself, post-
  4329. * expansion.
  4330. *
  4331. * Please read the documentation in includes/parser/Preprocessor.php for more information
  4332. * about the methods available in PPFrame and PPNode.
  4333. *
  4334. * @return string|callback The old callback function for this name, if any
  4335. */
  4336. public function setFunctionHook( $id, $callback, $flags = 0 ) {
  4337. global $wgContLang;
  4338. $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
  4339. $this->mFunctionHooks[$id] = array( $callback, $flags );
  4340. # Add to function cache
  4341. $mw = MagicWord::get( $id );
  4342. if ( !$mw )
  4343. throw new MWException( __METHOD__.'() expecting a magic word identifier.' );
  4344. $synonyms = $mw->getSynonyms();
  4345. $sensitive = intval( $mw->isCaseSensitive() );
  4346. foreach ( $synonyms as $syn ) {
  4347. # Case
  4348. if ( !$sensitive ) {
  4349. $syn = $wgContLang->lc( $syn );
  4350. }
  4351. # Add leading hash
  4352. if ( !( $flags & SFH_NO_HASH ) ) {
  4353. $syn = '#' . $syn;
  4354. }
  4355. # Remove trailing colon
  4356. if ( substr( $syn, -1, 1 ) === ':' ) {
  4357. $syn = substr( $syn, 0, -1 );
  4358. }
  4359. $this->mFunctionSynonyms[$sensitive][$syn] = $id;
  4360. }
  4361. return $oldVal;
  4362. }
  4363. /**
  4364. * Get all registered function hook identifiers
  4365. *
  4366. * @return Array
  4367. */
  4368. function getFunctionHooks() {
  4369. return array_keys( $this->mFunctionHooks );
  4370. }
  4371. /**
  4372. * Create a tag function, e.g. "<test>some stuff</test>".
  4373. * Unlike tag hooks, tag functions are parsed at preprocessor level.
  4374. * Unlike parser functions, their content is not preprocessed.
  4375. * @return null
  4376. */
  4377. function setFunctionTagHook( $tag, $callback, $flags ) {
  4378. $tag = strtolower( $tag );
  4379. if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
  4380. $old = isset( $this->mFunctionTagHooks[$tag] ) ?
  4381. $this->mFunctionTagHooks[$tag] : null;
  4382. $this->mFunctionTagHooks[$tag] = array( $callback, $flags );
  4383. if ( !in_array( $tag, $this->mStripList ) ) {
  4384. $this->mStripList[] = $tag;
  4385. }
  4386. return $old;
  4387. }
  4388. /**
  4389. * @todo FIXME: Update documentation. makeLinkObj() is deprecated.
  4390. * Replace "<!--LINK-->" link placeholders with actual links, in the buffer
  4391. * Placeholders created in Skin::makeLinkObj()
  4392. *
  4393. * @param $text string
  4394. * @param $options int
  4395. *
  4396. * @return array of link CSS classes, indexed by PDBK.
  4397. */
  4398. function replaceLinkHolders( &$text, $options = 0 ) {
  4399. return $this->mLinkHolders->replace( $text );
  4400. }
  4401. /**
  4402. * Replace "<!--LINK-->" link placeholders with plain text of links
  4403. * (not HTML-formatted).
  4404. *
  4405. * @param $text String
  4406. * @return String
  4407. */
  4408. function replaceLinkHoldersText( $text ) {
  4409. return $this->mLinkHolders->replaceText( $text );
  4410. }
  4411. /**
  4412. * Renders an image gallery from a text with one line per image.
  4413. * text labels may be given by using |-style alternative text. E.g.
  4414. * Image:one.jpg|The number "1"
  4415. * Image:tree.jpg|A tree
  4416. * given as text will return the HTML of a gallery with two images,
  4417. * labeled 'The number "1"' and
  4418. * 'A tree'.
  4419. *
  4420. * @param string $text
  4421. * @param array $params
  4422. * @return string HTML
  4423. */
  4424. function renderImageGallery( $text, $params ) {
  4425. $ig = new ImageGallery();
  4426. $ig->setContextTitle( $this->mTitle );
  4427. $ig->setShowBytes( false );
  4428. $ig->setShowFilename( false );
  4429. $ig->setParser( $this );
  4430. $ig->setHideBadImages();
  4431. $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
  4432. if ( isset( $params['showfilename'] ) ) {
  4433. $ig->setShowFilename( true );
  4434. } else {
  4435. $ig->setShowFilename( false );
  4436. }
  4437. if ( isset( $params['caption'] ) ) {
  4438. $caption = $params['caption'];
  4439. $caption = htmlspecialchars( $caption );
  4440. $caption = $this->replaceInternalLinks( $caption );
  4441. $ig->setCaptionHtml( $caption );
  4442. }
  4443. if ( isset( $params['perrow'] ) ) {
  4444. $ig->setPerRow( $params['perrow'] );
  4445. }
  4446. if ( isset( $params['widths'] ) ) {
  4447. $ig->setWidths( $params['widths'] );
  4448. }
  4449. if ( isset( $params['heights'] ) ) {
  4450. $ig->setHeights( $params['heights'] );
  4451. }
  4452. wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
  4453. $lines = StringUtils::explode( "\n", $text );
  4454. foreach ( $lines as $line ) {
  4455. # match lines like these:
  4456. # Image:someimage.jpg|This is some image
  4457. $matches = array();
  4458. preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
  4459. # Skip empty lines
  4460. if ( count( $matches ) == 0 ) {
  4461. continue;
  4462. }
  4463. if ( strpos( $matches[0], '%' ) !== false ) {
  4464. $matches[1] = rawurldecode( $matches[1] );
  4465. }
  4466. $title = Title::newFromText( $matches[1], NS_FILE );
  4467. if ( is_null( $title ) ) {
  4468. # Bogus title. Ignore these so we don't bomb out later.
  4469. continue;
  4470. }
  4471. $label = '';
  4472. $alt = '';
  4473. $link = '';
  4474. if ( isset( $matches[3] ) ) {
  4475. // look for an |alt= definition while trying not to break existing
  4476. // captions with multiple pipes (|) in it, until a more sensible grammar
  4477. // is defined for images in galleries
  4478. $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
  4479. $parameterMatches = StringUtils::explode('|', $matches[3]);
  4480. $magicWordAlt = MagicWord::get( 'img_alt' );
  4481. $magicWordLink = MagicWord::get( 'img_link' );
  4482. foreach ( $parameterMatches as $parameterMatch ) {
  4483. if ( $match = $magicWordAlt->matchVariableStartToEnd( $parameterMatch ) ) {
  4484. $alt = $this->stripAltText( $match, false );
  4485. }
  4486. elseif( $match = $magicWordLink->matchVariableStartToEnd( $parameterMatch ) ){
  4487. $link = strip_tags($this->replaceLinkHoldersText($match));
  4488. $chars = self::EXT_LINK_URL_CLASS;
  4489. $prots = $this->mUrlProtocols;
  4490. //check to see if link matches an absolute url, if not then it must be a wiki link.
  4491. if(!preg_match( "/^($prots)$chars+$/u", $link)){
  4492. $localLinkTitle = Title::newFromText($link);
  4493. $link = $localLinkTitle->getLocalURL();
  4494. }
  4495. }
  4496. else {
  4497. // concatenate all other pipes
  4498. $label .= '|' . $parameterMatch;
  4499. }
  4500. }
  4501. // remove the first pipe
  4502. $label = substr( $label, 1 );
  4503. }
  4504. $ig->add( $title, $label, $alt ,$link);
  4505. }
  4506. return $ig->toHTML();
  4507. }
  4508. /**
  4509. * @param $handler
  4510. * @return array
  4511. */
  4512. function getImageParams( $handler ) {
  4513. if ( $handler ) {
  4514. $handlerClass = get_class( $handler );
  4515. } else {
  4516. $handlerClass = '';
  4517. }
  4518. if ( !isset( $this->mImageParams[$handlerClass] ) ) {
  4519. # Initialise static lists
  4520. static $internalParamNames = array(
  4521. 'horizAlign' => array( 'left', 'right', 'center', 'none' ),
  4522. 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
  4523. 'bottom', 'text-bottom' ),
  4524. 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
  4525. 'upright', 'border', 'link', 'alt', 'class' ),
  4526. );
  4527. static $internalParamMap;
  4528. if ( !$internalParamMap ) {
  4529. $internalParamMap = array();
  4530. foreach ( $internalParamNames as $type => $names ) {
  4531. foreach ( $names as $name ) {
  4532. $magicName = str_replace( '-', '_', "img_$name" );
  4533. $internalParamMap[$magicName] = array( $type, $name );
  4534. }
  4535. }
  4536. }
  4537. # Add handler params
  4538. $paramMap = $internalParamMap;
  4539. if ( $handler ) {
  4540. $handlerParamMap = $handler->getParamMap();
  4541. foreach ( $handlerParamMap as $magic => $paramName ) {
  4542. $paramMap[$magic] = array( 'handler', $paramName );
  4543. }
  4544. }
  4545. $this->mImageParams[$handlerClass] = $paramMap;
  4546. $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
  4547. }
  4548. return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
  4549. }
  4550. /**
  4551. * Parse image options text and use it to make an image
  4552. *
  4553. * @param $title Title
  4554. * @param $options String
  4555. * @param $holders LinkHolderArray|bool
  4556. * @return string HTML
  4557. */
  4558. function makeImage( $title, $options, $holders = false ) {
  4559. # Check if the options text is of the form "options|alt text"
  4560. # Options are:
  4561. # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
  4562. # * left no resizing, just left align. label is used for alt= only
  4563. # * right same, but right aligned
  4564. # * none same, but not aligned
  4565. # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
  4566. # * center center the image
  4567. # * frame Keep original image size, no magnify-button.
  4568. # * framed Same as "frame"
  4569. # * frameless like 'thumb' but without a frame. Keeps user preferences for width
  4570. # * upright reduce width for upright images, rounded to full __0 px
  4571. # * border draw a 1px border around the image
  4572. # * alt Text for HTML alt attribute (defaults to empty)
  4573. # * class Set a class for img node
  4574. # * link Set the target of the image link. Can be external, interwiki, or local
  4575. # vertical-align values (no % or length right now):
  4576. # * baseline
  4577. # * sub
  4578. # * super
  4579. # * top
  4580. # * text-top
  4581. # * middle
  4582. # * bottom
  4583. # * text-bottom
  4584. $parts = StringUtils::explode( "|", $options );
  4585. # Give extensions a chance to select the file revision for us
  4586. $options = array();
  4587. $descQuery = false;
  4588. wfRunHooks( 'BeforeParserFetchFileAndTitle',
  4589. array( $this, $title, &$options, &$descQuery ) );
  4590. # Fetch and register the file (file title may be different via hooks)
  4591. list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
  4592. # Get parameter map
  4593. $handler = $file ? $file->getHandler() : false;
  4594. list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
  4595. if ( !$file ) {
  4596. $this->addTrackingCategory( 'broken-file-category' );
  4597. }
  4598. # Process the input parameters
  4599. $caption = '';
  4600. $params = array( 'frame' => array(), 'handler' => array(),
  4601. 'horizAlign' => array(), 'vertAlign' => array() );
  4602. foreach ( $parts as $part ) {
  4603. $part = trim( $part );
  4604. list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
  4605. $validated = false;
  4606. if ( isset( $paramMap[$magicName] ) ) {
  4607. list( $type, $paramName ) = $paramMap[$magicName];
  4608. # Special case; width and height come in one variable together
  4609. if ( $type === 'handler' && $paramName === 'width' ) {
  4610. $parsedWidthParam = $this->parseWidthParam( $value );
  4611. if( isset( $parsedWidthParam['width'] ) ) {
  4612. $width = $parsedWidthParam['width'];
  4613. if ( $handler->validateParam( 'width', $width ) ) {
  4614. $params[$type]['width'] = $width;
  4615. $validated = true;
  4616. }
  4617. }
  4618. if( isset( $parsedWidthParam['height'] ) ) {
  4619. $height = $parsedWidthParam['height'];
  4620. if ( $handler->validateParam( 'height', $height ) ) {
  4621. $params[$type]['height'] = $height;
  4622. $validated = true;
  4623. }
  4624. }
  4625. # else no validation -- bug 13436
  4626. } else {
  4627. if ( $type === 'handler' ) {
  4628. # Validate handler parameter
  4629. $validated = $handler->validateParam( $paramName, $value );
  4630. } else {
  4631. # Validate internal parameters
  4632. switch( $paramName ) {
  4633. case 'manualthumb':
  4634. case 'alt':
  4635. case 'class':
  4636. # @todo FIXME: Possibly check validity here for
  4637. # manualthumb? downstream behavior seems odd with
  4638. # missing manual thumbs.
  4639. $validated = true;
  4640. $value = $this->stripAltText( $value, $holders );
  4641. break;
  4642. case 'link':
  4643. $chars = self::EXT_LINK_URL_CLASS;
  4644. $prots = $this->mUrlProtocols;
  4645. if ( $value === '' ) {
  4646. $paramName = 'no-link';
  4647. $value = true;
  4648. $validated = true;
  4649. } elseif ( preg_match( "/^(?i)$prots/", $value ) ) {
  4650. if ( preg_match( "/^((?i)$prots)$chars+$/u", $value, $m ) ) {
  4651. $paramName = 'link-url';
  4652. $this->mOutput->addExternalLink( $value );
  4653. if ( $this->mOptions->getExternalLinkTarget() ) {
  4654. $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
  4655. }
  4656. $validated = true;
  4657. }
  4658. } else {
  4659. $linkTitle = Title::newFromText( $value );
  4660. if ( $linkTitle ) {
  4661. $paramName = 'link-title';
  4662. $value = $linkTitle;
  4663. $this->mOutput->addLink( $linkTitle );
  4664. $validated = true;
  4665. }
  4666. }
  4667. break;
  4668. default:
  4669. # Most other things appear to be empty or numeric...
  4670. $validated = ( $value === false || is_numeric( trim( $value ) ) );
  4671. }
  4672. }
  4673. if ( $validated ) {
  4674. $params[$type][$paramName] = $value;
  4675. }
  4676. }
  4677. }
  4678. if ( !$validated ) {
  4679. $caption = $part;
  4680. }
  4681. }
  4682. # Process alignment parameters
  4683. if ( $params['horizAlign'] ) {
  4684. $params['frame']['align'] = key( $params['horizAlign'] );
  4685. }
  4686. if ( $params['vertAlign'] ) {
  4687. $params['frame']['valign'] = key( $params['vertAlign'] );
  4688. }
  4689. $params['frame']['caption'] = $caption;
  4690. # Will the image be presented in a frame, with the caption below?
  4691. $imageIsFramed = isset( $params['frame']['frame'] ) ||
  4692. isset( $params['frame']['framed'] ) ||
  4693. isset( $params['frame']['thumbnail'] ) ||
  4694. isset( $params['frame']['manualthumb'] );
  4695. # In the old days, [[Image:Foo|text...]] would set alt text. Later it
  4696. # came to also set the caption, ordinary text after the image -- which
  4697. # makes no sense, because that just repeats the text multiple times in
  4698. # screen readers. It *also* came to set the title attribute.
  4699. #
  4700. # Now that we have an alt attribute, we should not set the alt text to
  4701. # equal the caption: that's worse than useless, it just repeats the
  4702. # text. This is the framed/thumbnail case. If there's no caption, we
  4703. # use the unnamed parameter for alt text as well, just for the time be-
  4704. # ing, if the unnamed param is set and the alt param is not.
  4705. #
  4706. # For the future, we need to figure out if we want to tweak this more,
  4707. # e.g., introducing a title= parameter for the title; ignoring the un-
  4708. # named parameter entirely for images without a caption; adding an ex-
  4709. # plicit caption= parameter and preserving the old magic unnamed para-
  4710. # meter for BC; ...
  4711. if ( $imageIsFramed ) { # Framed image
  4712. if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
  4713. # No caption or alt text, add the filename as the alt text so
  4714. # that screen readers at least get some description of the image
  4715. $params['frame']['alt'] = $title->getText();
  4716. }
  4717. # Do not set $params['frame']['title'] because tooltips don't make sense
  4718. # for framed images
  4719. } else { # Inline image
  4720. if ( !isset( $params['frame']['alt'] ) ) {
  4721. # No alt text, use the "caption" for the alt text
  4722. if ( $caption !== '') {
  4723. $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
  4724. } else {
  4725. # No caption, fall back to using the filename for the
  4726. # alt text
  4727. $params['frame']['alt'] = $title->getText();
  4728. }
  4729. }
  4730. # Use the "caption" for the tooltip text
  4731. $params['frame']['title'] = $this->stripAltText( $caption, $holders );
  4732. }
  4733. wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params, $this ) );
  4734. # Linker does the rest
  4735. $time = isset( $options['time'] ) ? $options['time'] : false;
  4736. $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
  4737. $time, $descQuery, $this->mOptions->getThumbSize() );
  4738. # Give the handler a chance to modify the parser object
  4739. if ( $handler ) {
  4740. $handler->parserTransformHook( $this, $file );
  4741. }
  4742. return $ret;
  4743. }
  4744. /**
  4745. * @param $caption
  4746. * @param $holders LinkHolderArray
  4747. * @return mixed|String
  4748. */
  4749. protected function stripAltText( $caption, $holders ) {
  4750. # Strip bad stuff out of the title (tooltip). We can't just use
  4751. # replaceLinkHoldersText() here, because if this function is called
  4752. # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
  4753. if ( $holders ) {
  4754. $tooltip = $holders->replaceText( $caption );
  4755. } else {
  4756. $tooltip = $this->replaceLinkHoldersText( $caption );
  4757. }
  4758. # make sure there are no placeholders in thumbnail attributes
  4759. # that are later expanded to html- so expand them now and
  4760. # remove the tags
  4761. $tooltip = $this->mStripState->unstripBoth( $tooltip );
  4762. $tooltip = Sanitizer::stripAllTags( $tooltip );
  4763. return $tooltip;
  4764. }
  4765. /**
  4766. * Set a flag in the output object indicating that the content is dynamic and
  4767. * shouldn't be cached.
  4768. */
  4769. function disableCache() {
  4770. wfDebug( "Parser output marked as uncacheable.\n" );
  4771. if ( !$this->mOutput ) {
  4772. throw new MWException( __METHOD__ .
  4773. " can only be called when actually parsing something" );
  4774. }
  4775. $this->mOutput->setCacheTime( -1 ); // old style, for compatibility
  4776. $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
  4777. }
  4778. /**
  4779. * Callback from the Sanitizer for expanding items found in HTML attribute
  4780. * values, so they can be safely tested and escaped.
  4781. *
  4782. * @param $text String
  4783. * @param $frame PPFrame
  4784. * @return String
  4785. */
  4786. function attributeStripCallback( &$text, $frame = false ) {
  4787. $text = $this->replaceVariables( $text, $frame );
  4788. $text = $this->mStripState->unstripBoth( $text );
  4789. return $text;
  4790. }
  4791. /**
  4792. * Accessor
  4793. *
  4794. * @return array
  4795. */
  4796. function getTags() {
  4797. return array_merge( array_keys( $this->mTransparentTagHooks ), array_keys( $this->mTagHooks ), array_keys( $this->mFunctionTagHooks ) );
  4798. }
  4799. /**
  4800. * Replace transparent tags in $text with the values given by the callbacks.
  4801. *
  4802. * Transparent tag hooks are like regular XML-style tag hooks, except they
  4803. * operate late in the transformation sequence, on HTML instead of wikitext.
  4804. *
  4805. * @param $text string
  4806. *
  4807. * @return string
  4808. */
  4809. function replaceTransparentTags( $text ) {
  4810. $matches = array();
  4811. $elements = array_keys( $this->mTransparentTagHooks );
  4812. $text = self::extractTagsAndParams( $elements, $text, $matches, $this->mUniqPrefix );
  4813. $replacements = array();
  4814. foreach ( $matches as $marker => $data ) {
  4815. list( $element, $content, $params, $tag ) = $data;
  4816. $tagName = strtolower( $element );
  4817. if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
  4818. $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], array( $content, $params, $this ) );
  4819. } else {
  4820. $output = $tag;
  4821. }
  4822. $replacements[$marker] = $output;
  4823. }
  4824. return strtr( $text, $replacements );
  4825. }
  4826. /**
  4827. * Break wikitext input into sections, and either pull or replace
  4828. * some particular section's text.
  4829. *
  4830. * External callers should use the getSection and replaceSection methods.
  4831. *
  4832. * @param $text String: Page wikitext
  4833. * @param $section String: a section identifier string of the form:
  4834. * "<flag1> - <flag2> - ... - <section number>"
  4835. *
  4836. * Currently the only recognised flag is "T", which means the target section number
  4837. * was derived during a template inclusion parse, in other words this is a template
  4838. * section edit link. If no flags are given, it was an ordinary section edit link.
  4839. * This flag is required to avoid a section numbering mismatch when a section is
  4840. * enclosed by "<includeonly>" (bug 6563).
  4841. *
  4842. * The section number 0 pulls the text before the first heading; other numbers will
  4843. * pull the given section along with its lower-level subsections. If the section is
  4844. * not found, $mode=get will return $newtext, and $mode=replace will return $text.
  4845. *
  4846. * Section 0 is always considered to exist, even if it only contains the empty
  4847. * string. If $text is the empty string and section 0 is replaced, $newText is
  4848. * returned.
  4849. *
  4850. * @param $mode String: one of "get" or "replace"
  4851. * @param $newText String: replacement text for section data.
  4852. * @return String: for "get", the extracted section text.
  4853. * for "replace", the whole page with the section replaced.
  4854. */
  4855. private function extractSections( $text, $section, $mode, $newText='' ) {
  4856. global $wgTitle; # not generally used but removes an ugly failure mode
  4857. $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
  4858. $outText = '';
  4859. $frame = $this->getPreprocessor()->newFrame();
  4860. # Process section extraction flags
  4861. $flags = 0;
  4862. $sectionParts = explode( '-', $section );
  4863. $sectionIndex = array_pop( $sectionParts );
  4864. foreach ( $sectionParts as $part ) {
  4865. if ( $part === 'T' ) {
  4866. $flags |= self::PTD_FOR_INCLUSION;
  4867. }
  4868. }
  4869. # Check for empty input
  4870. if ( strval( $text ) === '' ) {
  4871. # Only sections 0 and T-0 exist in an empty document
  4872. if ( $sectionIndex == 0 ) {
  4873. if ( $mode === 'get' ) {
  4874. return '';
  4875. } else {
  4876. return $newText;
  4877. }
  4878. } else {
  4879. if ( $mode === 'get' ) {
  4880. return $newText;
  4881. } else {
  4882. return $text;
  4883. }
  4884. }
  4885. }
  4886. # Preprocess the text
  4887. $root = $this->preprocessToDom( $text, $flags );
  4888. # <h> nodes indicate section breaks
  4889. # They can only occur at the top level, so we can find them by iterating the root's children
  4890. $node = $root->getFirstChild();
  4891. # Find the target section
  4892. if ( $sectionIndex == 0 ) {
  4893. # Section zero doesn't nest, level=big
  4894. $targetLevel = 1000;
  4895. } else {
  4896. while ( $node ) {
  4897. if ( $node->getName() === 'h' ) {
  4898. $bits = $node->splitHeading();
  4899. if ( $bits['i'] == $sectionIndex ) {
  4900. $targetLevel = $bits['level'];
  4901. break;
  4902. }
  4903. }
  4904. if ( $mode === 'replace' ) {
  4905. $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
  4906. }
  4907. $node = $node->getNextSibling();
  4908. }
  4909. }
  4910. if ( !$node ) {
  4911. # Not found
  4912. if ( $mode === 'get' ) {
  4913. return $newText;
  4914. } else {
  4915. return $text;
  4916. }
  4917. }
  4918. # Find the end of the section, including nested sections
  4919. do {
  4920. if ( $node->getName() === 'h' ) {
  4921. $bits = $node->splitHeading();
  4922. $curLevel = $bits['level'];
  4923. if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
  4924. break;
  4925. }
  4926. }
  4927. if ( $mode === 'get' ) {
  4928. $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
  4929. }
  4930. $node = $node->getNextSibling();
  4931. } while ( $node );
  4932. # Write out the remainder (in replace mode only)
  4933. if ( $mode === 'replace' ) {
  4934. # Output the replacement text
  4935. # Add two newlines on -- trailing whitespace in $newText is conventionally
  4936. # stripped by the editor, so we need both newlines to restore the paragraph gap
  4937. # Only add trailing whitespace if there is newText
  4938. if ( $newText != "" ) {
  4939. $outText .= $newText . "\n\n";
  4940. }
  4941. while ( $node ) {
  4942. $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
  4943. $node = $node->getNextSibling();
  4944. }
  4945. }
  4946. if ( is_string( $outText ) ) {
  4947. # Re-insert stripped tags
  4948. $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
  4949. }
  4950. return $outText;
  4951. }
  4952. /**
  4953. * This function returns the text of a section, specified by a number ($section).
  4954. * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
  4955. * the first section before any such heading (section 0).
  4956. *
  4957. * If a section contains subsections, these are also returned.
  4958. *
  4959. * @param $text String: text to look in
  4960. * @param $section String: section identifier
  4961. * @param $deftext String: default to return if section is not found
  4962. * @return string text of the requested section
  4963. */
  4964. public function getSection( $text, $section, $deftext='' ) {
  4965. return $this->extractSections( $text, $section, "get", $deftext );
  4966. }
  4967. /**
  4968. * This function returns $oldtext after the content of the section
  4969. * specified by $section has been replaced with $text. If the target
  4970. * section does not exist, $oldtext is returned unchanged.
  4971. *
  4972. * @param $oldtext String: former text of the article
  4973. * @param $section int section identifier
  4974. * @param $text String: replacing text
  4975. * @return String: modified text
  4976. */
  4977. public function replaceSection( $oldtext, $section, $text ) {
  4978. return $this->extractSections( $oldtext, $section, "replace", $text );
  4979. }
  4980. /**
  4981. * Get the ID of the revision we are parsing
  4982. *
  4983. * @return Mixed: integer or null
  4984. */
  4985. function getRevisionId() {
  4986. return $this->mRevisionId;
  4987. }
  4988. /**
  4989. * Get the revision object for $this->mRevisionId
  4990. *
  4991. * @return Revision|null either a Revision object or null
  4992. */
  4993. protected function getRevisionObject() {
  4994. if ( !is_null( $this->mRevisionObject ) ) {
  4995. return $this->mRevisionObject;
  4996. }
  4997. if ( is_null( $this->mRevisionId ) ) {
  4998. return null;
  4999. }
  5000. $this->mRevisionObject = Revision::newFromId( $this->mRevisionId );
  5001. return $this->mRevisionObject;
  5002. }
  5003. /**
  5004. * Get the timestamp associated with the current revision, adjusted for
  5005. * the default server-local timestamp
  5006. */
  5007. function getRevisionTimestamp() {
  5008. if ( is_null( $this->mRevisionTimestamp ) ) {
  5009. wfProfileIn( __METHOD__ );
  5010. global $wgContLang;
  5011. $revObject = $this->getRevisionObject();
  5012. $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
  5013. # The cryptic '' timezone parameter tells to use the site-default
  5014. # timezone offset instead of the user settings.
  5015. #
  5016. # Since this value will be saved into the parser cache, served
  5017. # to other users, and potentially even used inside links and such,
  5018. # it needs to be consistent for all visitors.
  5019. $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
  5020. wfProfileOut( __METHOD__ );
  5021. }
  5022. return $this->mRevisionTimestamp;
  5023. }
  5024. /**
  5025. * Get the name of the user that edited the last revision
  5026. *
  5027. * @return String: user name
  5028. */
  5029. function getRevisionUser() {
  5030. if( is_null( $this->mRevisionUser ) ) {
  5031. $revObject = $this->getRevisionObject();
  5032. # if this template is subst: the revision id will be blank,
  5033. # so just use the current user's name
  5034. if( $revObject ) {
  5035. $this->mRevisionUser = $revObject->getUserText();
  5036. } elseif( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
  5037. $this->mRevisionUser = $this->getUser()->getName();
  5038. }
  5039. }
  5040. return $this->mRevisionUser;
  5041. }
  5042. /**
  5043. * Mutator for $mDefaultSort
  5044. *
  5045. * @param $sort string New value
  5046. */
  5047. public function setDefaultSort( $sort ) {
  5048. $this->mDefaultSort = $sort;
  5049. $this->mOutput->setProperty( 'defaultsort', $sort );
  5050. }
  5051. /**
  5052. * Accessor for $mDefaultSort
  5053. * Will use the empty string if none is set.
  5054. *
  5055. * This value is treated as a prefix, so the
  5056. * empty string is equivalent to sorting by
  5057. * page name.
  5058. *
  5059. * @return string
  5060. */
  5061. public function getDefaultSort() {
  5062. if ( $this->mDefaultSort !== false ) {
  5063. return $this->mDefaultSort;
  5064. } else {
  5065. return '';
  5066. }
  5067. }
  5068. /**
  5069. * Accessor for $mDefaultSort
  5070. * Unlike getDefaultSort(), will return false if none is set
  5071. *
  5072. * @return string or false
  5073. */
  5074. public function getCustomDefaultSort() {
  5075. return $this->mDefaultSort;
  5076. }
  5077. /**
  5078. * Try to guess the section anchor name based on a wikitext fragment
  5079. * presumably extracted from a heading, for example "Header" from
  5080. * "== Header ==".
  5081. *
  5082. * @param $text string
  5083. *
  5084. * @return string
  5085. */
  5086. public function guessSectionNameFromWikiText( $text ) {
  5087. # Strip out wikitext links(they break the anchor)
  5088. $text = $this->stripSectionName( $text );
  5089. $text = Sanitizer::normalizeSectionNameWhitespace( $text );
  5090. return '#' . Sanitizer::escapeId( $text, 'noninitial' );
  5091. }
  5092. /**
  5093. * Same as guessSectionNameFromWikiText(), but produces legacy anchors
  5094. * instead. For use in redirects, since IE6 interprets Redirect: headers
  5095. * as something other than UTF-8 (apparently?), resulting in breakage.
  5096. *
  5097. * @param $text String: The section name
  5098. * @return string An anchor
  5099. */
  5100. public function guessLegacySectionNameFromWikiText( $text ) {
  5101. # Strip out wikitext links(they break the anchor)
  5102. $text = $this->stripSectionName( $text );
  5103. $text = Sanitizer::normalizeSectionNameWhitespace( $text );
  5104. return '#' . Sanitizer::escapeId( $text, array( 'noninitial', 'legacy' ) );
  5105. }
  5106. /**
  5107. * Strips a text string of wikitext for use in a section anchor
  5108. *
  5109. * Accepts a text string and then removes all wikitext from the
  5110. * string and leaves only the resultant text (i.e. the result of
  5111. * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
  5112. * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
  5113. * to create valid section anchors by mimicing the output of the
  5114. * parser when headings are parsed.
  5115. *
  5116. * @param $text String: text string to be stripped of wikitext
  5117. * for use in a Section anchor
  5118. * @return string Filtered text string
  5119. */
  5120. public function stripSectionName( $text ) {
  5121. # Strip internal link markup
  5122. $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
  5123. $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
  5124. # Strip external link markup
  5125. # @todo FIXME: Not tolerant to blank link text
  5126. # I.E. [http://www.mediawiki.org] will render as [1] or something depending
  5127. # on how many empty links there are on the page - need to figure that out.
  5128. $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
  5129. # Parse wikitext quotes (italics & bold)
  5130. $text = $this->doQuotes( $text );
  5131. # Strip HTML tags
  5132. $text = StringUtils::delimiterReplace( '<', '>', '', $text );
  5133. return $text;
  5134. }
  5135. /**
  5136. * strip/replaceVariables/unstrip for preprocessor regression testing
  5137. *
  5138. * @param $text string
  5139. * @param $title Title
  5140. * @param $options ParserOptions
  5141. * @param $outputType int
  5142. *
  5143. * @return string
  5144. */
  5145. function testSrvus( $text, Title $title, ParserOptions $options, $outputType = self::OT_HTML ) {
  5146. $this->startParse( $title, $options, $outputType, true );
  5147. $text = $this->replaceVariables( $text );
  5148. $text = $this->mStripState->unstripBoth( $text );
  5149. $text = Sanitizer::removeHTMLtags( $text );
  5150. return $text;
  5151. }
  5152. /**
  5153. * @param $text string
  5154. * @param $title Title
  5155. * @param $options ParserOptions
  5156. * @return string
  5157. */
  5158. function testPst( $text, Title $title, ParserOptions $options ) {
  5159. return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
  5160. }
  5161. /**
  5162. * @param $text
  5163. * @param $title Title
  5164. * @param $options ParserOptions
  5165. * @return string
  5166. */
  5167. function testPreprocess( $text, Title $title, ParserOptions $options ) {
  5168. return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
  5169. }
  5170. /**
  5171. * Call a callback function on all regions of the given text that are not
  5172. * inside strip markers, and replace those regions with the return value
  5173. * of the callback. For example, with input:
  5174. *
  5175. * aaa<MARKER>bbb
  5176. *
  5177. * This will call the callback function twice, with 'aaa' and 'bbb'. Those
  5178. * two strings will be replaced with the value returned by the callback in
  5179. * each case.
  5180. *
  5181. * @param $s string
  5182. * @param $callback
  5183. *
  5184. * @return string
  5185. */
  5186. function markerSkipCallback( $s, $callback ) {
  5187. $i = 0;
  5188. $out = '';
  5189. while ( $i < strlen( $s ) ) {
  5190. $markerStart = strpos( $s, $this->mUniqPrefix, $i );
  5191. if ( $markerStart === false ) {
  5192. $out .= call_user_func( $callback, substr( $s, $i ) );
  5193. break;
  5194. } else {
  5195. $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
  5196. $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
  5197. if ( $markerEnd === false ) {
  5198. $out .= substr( $s, $markerStart );
  5199. break;
  5200. } else {
  5201. $markerEnd += strlen( self::MARKER_SUFFIX );
  5202. $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
  5203. $i = $markerEnd;
  5204. }
  5205. }
  5206. }
  5207. return $out;
  5208. }
  5209. /**
  5210. * Remove any strip markers found in the given text.
  5211. *
  5212. * @param $text Input string
  5213. * @return string
  5214. */
  5215. function killMarkers( $text ) {
  5216. return $this->mStripState->killMarkers( $text );
  5217. }
  5218. /**
  5219. * Save the parser state required to convert the given half-parsed text to
  5220. * HTML. "Half-parsed" in this context means the output of
  5221. * recursiveTagParse() or internalParse(). This output has strip markers
  5222. * from replaceVariables (extensionSubstitution() etc.), and link
  5223. * placeholders from replaceLinkHolders().
  5224. *
  5225. * Returns an array which can be serialized and stored persistently. This
  5226. * array can later be loaded into another parser instance with
  5227. * unserializeHalfParsedText(). The text can then be safely incorporated into
  5228. * the return value of a parser hook.
  5229. *
  5230. * @param $text string
  5231. *
  5232. * @return array
  5233. */
  5234. function serializeHalfParsedText( $text ) {
  5235. wfProfileIn( __METHOD__ );
  5236. $data = array(
  5237. 'text' => $text,
  5238. 'version' => self::HALF_PARSED_VERSION,
  5239. 'stripState' => $this->mStripState->getSubState( $text ),
  5240. 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
  5241. );
  5242. wfProfileOut( __METHOD__ );
  5243. return $data;
  5244. }
  5245. /**
  5246. * Load the parser state given in the $data array, which is assumed to
  5247. * have been generated by serializeHalfParsedText(). The text contents is
  5248. * extracted from the array, and its markers are transformed into markers
  5249. * appropriate for the current Parser instance. This transformed text is
  5250. * returned, and can be safely included in the return value of a parser
  5251. * hook.
  5252. *
  5253. * If the $data array has been stored persistently, the caller should first
  5254. * check whether it is still valid, by calling isValidHalfParsedText().
  5255. *
  5256. * @param $data array Serialized data
  5257. * @return String
  5258. */
  5259. function unserializeHalfParsedText( $data ) {
  5260. if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
  5261. throw new MWException( __METHOD__.': invalid version' );
  5262. }
  5263. # First, extract the strip state.
  5264. $texts = array( $data['text'] );
  5265. $texts = $this->mStripState->merge( $data['stripState'], $texts );
  5266. # Now renumber links
  5267. $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
  5268. # Should be good to go.
  5269. return $texts[0];
  5270. }
  5271. /**
  5272. * Returns true if the given array, presumed to be generated by
  5273. * serializeHalfParsedText(), is compatible with the current version of the
  5274. * parser.
  5275. *
  5276. * @param $data Array
  5277. *
  5278. * @return bool
  5279. */
  5280. function isValidHalfParsedText( $data ) {
  5281. return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
  5282. }
  5283. /**
  5284. * Parsed a width param of imagelink like 300px or 200x300px
  5285. *
  5286. * @param $value String
  5287. *
  5288. * @return array
  5289. * @since 1.20
  5290. */
  5291. public function parseWidthParam( $value ) {
  5292. $parsedWidthParam = array();
  5293. if( $value === '' ) {
  5294. return $parsedWidthParam;
  5295. }
  5296. $m = array();
  5297. # (bug 13500) In both cases (width/height and width only),
  5298. # permit trailing "px" for backward compatibility.
  5299. if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
  5300. $width = intval( $m[1] );
  5301. $height = intval( $m[2] );
  5302. $parsedWidthParam['width'] = $width;
  5303. $parsedWidthParam['height'] = $height;
  5304. } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
  5305. $width = intval( $value );
  5306. $parsedWidthParam['width'] = $width;
  5307. }
  5308. return $parsedWidthParam;
  5309. }
  5310. }