PageRenderTime 64ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/includes/parser/Parser.php

https://bitbucket.org/ghostfreeman/freeside-wiki
PHP | 5824 lines | 3544 code | 502 blank | 1778 comment | 693 complexity | 9d59990fe5fcb7ecb49bf426312bf069 MD5 | raw file
Possible License(s): GPL-2.0, Apache-2.0, LGPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * PHP parser that converts wiki markup to HTML.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Parser
  22. */
  23. /**
  24. * @defgroup Parser Parser
  25. */
  26. /**
  27. * PHP Parser - Processes wiki markup (which uses a more user-friendly
  28. * syntax, such as "[[link]]" for making links), and provides a one-way
  29. * transformation of that wiki markup it into XHTML output / markup
  30. * (which in turn the browser understands, and can display).
  31. *
  32. * There are seven main entry points into the Parser class:
  33. *
  34. * - Parser::parse()
  35. * produces HTML output
  36. * - Parser::preSaveTransform().
  37. * produces altered wiki markup.
  38. * - Parser::preprocess()
  39. * removes HTML comments and expands templates
  40. * - Parser::cleanSig() and Parser::cleanSigInSig()
  41. * Cleans a signature before saving it to preferences
  42. * - Parser::getSection()
  43. * Return the content of a section from an article for section editing
  44. * - Parser::replaceSection()
  45. * Replaces a section by number inside an article
  46. * - Parser::getPreloadText()
  47. * Removes <noinclude> sections, and <includeonly> tags.
  48. *
  49. * Globals used:
  50. * object: $wgContLang
  51. *
  52. * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
  53. *
  54. * @par Settings:
  55. * $wgLocaltimezone
  56. * $wgNamespacesWithSubpages
  57. *
  58. * @par Settings only within ParserOptions:
  59. * $wgAllowExternalImages
  60. * $wgAllowSpecialInclusion
  61. * $wgInterwikiMagic
  62. * $wgMaxArticleSize
  63. * $wgUseDynamicDates
  64. *
  65. * @ingroup Parser
  66. */
  67. class Parser {
  68. /**
  69. * Update this version number when the ParserOutput format
  70. * changes in an incompatible way, so the parser cache
  71. * can automatically discard old data.
  72. */
  73. const VERSION = '1.6.4';
  74. /**
  75. * Update this version number when the output of serialiseHalfParsedText()
  76. * changes in an incompatible way
  77. */
  78. const HALF_PARSED_VERSION = 2;
  79. # Flags for Parser::setFunctionHook
  80. # Also available as global constants from Defines.php
  81. const SFH_NO_HASH = 1;
  82. const SFH_OBJECT_ARGS = 2;
  83. # Constants needed for external link processing
  84. # Everything except bracket, space, or control characters
  85. # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
  86. # as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
  87. const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
  88. const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
  89. \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
  90. # State constants for the definition list colon extraction
  91. const COLON_STATE_TEXT = 0;
  92. const COLON_STATE_TAG = 1;
  93. const COLON_STATE_TAGSTART = 2;
  94. const COLON_STATE_CLOSETAG = 3;
  95. const COLON_STATE_TAGSLASH = 4;
  96. const COLON_STATE_COMMENT = 5;
  97. const COLON_STATE_COMMENTDASH = 6;
  98. const COLON_STATE_COMMENTDASHDASH = 7;
  99. # Flags for preprocessToDom
  100. const PTD_FOR_INCLUSION = 1;
  101. # Allowed values for $this->mOutputType
  102. # Parameter to startExternalParse().
  103. const OT_HTML = 1; # like parse()
  104. const OT_WIKI = 2; # like preSaveTransform()
  105. const OT_PREPROCESS = 3; # like preprocess()
  106. const OT_MSG = 3;
  107. const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
  108. # Marker Suffix needs to be accessible staticly.
  109. const MARKER_SUFFIX = "-QINU\x7f";
  110. # Persistent:
  111. var $mTagHooks = array();
  112. var $mTransparentTagHooks = array();
  113. var $mFunctionHooks = array();
  114. var $mFunctionSynonyms = array( 0 => array(), 1 => array() );
  115. var $mFunctionTagHooks = array();
  116. var $mStripList = array();
  117. var $mDefaultStripList = array();
  118. var $mVarCache = array();
  119. var $mImageParams = array();
  120. var $mImageParamsMagicArray = array();
  121. var $mMarkerIndex = 0;
  122. var $mFirstCall = true;
  123. # Initialised by initialiseVariables()
  124. /**
  125. * @var MagicWordArray
  126. */
  127. var $mVariables;
  128. /**
  129. * @var MagicWordArray
  130. */
  131. var $mSubstWords;
  132. var $mConf, $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols; # Initialised in constructor
  133. # Cleared with clearState():
  134. /**
  135. * @var ParserOutput
  136. */
  137. var $mOutput;
  138. var $mAutonumber, $mDTopen;
  139. /**
  140. * @var StripState
  141. */
  142. var $mStripState;
  143. var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
  144. /**
  145. * @var LinkHolderArray
  146. */
  147. var $mLinkHolders;
  148. var $mLinkID;
  149. var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
  150. var $mDefaultSort;
  151. var $mTplExpandCache; # empty-frame expansion cache
  152. var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
  153. var $mExpensiveFunctionCount; # number of expensive parser function calls
  154. var $mShowToc, $mForceTocPosition;
  155. /**
  156. * @var User
  157. */
  158. var $mUser; # User object; only used when doing pre-save transform
  159. # Temporary
  160. # These are variables reset at least once per parse regardless of $clearState
  161. /**
  162. * @var ParserOptions
  163. */
  164. var $mOptions;
  165. /**
  166. * @var Title
  167. */
  168. var $mTitle; # Title context, used for self-link rendering and similar things
  169. var $mOutputType; # Output type, one of the OT_xxx constants
  170. var $ot; # Shortcut alias, see setOutputType()
  171. var $mRevisionObject; # The revision object of the specified revision ID
  172. var $mRevisionId; # ID to display in {{REVISIONID}} tags
  173. var $mRevisionTimestamp; # The timestamp of the specified revision ID
  174. var $mRevisionUser; # User to display in {{REVISIONUSER}} tag
  175. var $mRevIdForTs; # The revision ID which was used to fetch the timestamp
  176. /**
  177. * @var string
  178. */
  179. var $mUniqPrefix;
  180. /**
  181. * Constructor
  182. *
  183. * @param $conf array
  184. */
  185. public function __construct( $conf = array() ) {
  186. $this->mConf = $conf;
  187. $this->mUrlProtocols = wfUrlProtocols();
  188. $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')'.
  189. self::EXT_LINK_URL_CLASS.'+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
  190. if ( isset( $conf['preprocessorClass'] ) ) {
  191. $this->mPreprocessorClass = $conf['preprocessorClass'];
  192. } elseif ( defined( 'MW_COMPILED' ) ) {
  193. # Preprocessor_Hash is much faster than Preprocessor_DOM in compiled mode
  194. $this->mPreprocessorClass = 'Preprocessor_Hash';
  195. } elseif ( extension_loaded( 'domxml' ) ) {
  196. # PECL extension that conflicts with the core DOM extension (bug 13770)
  197. wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
  198. $this->mPreprocessorClass = 'Preprocessor_Hash';
  199. } elseif ( extension_loaded( 'dom' ) ) {
  200. $this->mPreprocessorClass = 'Preprocessor_DOM';
  201. } else {
  202. $this->mPreprocessorClass = 'Preprocessor_Hash';
  203. }
  204. wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
  205. }
  206. /**
  207. * Reduce memory usage to reduce the impact of circular references
  208. */
  209. function __destruct() {
  210. if ( isset( $this->mLinkHolders ) ) {
  211. unset( $this->mLinkHolders );
  212. }
  213. foreach ( $this as $name => $value ) {
  214. unset( $this->$name );
  215. }
  216. }
  217. /**
  218. * Do various kinds of initialisation on the first call of the parser
  219. */
  220. function firstCallInit() {
  221. if ( !$this->mFirstCall ) {
  222. return;
  223. }
  224. $this->mFirstCall = false;
  225. wfProfileIn( __METHOD__ );
  226. CoreParserFunctions::register( $this );
  227. CoreTagHooks::register( $this );
  228. $this->initialiseVariables();
  229. wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
  230. wfProfileOut( __METHOD__ );
  231. }
  232. /**
  233. * Clear Parser state
  234. *
  235. * @private
  236. */
  237. function clearState() {
  238. wfProfileIn( __METHOD__ );
  239. if ( $this->mFirstCall ) {
  240. $this->firstCallInit();
  241. }
  242. $this->mOutput = new ParserOutput;
  243. $this->mOptions->registerWatcher( array( $this->mOutput, 'recordOption' ) );
  244. $this->mAutonumber = 0;
  245. $this->mLastSection = '';
  246. $this->mDTopen = false;
  247. $this->mIncludeCount = array();
  248. $this->mArgStack = false;
  249. $this->mInPre = false;
  250. $this->mLinkHolders = new LinkHolderArray( $this );
  251. $this->mLinkID = 0;
  252. $this->mRevisionObject = $this->mRevisionTimestamp =
  253. $this->mRevisionId = $this->mRevisionUser = null;
  254. $this->mVarCache = array();
  255. $this->mUser = null;
  256. /**
  257. * Prefix for temporary replacement strings for the multipass parser.
  258. * \x07 should never appear in input as it's disallowed in XML.
  259. * Using it at the front also gives us a little extra robustness
  260. * since it shouldn't match when butted up against identifier-like
  261. * string constructs.
  262. *
  263. * Must not consist of all title characters, or else it will change
  264. * the behaviour of <nowiki> in a link.
  265. */
  266. $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
  267. $this->mStripState = new StripState( $this->mUniqPrefix );
  268. # Clear these on every parse, bug 4549
  269. $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
  270. $this->mShowToc = true;
  271. $this->mForceTocPosition = false;
  272. $this->mIncludeSizes = array(
  273. 'post-expand' => 0,
  274. 'arg' => 0,
  275. );
  276. $this->mPPNodeCount = 0;
  277. $this->mGeneratedPPNodeCount = 0;
  278. $this->mHighestExpansionDepth = 0;
  279. $this->mDefaultSort = false;
  280. $this->mHeadings = array();
  281. $this->mDoubleUnderscores = array();
  282. $this->mExpensiveFunctionCount = 0;
  283. # Fix cloning
  284. if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
  285. $this->mPreprocessor = null;
  286. }
  287. wfRunHooks( 'ParserClearState', array( &$this ) );
  288. wfProfileOut( __METHOD__ );
  289. }
  290. /**
  291. * Convert wikitext to HTML
  292. * Do not call this function recursively.
  293. *
  294. * @param $text String: text we want to parse
  295. * @param $title Title object
  296. * @param $options ParserOptions
  297. * @param $linestart boolean
  298. * @param $clearState boolean
  299. * @param $revid Int: number to pass in {{REVISIONID}}
  300. * @return ParserOutput a ParserOutput
  301. */
  302. public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) {
  303. /**
  304. * First pass--just handle <nowiki> sections, pass the rest off
  305. * to internalParse() which does all the real work.
  306. */
  307. global $wgUseTidy, $wgAlwaysUseTidy;
  308. $fname = __METHOD__.'-' . wfGetCaller();
  309. wfProfileIn( __METHOD__ );
  310. wfProfileIn( $fname );
  311. $this->startParse( $title, $options, self::OT_HTML, $clearState );
  312. # Remove the strip marker tag prefix from the input, if present.
  313. if ( $clearState ) {
  314. $text = str_replace( $this->mUniqPrefix, '', $text );
  315. }
  316. $oldRevisionId = $this->mRevisionId;
  317. $oldRevisionObject = $this->mRevisionObject;
  318. $oldRevisionTimestamp = $this->mRevisionTimestamp;
  319. $oldRevisionUser = $this->mRevisionUser;
  320. if ( $revid !== null ) {
  321. $this->mRevisionId = $revid;
  322. $this->mRevisionObject = null;
  323. $this->mRevisionTimestamp = null;
  324. $this->mRevisionUser = null;
  325. }
  326. wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
  327. # No more strip!
  328. wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
  329. $text = $this->internalParse( $text );
  330. wfRunHooks( 'ParserAfterParse', array( &$this, &$text, &$this->mStripState ) );
  331. $text = $this->mStripState->unstripGeneral( $text );
  332. # Clean up special characters, only run once, next-to-last before doBlockLevels
  333. $fixtags = array(
  334. # french spaces, last one Guillemet-left
  335. # only if there is something before the space
  336. '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
  337. # french spaces, Guillemet-right
  338. '/(\\302\\253) /' => '\\1&#160;',
  339. '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
  340. );
  341. $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
  342. $text = $this->doBlockLevels( $text, $linestart );
  343. $this->replaceLinkHolders( $text );
  344. /**
  345. * The input doesn't get language converted if
  346. * a) It's disabled
  347. * b) Content isn't converted
  348. * c) It's a conversion table
  349. * d) it is an interface message (which is in the user language)
  350. */
  351. if ( !( $options->getDisableContentConversion()
  352. || isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) )
  353. {
  354. # Run convert unconditionally in 1.18-compatible mode
  355. global $wgBug34832TransitionalRollback;
  356. if ( $wgBug34832TransitionalRollback || !$this->mOptions->getInterfaceMessage() ) {
  357. # The position of the convert() call should not be changed. it
  358. # assumes that the links are all replaced and the only thing left
  359. # is the <nowiki> mark.
  360. $text = $this->getConverterLanguage()->convert( $text );
  361. }
  362. }
  363. /**
  364. * A converted title will be provided in the output object if title and
  365. * content conversion are enabled, the article text does not contain
  366. * a conversion-suppressing double-underscore tag, and no
  367. * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
  368. * automatic link conversion.
  369. */
  370. if ( !( $options->getDisableTitleConversion()
  371. || isset( $this->mDoubleUnderscores['nocontentconvert'] )
  372. || isset( $this->mDoubleUnderscores['notitleconvert'] )
  373. || $this->mOutput->getDisplayTitle() !== false ) )
  374. {
  375. $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
  376. if ( $convruletitle ) {
  377. $this->mOutput->setTitleText( $convruletitle );
  378. } else {
  379. $titleText = $this->getConverterLanguage()->convertTitle( $title );
  380. $this->mOutput->setTitleText( $titleText );
  381. }
  382. }
  383. $text = $this->mStripState->unstripNoWiki( $text );
  384. wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
  385. $text = $this->replaceTransparentTags( $text );
  386. $text = $this->mStripState->unstripGeneral( $text );
  387. $text = Sanitizer::normalizeCharReferences( $text );
  388. if ( ( $wgUseTidy && $this->mOptions->getTidy() ) || $wgAlwaysUseTidy ) {
  389. $text = MWTidy::tidy( $text );
  390. } else {
  391. # attempt to sanitize at least some nesting problems
  392. # (bug #2702 and quite a few others)
  393. $tidyregs = array(
  394. # ''Something [http://www.cool.com cool''] -->
  395. # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
  396. '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
  397. '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
  398. # fix up an anchor inside another anchor, only
  399. # at least for a single single nested link (bug 3695)
  400. '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
  401. '\\1\\2</a>\\3</a>\\1\\4</a>',
  402. # fix div inside inline elements- doBlockLevels won't wrap a line which
  403. # contains a div, so fix it up here; replace
  404. # div with escaped text
  405. '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
  406. '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
  407. # remove empty italic or bold tag pairs, some
  408. # introduced by rules above
  409. '/<([bi])><\/\\1>/' => '',
  410. );
  411. $text = preg_replace(
  412. array_keys( $tidyregs ),
  413. array_values( $tidyregs ),
  414. $text );
  415. }
  416. if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
  417. $this->limitationWarn( 'expensive-parserfunction',
  418. $this->mExpensiveFunctionCount,
  419. $this->mOptions->getExpensiveParserFunctionLimit()
  420. );
  421. }
  422. wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
  423. # Information on include size limits, for the benefit of users who try to skirt them
  424. if ( $this->mOptions->getEnableLimitReport() ) {
  425. $max = $this->mOptions->getMaxIncludeSize();
  426. $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/{$this->mOptions->getExpensiveParserFunctionLimit()}\n";
  427. $limitReport =
  428. "NewPP limit report\n" .
  429. "Preprocessor visited node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\n" .
  430. "Preprocessor generated node count: " .
  431. "{$this->mGeneratedPPNodeCount}/{$this->mOptions->getMaxGeneratedPPNodeCount()}\n" .
  432. "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
  433. "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
  434. "Highest expansion depth: {$this->mHighestExpansionDepth}/{$this->mOptions->getMaxPPExpandDepth()}\n".
  435. $PFreport;
  436. wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
  437. $text .= "\n<!-- \n$limitReport-->\n";
  438. }
  439. $this->mOutput->setText( $text );
  440. $this->mRevisionId = $oldRevisionId;
  441. $this->mRevisionObject = $oldRevisionObject;
  442. $this->mRevisionTimestamp = $oldRevisionTimestamp;
  443. $this->mRevisionUser = $oldRevisionUser;
  444. wfProfileOut( $fname );
  445. wfProfileOut( __METHOD__ );
  446. return $this->mOutput;
  447. }
  448. /**
  449. * Recursive parser entry point that can be called from an extension tag
  450. * hook.
  451. *
  452. * If $frame is not provided, then template variables (e.g., {{{1}}}) within $text are not expanded
  453. *
  454. * @param $text String: text extension wants to have parsed
  455. * @param $frame PPFrame: The frame to use for expanding any template variables
  456. *
  457. * @return string
  458. */
  459. function recursiveTagParse( $text, $frame=false ) {
  460. wfProfileIn( __METHOD__ );
  461. wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
  462. wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
  463. $text = $this->internalParse( $text, false, $frame );
  464. wfProfileOut( __METHOD__ );
  465. return $text;
  466. }
  467. /**
  468. * Expand templates and variables in the text, producing valid, static wikitext.
  469. * Also removes comments.
  470. * @return mixed|string
  471. */
  472. function preprocess( $text, Title $title, ParserOptions $options, $revid = null ) {
  473. wfProfileIn( __METHOD__ );
  474. $this->startParse( $title, $options, self::OT_PREPROCESS, true );
  475. if ( $revid !== null ) {
  476. $this->mRevisionId = $revid;
  477. }
  478. wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
  479. wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
  480. $text = $this->replaceVariables( $text );
  481. $text = $this->mStripState->unstripBoth( $text );
  482. wfProfileOut( __METHOD__ );
  483. return $text;
  484. }
  485. /**
  486. * Recursive parser entry point that can be called from an extension tag
  487. * hook.
  488. *
  489. * @param $text String: text to be expanded
  490. * @param $frame PPFrame: The frame to use for expanding any template variables
  491. * @return String
  492. * @since 1.19
  493. */
  494. public function recursivePreprocess( $text, $frame = false ) {
  495. wfProfileIn( __METHOD__ );
  496. $text = $this->replaceVariables( $text, $frame );
  497. $text = $this->mStripState->unstripBoth( $text );
  498. wfProfileOut( __METHOD__ );
  499. return $text;
  500. }
  501. /**
  502. * Process the wikitext for the "?preload=" feature. (bug 5210)
  503. *
  504. * "<noinclude>", "<includeonly>" etc. are parsed as for template
  505. * transclusion, comments, templates, arguments, tags hooks and parser
  506. * functions are untouched.
  507. *
  508. * @param $text String
  509. * @param $title Title
  510. * @param $options ParserOptions
  511. * @return String
  512. */
  513. public function getPreloadText( $text, Title $title, ParserOptions $options ) {
  514. # Parser (re)initialisation
  515. $this->startParse( $title, $options, self::OT_PLAIN, true );
  516. $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
  517. $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
  518. $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
  519. $text = $this->mStripState->unstripBoth( $text );
  520. return $text;
  521. }
  522. /**
  523. * Get a random string
  524. *
  525. * @return string
  526. */
  527. static public function getRandomString() {
  528. return wfRandomString( 16 );
  529. }
  530. /**
  531. * Set the current user.
  532. * Should only be used when doing pre-save transform.
  533. *
  534. * @param $user Mixed: User object or null (to reset)
  535. */
  536. function setUser( $user ) {
  537. $this->mUser = $user;
  538. }
  539. /**
  540. * Accessor for mUniqPrefix.
  541. *
  542. * @return String
  543. */
  544. public function uniqPrefix() {
  545. if ( !isset( $this->mUniqPrefix ) ) {
  546. # @todo FIXME: This is probably *horribly wrong*
  547. # LanguageConverter seems to want $wgParser's uniqPrefix, however
  548. # if this is called for a parser cache hit, the parser may not
  549. # have ever been initialized in the first place.
  550. # Not really sure what the heck is supposed to be going on here.
  551. return '';
  552. # throw new MWException( "Accessing uninitialized mUniqPrefix" );
  553. }
  554. return $this->mUniqPrefix;
  555. }
  556. /**
  557. * Set the context title
  558. *
  559. * @param $t Title
  560. */
  561. function setTitle( $t ) {
  562. if ( !$t || $t instanceof FakeTitle ) {
  563. $t = Title::newFromText( 'NO TITLE' );
  564. }
  565. if ( strval( $t->getFragment() ) !== '' ) {
  566. # Strip the fragment to avoid various odd effects
  567. $this->mTitle = clone $t;
  568. $this->mTitle->setFragment( '' );
  569. } else {
  570. $this->mTitle = $t;
  571. }
  572. }
  573. /**
  574. * Accessor for the Title object
  575. *
  576. * @return Title object
  577. */
  578. function getTitle() {
  579. return $this->mTitle;
  580. }
  581. /**
  582. * Accessor/mutator for the Title object
  583. *
  584. * @param $x Title object or null to just get the current one
  585. * @return Title object
  586. */
  587. function Title( $x = null ) {
  588. return wfSetVar( $this->mTitle, $x );
  589. }
  590. /**
  591. * Set the output type
  592. *
  593. * @param $ot Integer: new value
  594. */
  595. function setOutputType( $ot ) {
  596. $this->mOutputType = $ot;
  597. # Shortcut alias
  598. $this->ot = array(
  599. 'html' => $ot == self::OT_HTML,
  600. 'wiki' => $ot == self::OT_WIKI,
  601. 'pre' => $ot == self::OT_PREPROCESS,
  602. 'plain' => $ot == self::OT_PLAIN,
  603. );
  604. }
  605. /**
  606. * Accessor/mutator for the output type
  607. *
  608. * @param $x int|null New value or null to just get the current one
  609. * @return Integer
  610. */
  611. function OutputType( $x = null ) {
  612. return wfSetVar( $this->mOutputType, $x );
  613. }
  614. /**
  615. * Get the ParserOutput object
  616. *
  617. * @return ParserOutput object
  618. */
  619. function getOutput() {
  620. return $this->mOutput;
  621. }
  622. /**
  623. * Get the ParserOptions object
  624. *
  625. * @return ParserOptions object
  626. */
  627. function getOptions() {
  628. return $this->mOptions;
  629. }
  630. /**
  631. * Accessor/mutator for the ParserOptions object
  632. *
  633. * @param $x ParserOptions New value or null to just get the current one
  634. * @return ParserOptions Current ParserOptions object
  635. */
  636. function Options( $x = null ) {
  637. return wfSetVar( $this->mOptions, $x );
  638. }
  639. /**
  640. * @return int
  641. */
  642. function nextLinkID() {
  643. return $this->mLinkID++;
  644. }
  645. /**
  646. * @param $id int
  647. */
  648. function setLinkID( $id ) {
  649. $this->mLinkID = $id;
  650. }
  651. /**
  652. * Get a language object for use in parser functions such as {{FORMATNUM:}}
  653. * @return Language
  654. */
  655. function getFunctionLang() {
  656. return $this->getTargetLanguage();
  657. }
  658. /**
  659. * Get the target language for the content being parsed. This is usually the
  660. * language that the content is in.
  661. *
  662. * @since 1.19
  663. *
  664. * @return Language|null
  665. */
  666. public function getTargetLanguage() {
  667. $target = $this->mOptions->getTargetLanguage();
  668. if ( $target !== null ) {
  669. return $target;
  670. } elseif( $this->mOptions->getInterfaceMessage() ) {
  671. return $this->mOptions->getUserLangObj();
  672. } elseif( is_null( $this->mTitle ) ) {
  673. throw new MWException( __METHOD__ . ': $this->mTitle is null' );
  674. }
  675. return $this->mTitle->getPageLanguage();
  676. }
  677. /**
  678. * Get the language object for language conversion
  679. */
  680. function getConverterLanguage() {
  681. global $wgBug34832TransitionalRollback, $wgContLang;
  682. if ( $wgBug34832TransitionalRollback ) {
  683. return $wgContLang;
  684. } else {
  685. return $this->getTargetLanguage();
  686. }
  687. }
  688. /**
  689. * Get a User object either from $this->mUser, if set, or from the
  690. * ParserOptions object otherwise
  691. *
  692. * @return User object
  693. */
  694. function getUser() {
  695. if ( !is_null( $this->mUser ) ) {
  696. return $this->mUser;
  697. }
  698. return $this->mOptions->getUser();
  699. }
  700. /**
  701. * Get a preprocessor object
  702. *
  703. * @return Preprocessor instance
  704. */
  705. function getPreprocessor() {
  706. if ( !isset( $this->mPreprocessor ) ) {
  707. $class = $this->mPreprocessorClass;
  708. $this->mPreprocessor = new $class( $this );
  709. }
  710. return $this->mPreprocessor;
  711. }
  712. /**
  713. * Replaces all occurrences of HTML-style comments and the given tags
  714. * in the text with a random marker and returns the next text. The output
  715. * parameter $matches will be an associative array filled with data in
  716. * the form:
  717. *
  718. * @code
  719. * 'UNIQ-xxxxx' => array(
  720. * 'element',
  721. * 'tag content',
  722. * array( 'param' => 'x' ),
  723. * '<element param="x">tag content</element>' ) )
  724. * @endcode
  725. *
  726. * @param $elements array list of element names. Comments are always extracted.
  727. * @param $text string Source text string.
  728. * @param $matches array Out parameter, Array: extracted tags
  729. * @param $uniq_prefix string
  730. * @return String: stripped text
  731. */
  732. public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = '' ) {
  733. static $n = 1;
  734. $stripped = '';
  735. $matches = array();
  736. $taglist = implode( '|', $elements );
  737. $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
  738. while ( $text != '' ) {
  739. $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
  740. $stripped .= $p[0];
  741. if ( count( $p ) < 5 ) {
  742. break;
  743. }
  744. if ( count( $p ) > 5 ) {
  745. # comment
  746. $element = $p[4];
  747. $attributes = '';
  748. $close = '';
  749. $inside = $p[5];
  750. } else {
  751. # tag
  752. $element = $p[1];
  753. $attributes = $p[2];
  754. $close = $p[3];
  755. $inside = $p[4];
  756. }
  757. $marker = "$uniq_prefix-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
  758. $stripped .= $marker;
  759. if ( $close === '/>' ) {
  760. # Empty element tag, <tag />
  761. $content = null;
  762. $text = $inside;
  763. $tail = null;
  764. } else {
  765. if ( $element === '!--' ) {
  766. $end = '/(-->)/';
  767. } else {
  768. $end = "/(<\\/$element\\s*>)/i";
  769. }
  770. $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
  771. $content = $q[0];
  772. if ( count( $q ) < 3 ) {
  773. # No end tag -- let it run out to the end of the text.
  774. $tail = '';
  775. $text = '';
  776. } else {
  777. $tail = $q[1];
  778. $text = $q[2];
  779. }
  780. }
  781. $matches[$marker] = array( $element,
  782. $content,
  783. Sanitizer::decodeTagAttributes( $attributes ),
  784. "<$element$attributes$close$content$tail" );
  785. }
  786. return $stripped;
  787. }
  788. /**
  789. * Get a list of strippable XML-like elements
  790. *
  791. * @return array
  792. */
  793. function getStripList() {
  794. return $this->mStripList;
  795. }
  796. /**
  797. * Add an item to the strip state
  798. * Returns the unique tag which must be inserted into the stripped text
  799. * The tag will be replaced with the original text in unstrip()
  800. *
  801. * @param $text string
  802. *
  803. * @return string
  804. */
  805. function insertStripItem( $text ) {
  806. $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
  807. $this->mMarkerIndex++;
  808. $this->mStripState->addGeneral( $rnd, $text );
  809. return $rnd;
  810. }
  811. /**
  812. * parse the wiki syntax used to render tables
  813. *
  814. * @private
  815. * @return string
  816. */
  817. function doTableStuff( $text ) {
  818. wfProfileIn( __METHOD__ );
  819. $lines = StringUtils::explode( "\n", $text );
  820. $out = '';
  821. $td_history = array(); # Is currently a td tag open?
  822. $last_tag_history = array(); # Save history of last lag activated (td, th or caption)
  823. $tr_history = array(); # Is currently a tr tag open?
  824. $tr_attributes = array(); # history of tr attributes
  825. $has_opened_tr = array(); # Did this table open a <tr> element?
  826. $indent_level = 0; # indent level of the table
  827. foreach ( $lines as $outLine ) {
  828. $line = trim( $outLine );
  829. if ( $line === '' ) { # empty line, go to next line
  830. $out .= $outLine."\n";
  831. continue;
  832. }
  833. $first_character = $line[0];
  834. $matches = array();
  835. if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) {
  836. # First check if we are starting a new table
  837. $indent_level = strlen( $matches[1] );
  838. $attributes = $this->mStripState->unstripBoth( $matches[2] );
  839. $attributes = Sanitizer::fixTagAttributes( $attributes , 'table' );
  840. $outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
  841. array_push( $td_history , false );
  842. array_push( $last_tag_history , '' );
  843. array_push( $tr_history , false );
  844. array_push( $tr_attributes , '' );
  845. array_push( $has_opened_tr , false );
  846. } elseif ( count( $td_history ) == 0 ) {
  847. # Don't do any of the following
  848. $out .= $outLine."\n";
  849. continue;
  850. } elseif ( substr( $line , 0 , 2 ) === '|}' ) {
  851. # We are ending a table
  852. $line = '</table>' . substr( $line , 2 );
  853. $last_tag = array_pop( $last_tag_history );
  854. if ( !array_pop( $has_opened_tr ) ) {
  855. $line = "<tr><td></td></tr>{$line}";
  856. }
  857. if ( array_pop( $tr_history ) ) {
  858. $line = "</tr>{$line}";
  859. }
  860. if ( array_pop( $td_history ) ) {
  861. $line = "</{$last_tag}>{$line}";
  862. }
  863. array_pop( $tr_attributes );
  864. $outLine = $line . str_repeat( '</dd></dl>' , $indent_level );
  865. } elseif ( substr( $line , 0 , 2 ) === '|-' ) {
  866. # Now we have a table row
  867. $line = preg_replace( '#^\|-+#', '', $line );
  868. # Whats after the tag is now only attributes
  869. $attributes = $this->mStripState->unstripBoth( $line );
  870. $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
  871. array_pop( $tr_attributes );
  872. array_push( $tr_attributes, $attributes );
  873. $line = '';
  874. $last_tag = array_pop( $last_tag_history );
  875. array_pop( $has_opened_tr );
  876. array_push( $has_opened_tr , true );
  877. if ( array_pop( $tr_history ) ) {
  878. $line = '</tr>';
  879. }
  880. if ( array_pop( $td_history ) ) {
  881. $line = "</{$last_tag}>{$line}";
  882. }
  883. $outLine = $line;
  884. array_push( $tr_history , false );
  885. array_push( $td_history , false );
  886. array_push( $last_tag_history , '' );
  887. } elseif ( $first_character === '|' || $first_character === '!' || substr( $line , 0 , 2 ) === '|+' ) {
  888. # This might be cell elements, td, th or captions
  889. if ( substr( $line , 0 , 2 ) === '|+' ) {
  890. $first_character = '+';
  891. $line = substr( $line , 1 );
  892. }
  893. $line = substr( $line , 1 );
  894. if ( $first_character === '!' ) {
  895. $line = str_replace( '!!' , '||' , $line );
  896. }
  897. # Split up multiple cells on the same line.
  898. # FIXME : This can result in improper nesting of tags processed
  899. # by earlier parser steps, but should avoid splitting up eg
  900. # attribute values containing literal "||".
  901. $cells = StringUtils::explodeMarkup( '||' , $line );
  902. $outLine = '';
  903. # Loop through each table cell
  904. foreach ( $cells as $cell ) {
  905. $previous = '';
  906. if ( $first_character !== '+' ) {
  907. $tr_after = array_pop( $tr_attributes );
  908. if ( !array_pop( $tr_history ) ) {
  909. $previous = "<tr{$tr_after}>\n";
  910. }
  911. array_push( $tr_history , true );
  912. array_push( $tr_attributes , '' );
  913. array_pop( $has_opened_tr );
  914. array_push( $has_opened_tr , true );
  915. }
  916. $last_tag = array_pop( $last_tag_history );
  917. if ( array_pop( $td_history ) ) {
  918. $previous = "</{$last_tag}>\n{$previous}";
  919. }
  920. if ( $first_character === '|' ) {
  921. $last_tag = 'td';
  922. } elseif ( $first_character === '!' ) {
  923. $last_tag = 'th';
  924. } elseif ( $first_character === '+' ) {
  925. $last_tag = 'caption';
  926. } else {
  927. $last_tag = '';
  928. }
  929. array_push( $last_tag_history , $last_tag );
  930. # A cell could contain both parameters and data
  931. $cell_data = explode( '|' , $cell , 2 );
  932. # Bug 553: Note that a '|' inside an invalid link should not
  933. # be mistaken as delimiting cell parameters
  934. if ( strpos( $cell_data[0], '[[' ) !== false ) {
  935. $cell = "{$previous}<{$last_tag}>{$cell}";
  936. } elseif ( count( $cell_data ) == 1 ) {
  937. $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
  938. } else {
  939. $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
  940. $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
  941. $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
  942. }
  943. $outLine .= $cell;
  944. array_push( $td_history , true );
  945. }
  946. }
  947. $out .= $outLine . "\n";
  948. }
  949. # Closing open td, tr && table
  950. while ( count( $td_history ) > 0 ) {
  951. if ( array_pop( $td_history ) ) {
  952. $out .= "</td>\n";
  953. }
  954. if ( array_pop( $tr_history ) ) {
  955. $out .= "</tr>\n";
  956. }
  957. if ( !array_pop( $has_opened_tr ) ) {
  958. $out .= "<tr><td></td></tr>\n" ;
  959. }
  960. $out .= "</table>\n";
  961. }
  962. # Remove trailing line-ending (b/c)
  963. if ( substr( $out, -1 ) === "\n" ) {
  964. $out = substr( $out, 0, -1 );
  965. }
  966. # special case: don't return empty table
  967. if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
  968. $out = '';
  969. }
  970. wfProfileOut( __METHOD__ );
  971. return $out;
  972. }
  973. /**
  974. * Helper function for parse() that transforms wiki markup into
  975. * HTML. Only called for $mOutputType == self::OT_HTML.
  976. *
  977. * @private
  978. *
  979. * @param $text string
  980. * @param $isMain bool
  981. * @param $frame bool
  982. *
  983. * @return string
  984. */
  985. function internalParse( $text, $isMain = true, $frame = false ) {
  986. wfProfileIn( __METHOD__ );
  987. $origText = $text;
  988. # Hook to suspend the parser in this state
  989. if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
  990. wfProfileOut( __METHOD__ );
  991. return $text ;
  992. }
  993. # if $frame is provided, then use $frame for replacing any variables
  994. if ( $frame ) {
  995. # use frame depth to infer how include/noinclude tags should be handled
  996. # depth=0 means this is the top-level document; otherwise it's an included document
  997. if ( !$frame->depth ) {
  998. $flag = 0;
  999. } else {
  1000. $flag = Parser::PTD_FOR_INCLUSION;
  1001. }
  1002. $dom = $this->preprocessToDom( $text, $flag );
  1003. $text = $frame->expand( $dom );
  1004. } else {
  1005. # if $frame is not provided, then use old-style replaceVariables
  1006. $text = $this->replaceVariables( $text );
  1007. }
  1008. wfRunHooks( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) );
  1009. $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
  1010. wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
  1011. # Tables need to come after variable replacement for things to work
  1012. # properly; putting them before other transformations should keep
  1013. # exciting things like link expansions from showing up in surprising
  1014. # places.
  1015. $text = $this->doTableStuff( $text );
  1016. $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
  1017. $text = $this->doDoubleUnderscore( $text );
  1018. $text = $this->doHeadings( $text );
  1019. if ( $this->mOptions->getUseDynamicDates() ) {
  1020. $df = DateFormatter::getInstance();
  1021. $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
  1022. }
  1023. $text = $this->replaceInternalLinks( $text );
  1024. $text = $this->doAllQuotes( $text );
  1025. $text = $this->replaceExternalLinks( $text );
  1026. # replaceInternalLinks may sometimes leave behind
  1027. # absolute URLs, which have to be masked to hide them from replaceExternalLinks
  1028. $text = str_replace( $this->mUniqPrefix.'NOPARSE', '', $text );
  1029. $text = $this->doMagicLinks( $text );
  1030. $text = $this->formatHeadings( $text, $origText, $isMain );
  1031. wfProfileOut( __METHOD__ );
  1032. return $text;
  1033. }
  1034. /**
  1035. * Replace special strings like "ISBN xxx" and "RFC xxx" with
  1036. * magic external links.
  1037. *
  1038. * DML
  1039. * @private
  1040. *
  1041. * @param $text string
  1042. *
  1043. * @return string
  1044. */
  1045. function doMagicLinks( $text ) {
  1046. wfProfileIn( __METHOD__ );
  1047. $prots = wfUrlProtocolsWithoutProtRel();
  1048. $urlChar = self::EXT_LINK_URL_CLASS;
  1049. $text = preg_replace_callback(
  1050. '!(?: # Start cases
  1051. (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
  1052. (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
  1053. (\\b(?i:$prots)$urlChar+) | # m[3]: Free external links" . '
  1054. (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number
  1055. ISBN\s+(\b # m[5]: ISBN, capture number
  1056. (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
  1057. (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters
  1058. [0-9Xx] # check digit
  1059. \b)
  1060. )!xu', array( &$this, 'magicLinkCallback' ), $text );
  1061. wfProfileOut( __METHOD__ );
  1062. return $text;
  1063. }
  1064. /**
  1065. * @throws MWException
  1066. * @param $m array
  1067. * @return HTML|string
  1068. */
  1069. function magicLinkCallback( $m ) {
  1070. if ( isset( $m[1] ) && $m[1] !== '' ) {
  1071. # Skip anchor
  1072. return $m[0];
  1073. } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
  1074. # Skip HTML element
  1075. return $m[0];
  1076. } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
  1077. # Free external link
  1078. return $this->makeFreeExternalLink( $m[0] );
  1079. } elseif ( isset( $m[4] ) && $m[4] !== '' ) {
  1080. # RFC or PMID
  1081. if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
  1082. $keyword = 'RFC';
  1083. $urlmsg = 'rfcurl';
  1084. $CssClass = 'mw-magiclink-rfc';
  1085. $id = $m[4];
  1086. } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
  1087. $keyword = 'PMID';
  1088. $urlmsg = 'pubmedurl';
  1089. $CssClass = 'mw-magiclink-pmid';
  1090. $id = $m[4];
  1091. } else {
  1092. throw new MWException( __METHOD__.': unrecognised match type "' .
  1093. substr( $m[0], 0, 20 ) . '"' );
  1094. }
  1095. $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
  1096. return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $CssClass );
  1097. } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
  1098. # ISBN
  1099. $isbn = $m[5];
  1100. $num = strtr( $isbn, array(
  1101. '-' => '',
  1102. ' ' => '',
  1103. 'x' => 'X',
  1104. ));
  1105. $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
  1106. return'<a href="' .
  1107. htmlspecialchars( $titleObj->getLocalUrl() ) .
  1108. "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
  1109. } else {
  1110. return $m[0];
  1111. }
  1112. }
  1113. /**
  1114. * Make a free external link, given a user-supplied URL
  1115. *
  1116. * @param $url string
  1117. *
  1118. * @return string HTML
  1119. * @private
  1120. */
  1121. function makeFreeExternalLink( $url ) {
  1122. wfProfileIn( __METHOD__ );
  1123. $trail = '';
  1124. # The characters '<' and '>' (which were escaped by
  1125. # removeHTMLtags()) should not be included in
  1126. # URLs, per RFC 2396.
  1127. $m2 = array();
  1128. if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
  1129. $trail = substr( $url, $m2[0][1] ) . $trail;
  1130. $url = substr( $url, 0, $m2[0][1] );
  1131. }
  1132. # Move trailing punctuation to $trail
  1133. $sep = ',;\.:!?';
  1134. # If there is no left bracket, then consider right brackets fair game too
  1135. if ( strpos( $url, '(' ) === false ) {
  1136. $sep .= ')';
  1137. }
  1138. $numSepChars = strspn( strrev( $url ), $sep );
  1139. if ( $numSepChars ) {
  1140. $trail = substr( $url, -$numSepChars ) . $trail;
  1141. $url = substr( $url, 0, -$numSepChars );
  1142. }
  1143. $url = Sanitizer::cleanUrl( $url );
  1144. # Is this an external image?
  1145. $text = $this->maybeMakeExternalImage( $url );
  1146. if ( $text === false ) {
  1147. # Not an image, make a link
  1148. $text = Linker::makeExternalLink( $url,
  1149. $this->getConverterLanguage()->markNoConversion($url), true, 'free',
  1150. $this->getExternalLinkAttribs( $url ) );
  1151. # Register it in the output object...
  1152. # Replace unnecessary URL escape codes with their equivalent characters
  1153. $pasteurized = self::replaceUnusualEscapes( $url );
  1154. $this->mOutput->addExternalLink( $pasteurized );
  1155. }
  1156. wfProfileOut( __METHOD__ );
  1157. return $text . $trail;
  1158. }
  1159. /**
  1160. * Parse headers and return html
  1161. *
  1162. * @private
  1163. *
  1164. * @param $text string
  1165. *
  1166. * @return string
  1167. */
  1168. function doHeadings( $text ) {
  1169. wfProfileIn( __METHOD__ );
  1170. for ( $i = 6; $i >= 1; --$i ) {
  1171. $h = str_repeat( '=', $i );
  1172. $text = preg_replace( "/^$h(.+)$h\\s*$/m",
  1173. "<h$i>\\1</h$i>", $text );
  1174. }
  1175. wfProfileOut( __METHOD__ );
  1176. return $text;
  1177. }
  1178. /**
  1179. * Replace single quotes with HTML markup
  1180. * @private
  1181. *
  1182. * @param $text string
  1183. *
  1184. * @return string the altered text
  1185. */
  1186. function doAllQuotes( $text ) {
  1187. wfProfileIn( __METHOD__ );
  1188. $outtext = '';
  1189. $lines = StringUtils::explode( "\n", $text );
  1190. foreach ( $lines as $line ) {
  1191. $outtext .= $this->doQuotes( $line ) . "\n";
  1192. }
  1193. $outtext = substr( $outtext, 0,-1 );
  1194. wfProfileOut( __METHOD__ );
  1195. return $outtext;
  1196. }
  1197. /**
  1198. * Helper function for doAllQuotes()
  1199. *
  1200. * @param $text string
  1201. *
  1202. * @return string
  1203. */
  1204. public function doQuotes( $text ) {
  1205. $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
  1206. if ( count( $arr ) == 1 ) {
  1207. return $text;
  1208. } else {
  1209. # First, do some preliminary work. This may shift some apostrophes from
  1210. # being mark-up to being text. It also counts the number of occurrences
  1211. # of bold and italics mark-ups.
  1212. $numbold = 0;
  1213. $numitalics = 0;
  1214. for ( $i = 0; $i < count( $arr ); $i++ ) {
  1215. if ( ( $i % 2 ) == 1 ) {
  1216. # If there are ever four apostrophes, assume the first is supposed to
  1217. # be text, and the remaining three constitute mark-up for bold text.
  1218. if ( strlen( $arr[$i] ) == 4 ) {
  1219. $arr[$i-1] .= "'";
  1220. $arr[$i] = "'''";
  1221. } elseif ( strlen( $arr[$i] ) > 5 ) {
  1222. # If there are more than 5 apostrophes in a row, assume they're all
  1223. # text except for the last 5.
  1224. $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
  1225. $arr[$i] = "'''''";
  1226. }
  1227. # Count the number of occurrences of bold and italics mark-ups.
  1228. # We are not counting sequences of five apostrophes.
  1229. if ( strlen( $arr[$i] ) == 2 ) {
  1230. $numitalics++;
  1231. } elseif ( strlen( $arr[$i] ) == 3 ) {
  1232. $numbold++;
  1233. } elseif ( strlen( $arr[$i] ) == 5 ) {
  1234. $numitalics++;
  1235. $numbold++;
  1236. }
  1237. }
  1238. }
  1239. # If there is an odd number of both bold and italics, it is likely
  1240. # that one of the bold ones was meant to be an apostrophe followed
  1241. # by italics. Which one we cannot know for certain, but it is more
  1242. # likely to be one that has a single-letter word before it.
  1243. if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
  1244. $i = 0;
  1245. $firstsingleletterword = -1;
  1246. $firstmultiletterword = -1;
  1247. $firstspace = -1;
  1248. foreach ( $arr as $r ) {
  1249. if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) ) {
  1250. $x1 = substr( $arr[$i-1], -1 );
  1251. $x2 = substr( $arr[$i-1], -2, 1 );
  1252. if ( $x1 === ' ' ) {
  1253. if ( $firstspace == -1 ) {
  1254. $firstspace = $i;
  1255. }
  1256. } elseif ( $x2 === ' ') {
  1257. if ( $firstsingleletterword == -1 ) {
  1258. $firstsingleletterword = $i;
  1259. }
  1260. } else {
  1261. if ( $firstmultiletterword == -1 ) {
  1262. $firstmultiletterword = $i;
  1263. }
  1264. }
  1265. }
  1266. $i++;
  1267. }
  1268. # If there is a single-letter word, use it!
  1269. if ( $firstsingleletterword > -1 ) {
  1270. $arr[$firstsingleletterword] = "''";
  1271. $arr[$firstsingleletterword-1] .= "'";
  1272. } elseif ( $firstmultiletterword > -1 ) {
  1273. # If not, but there's a multi-letter word, use that one.
  1274. $arr[$firstmultiletterword] = "''";
  1275. $arr[$firstmultiletterword-1] .= "'";
  1276. } elseif ( $firstspace > -1 ) {
  1277. # ... otherwise use the first one that has neither.
  1278. # (notice that it is possible for all three to be -1 if, for example,
  1279. # there is only one pentuple-apostrophe in the line)
  1280. $arr[$firstspace] = "''";
  1281. $arr[$firstspace-1] .= "'";
  1282. }
  1283. }
  1284. # Now let's actually convert our apostrophic mush to HTML!
  1285. $output = '';
  1286. $buffer = '';
  1287. $state = '';
  1288. $i = 0;
  1289. foreach ( $arr as $r ) {
  1290. if ( ( $i % 2 ) == 0 ) {
  1291. if ( $state === 'both' ) {
  1292. $buffer .= $r;
  1293. } else {
  1294. $output .= $r;
  1295. }
  1296. } else {
  1297. if ( strlen( $r ) == 2 ) {
  1298. if ( $state === 'i' ) {
  1299. $output .= '</i>'; $state = '';
  1300. } elseif ( $state === 'bi' ) {
  1301. $output .= '</i>'; $state = 'b';
  1302. } elseif ( $state === 'ib' ) {
  1303. $output .= '</b></i><b>'; $state = 'b';
  1304. } elseif ( $state === 'both' ) {
  1305. $output .= '<b><i>'.$buffer.'</i>'; $state = 'b';
  1306. } else { # $state can be 'b' or ''
  1307. $output .= '<i>'; $state .= 'i';
  1308. }
  1309. } elseif ( strlen( $r ) == 3 ) {
  1310. if ( $state === 'b' ) {
  1311. $output .= '</b>'; $state = '';
  1312. } elseif ( $state === 'bi' ) {
  1313. $output .= '</i></b><i>'; $state = 'i';
  1314. } elseif ( $state === 'ib' ) {
  1315. $output .= '</b>'; $state = 'i';
  1316. } elseif ( $state === 'both' ) {
  1317. $output .= '<i><b>'.$buffer.'</b>'; $state = 'i';
  1318. } else { # $state can be 'i' or ''
  1319. $output .= '<b>'; $state .= 'b';
  1320. }
  1321. } elseif ( strlen( $r ) == 5 ) {
  1322. if ( $state === 'b' ) {
  1323. $output .= '</b><i>'; $state = 'i';
  1324. } elseif ( $state === 'i' ) {
  1325. $output .= '</i><b>'; $state = 'b';
  1326. } elseif ( $state === 'bi' ) {
  1327. $output .= '</i></b>'; $state = '';
  1328. } elseif ( $state === 'ib' ) {
  1329. $output .= '</b></i>'; $state = '';
  1330. } elseif ( $state === 'both' ) {
  1331. $output .= '<i><b>'.$buffer.'</b></i>'; $state = '';
  1332. } else { # ($state == '')
  1333. $buffer = ''; $state = 'both';
  1334. }
  1335. }
  1336. }
  1337. $i++;
  1338. }
  1339. # Now close all remaining tags. Notice that the order is important.
  1340. if ( $state === 'b' || $state === 'ib' ) {
  1341. $output .= '</b>';
  1342. }
  1343. if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
  1344. $output .= '</i>';
  1345. }
  1346. if ( $state === 'bi' ) {
  1347. $output .= '</b>';
  1348. }
  1349. # There might be lonely ''''', so make sure we have a buffer
  1350. if ( $state === 'both' && $buffer ) {
  1351. $output .= '<b><i>'.$buffer.'</i></b>';
  1352. }
  1353. return $output;
  1354. }
  1355. }
  1356. /**
  1357. * Replace external links (REL)
  1358. *
  1359. * Note: this is all very hackish and the order of execution matters a lot.
  1360. * Make sure to run maintenance/parserTests.php if you change this code.
  1361. *
  1362. * @private
  1363. *
  1364. * @param $text string
  1365. *
  1366. * @return string
  1367. */
  1368. function replaceExternalLinks( $text ) {
  1369. wfProfileIn( __METHOD__ );
  1370. $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
  1371. if ( $bits === false ) {
  1372. throw new MWException( "PCRE needs to be compiled with --enable-unicode-properties in order for MediaWiki to function" );
  1373. }
  1374. $s = array_shift( $bits );
  1375. $i = 0;
  1376. while ( $i<count( $bits ) ) {
  1377. $url = $bits[$i++];
  1378. $protocol = $bits[$i++];
  1379. $text = $bits[$i++];
  1380. $trail = $bits[$i++];
  1381. # The characters '<' and '>' (which were escaped by
  1382. # removeHTMLtags()) should not be included in
  1383. # URLs, per RFC 2396.
  1384. $m2 = array();
  1385. if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
  1386. $text = substr( $url, $m2[0][1] ) . ' ' . $text;
  1387. $url = substr( $url, 0, $m2[0][1] );
  1388. }
  1389. # If the link text is an image URL, replace it with an <img> tag
  1390. # This happened by accident in the original parser, but some people used it extensively
  1391. $img = $this->maybeMakeExternalImage( $text );
  1392. if ( $img !== false ) {
  1393. $text = $img;
  1394. }
  1395. $dtrail = '';
  1396. # Set linktype for CSS - if URL==text, link is essentially free
  1397. $linktype = ( $text === $url ) ? 'free' : 'text';
  1398. # No link text, e.g. [http://domain.tld/some.link]
  1399. if ( $text == '' ) {
  1400. # Autonumber
  1401. $langObj = $this->getTargetLanguage();
  1402. $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
  1403. $linktype = 'autonumber';
  1404. } else {
  1405. # Have link text, e.g. [http://domain.tld/some.link text]s
  1406. # Check for trail
  1407. list( $dtrail, $trail ) = Linker::splitTrail( $trail );
  1408. }
  1409. $text = $this->getConverterLanguage()->markNoConversion( $text );
  1410. $url = Sanitizer::cleanUrl( $url );
  1411. # Use the encoded URL
  1412. # This means that users can paste URLs directly into the text
  1413. # Funny characters like ö aren't valid in URLs anyway
  1414. # This was changed in August 2004
  1415. $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
  1416. $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
  1417. # Register link in the output object.
  1418. # Replace unnecessary URL escape codes with the referenced character
  1419. # This prevents spammers from hiding links from the filters
  1420. $pasteurized = self::replaceUnusualEscapes( $url );
  1421. $this->mOutput->addExternalLink( $pasteurized );
  1422. }
  1423. wfProfileOut( __METHOD__ );
  1424. return $s;
  1425. }
  1426. /**
  1427. * Get an associative array of additional HTML attributes appropriate for a
  1428. * particular external link. This currently may include rel => nofollow
  1429. * (depending on configuration, namespace, and the URL's domain) and/or a
  1430. * target attribute (depending on configuration).
  1431. *
  1432. * @param $url String|bool optional URL, to extract the domain from for rel =>
  1433. * nofollow if appropriate
  1434. * @return Array associative array of HTML attributes
  1435. */
  1436. function getExternalLinkAttribs( $url = false ) {
  1437. $attribs = array();
  1438. global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
  1439. $ns = $this->mTitle->getNamespace();
  1440. if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) &&
  1441. !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions ) )
  1442. {
  1443. $attribs['rel'] = 'nofollow';
  1444. }
  1445. if ( $this->mOptions->getExternalLinkTarget() ) {
  1446. $attribs['target'] = $this->mOptions->getExternalLinkTarget();
  1447. }
  1448. return $attribs;
  1449. }
  1450. /**
  1451. * Replace unusual URL escape codes with their equivalent characters
  1452. *
  1453. * @param $url String
  1454. * @return String
  1455. *
  1456. * @todo This can merge genuinely required bits in the path or query string,
  1457. * breaking legit URLs. A proper fix would treat the various parts of
  1458. * the URL differently; as a workaround, just use the output for
  1459. * statistical records, not for actual linking/output.
  1460. */
  1461. static function replaceUnusualEscapes( $url ) {
  1462. return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
  1463. array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
  1464. }
  1465. /**
  1466. * Callback function used in replaceUnusualEscapes().
  1467. * Replaces unusual URL escape codes with their equivalent character
  1468. *
  1469. * @param $matches array
  1470. *
  1471. * @return string
  1472. */
  1473. private static function replaceUnusualEscapesCallback( $matches ) {
  1474. $char = urldecode( $matches[0] );
  1475. $ord = ord( $char );
  1476. # Is it an unsafe or HTTP reserved character according to RFC 1738?
  1477. if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
  1478. # No, shouldn't be escaped
  1479. return $char;
  1480. } else {
  1481. # Yes, leave it escaped
  1482. return $matches[0];
  1483. }
  1484. }
  1485. /**
  1486. * make an image if it's allowed, either through the global
  1487. * option, through th…

Large files files are truncated, but you can click here to view the full file