PageRenderTime 84ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/includes/parser/Parser.php

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