Parser_LinkHooks.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. <?php
  2. /**
  3. * Parser with LinkHooks experiment
  4. * @ingroup Parser
  5. */
  6. class Parser_LinkHooks extends Parser
  7. {
  8. /**
  9. * Update this version number when the ParserOutput format
  10. * changes in an incompatible way, so the parser cache
  11. * can automatically discard old data.
  12. */
  13. const VERSION = '1.6.4';
  14. # Flags for Parser::setLinkHook
  15. # Also available as global constants from Defines.php
  16. const SLH_PATTERN = 1;
  17. # Constants needed for external link processing
  18. # Everything except bracket, space, or control characters
  19. const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
  20. const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
  21. \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
  22. /**#@+
  23. * @private
  24. */
  25. # Persistent:
  26. var $mLinkHooks;
  27. /**#@-*/
  28. /**
  29. * Constructor
  30. *
  31. * @public
  32. */
  33. function __construct( $conf = array() ) {
  34. parent::__construct( $conf );
  35. $this->mLinkHooks = array();
  36. }
  37. /**
  38. * Do various kinds of initialisation on the first call of the parser
  39. */
  40. function firstCallInit() {
  41. parent::__construct();
  42. if ( !$this->mFirstCall ) {
  43. return;
  44. }
  45. $this->mFirstCall = false;
  46. wfProfileIn( __METHOD__ );
  47. $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
  48. CoreParserFunctions::register( $this );
  49. CoreLinkFunctions::register( $this );
  50. $this->initialiseVariables();
  51. wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
  52. wfProfileOut( __METHOD__ );
  53. }
  54. /**
  55. * Create a link hook, e.g. [[Namepsace:...|display}}
  56. * The callback function should have the form:
  57. * function myLinkCallback( $parser, $holders, $markers,
  58. * Title $title, $titleText, &$sortText = null, &$leadingColon = false ) { ... }
  59. *
  60. * Or with SLH_PATTERN:
  61. * function myLinkCallback( $parser, $holders, $markers, )
  62. * &$titleText, &$sortText = null, &$leadingColon = false ) { ... }
  63. *
  64. * The callback may either return a number of different possible values:
  65. * String) Text result of the link
  66. * True) (Treat as link) Parse the link according to normal link rules
  67. * False) (Bad link) Just output the raw wikitext (You may modify the text first)
  68. *
  69. * @public
  70. *
  71. * @param integer|string $ns The Namespace ID or regex pattern if SLH_PATTERN is set
  72. * @param mixed $callback The callback function (and object) to use
  73. * @param integer $flags a combination of the following flags:
  74. * SLH_PATTERN Use a regex link pattern rather than a namespace
  75. *
  76. * @return The old callback function for this name, if any
  77. */
  78. function setLinkHook( $ns, $callback, $flags = 0 ) {
  79. if( $flags & SLH_PATTERN && !is_string($ns) )
  80. throw new MWException( __METHOD__.'() expecting a regex string pattern.' );
  81. elseif( $flags | ~SLH_PATTERN && !is_int($ns) )
  82. throw new MWException( __METHOD__.'() expecting a namespace index.' );
  83. $oldVal = isset( $this->mLinkHooks[$ns] ) ? $this->mLinkHooks[$ns][0] : null;
  84. $this->mLinkHooks[$ns] = array( $callback, $flags );
  85. return $oldVal;
  86. }
  87. /**
  88. * Get all registered link hook identifiers
  89. *
  90. * @return array
  91. */
  92. function getLinkHooks() {
  93. return array_keys( $this->mLinkHooks );
  94. }
  95. /**
  96. * Process [[ ]] wikilinks
  97. * @return LinkHolderArray
  98. *
  99. * @private
  100. */
  101. function replaceInternalLinks2( &$s ) {
  102. global $wgContLang;
  103. wfProfileIn( __METHOD__ );
  104. wfProfileIn( __METHOD__.'-setup' );
  105. static $tc = FALSE, $titleRegex;//$e1, $e1_img;
  106. if( !$tc ) {
  107. # the % is needed to support urlencoded titles as well
  108. $tc = Title::legalChars() . '#%';
  109. # Match a link having the form [[namespace:link|alternate]]trail
  110. //$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
  111. # Match cases where there is no "]]", which might still be images
  112. //$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
  113. # Match a valid plain title
  114. $titleRegex = "/^([{$tc}]+)$/sD";
  115. }
  116. $sk = $this->mOptions->getSkin();
  117. $holders = new LinkHolderArray( $this );
  118. if( is_null( $this->mTitle ) ) {
  119. wfProfileOut( __METHOD__ );
  120. wfProfileOut( __METHOD__.'-setup' );
  121. throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
  122. }
  123. $nottalk = !$this->mTitle->isTalkPage();
  124. if($wgContLang->hasVariants()) {
  125. $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
  126. } else {
  127. $selflink = array($this->mTitle->getPrefixedText());
  128. }
  129. wfProfileOut( __METHOD__.'-setup' );
  130. $offset = 0;
  131. $offsetStack = array();
  132. $markers = new LinkMarkerReplacer( $this, $holders, array( &$this, 'replaceInternalLinksCallback' ) );
  133. while( true ) {
  134. $startBracketOffset = strpos( $s, '[[', $offset );
  135. $endBracketOffset = strpos( $s, ']]', $offset );
  136. # Finish when there are no more brackets
  137. if( $startBracketOffset === false && $endBracketOffset === false ) break;
  138. # Determine if the bracket is a starting or ending bracket
  139. # When we find both, use the first one
  140. elseif( $startBracketOffset !== false && $endBracketOffset !== false )
  141. $isStart = $startBracketOffset <= $endBracketOffset;
  142. # When we only found one, check which it is
  143. else $isStart = $startBracketOffset !== false;
  144. $bracketOffset = $isStart ? $startBracketOffset : $endBracketOffset;
  145. if( $isStart ) {
  146. /** Opening bracket **/
  147. # Just push our current offset in the string onto the stack
  148. $offsetStack[] = $startBracketOffset;
  149. } else {
  150. /** Closing bracket **/
  151. # Pop the start pos for our current link zone off the stack
  152. $startBracketOffset = array_pop($offsetStack);
  153. # Just to clean up the code, lets place offsets on the outer ends
  154. $endBracketOffset += 2;
  155. # Only do logic if we actually have a opening bracket for this
  156. if( isset($startBracketOffset) ) {
  157. # Extract text inside the link
  158. @list( $titleText, $paramText ) = explode('|',
  159. substr($s, $startBracketOffset+2, $endBracketOffset-$startBracketOffset-4), 2);
  160. # Create markers only for valid links
  161. if( preg_match( $titleRegex, $titleText ) ) {
  162. # Store the text for the marker
  163. $marker = $markers->addMarker($titleText, $paramText);
  164. # Replace the current link with the marker
  165. $s = substr($s,0,$startBracketOffset).
  166. $marker.
  167. substr($s, $endBracketOffset);
  168. # We have modified $s, because of this we need to set the
  169. # offset manually since the end position is different now
  170. $offset = $startBracketOffset+strlen($marker);
  171. continue;
  172. }
  173. # ToDo: Some LinkHooks may allow recursive links inside of
  174. # the link text, create a regex that also matches our
  175. # <!-- LINKMARKER ### --> sequence in titles
  176. # ToDO: Some LinkHooks use patterns rather than namespaces
  177. # these need to be tested at this point here
  178. }
  179. }
  180. # Bump our offset to after our current bracket
  181. $offset = $bracketOffset+2;
  182. }
  183. # Now expand our tree
  184. wfProfileIn( __METHOD__.'-expand' );
  185. $s = $markers->expand( $s );
  186. wfProfileOut( __METHOD__.'-expand' );
  187. wfProfileOut( __METHOD__ );
  188. return $holders;
  189. }
  190. function replaceInternalLinksCallback( $parser, $holders, $markers, $titleText, $paramText ) {
  191. wfProfileIn( __METHOD__ );
  192. $wt = isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
  193. wfProfileIn( __METHOD__."-misc" );
  194. # Don't allow internal links to pages containing
  195. # PROTO: where PROTO is a valid URL protocol; these
  196. # should be external links.
  197. if( preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $titleText) ) {
  198. wfProfileOut( __METHOD__ );
  199. return $wt;
  200. }
  201. # Make subpage if necessary
  202. if( $this->areSubpagesAllowed() ) {
  203. $titleText = $this->maybeDoSubpageLink( $titleText, $paramText );
  204. }
  205. # Check for a leading colon and strip it if it is there
  206. $leadingColon = $titleText[0] == ':';
  207. if( $leadingColon ) $titleText = substr( $titleText, 1 );
  208. wfProfileOut( __METHOD__."-misc" );
  209. # Make title object
  210. wfProfileIn( __METHOD__."-title" );
  211. $title = Title::newFromText( $this->mStripState->unstripNoWiki($titleText) );
  212. if( !$title ) {
  213. wfProfileOut( __METHOD__."-title" );
  214. wfProfileOut( __METHOD__ );
  215. return $wt;
  216. }
  217. $ns = $title->getNamespace();
  218. wfProfileOut( __METHOD__."-title" );
  219. # Default for Namespaces is a default link
  220. # ToDo: Default for patterns is plain wikitext
  221. $return = true;
  222. if( isset($this->mLinkHooks[$ns]) ) {
  223. list( $callback, $flags ) = $this->mLinkHooks[$ns];
  224. if( $flags & SLH_PATTERN ) {
  225. $args = array( $parser, $holders, $markers, $titleText, &$paramText, &$leadingColon );
  226. } else {
  227. $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon );
  228. }
  229. # Workaround for PHP bug 35229 and similar
  230. if ( !is_callable( $callback ) ) {
  231. throw new MWException( "Tag hook for $name is not callable\n" );
  232. }
  233. $return = call_user_func_array( $callback, $args );
  234. }
  235. if( $return === true ) {
  236. # True (treat as plain link) was returned, call the defaultLinkHook
  237. $args = array( $parser, $holders, $markers, $title, $titleText, &$paramText, &$leadingColon );
  238. $return = call_user_func_array( array( 'CoreLinkFunctions', 'defaultLinkHook' ), $args );
  239. }
  240. if( $return === false ) {
  241. # False (no link) was returned, output plain wikitext
  242. # Build it again as the hook is allowed to modify $paramText
  243. return isset($paramText) ? "[[$titleText|$paramText]]" : "[[$titleText]]";
  244. }
  245. # Content was returned, return it
  246. return $return;
  247. }
  248. }
  249. class LinkMarkerReplacer {
  250. protected $markers, $nextId, $parser, $holders, $callback;
  251. function __construct( $parser, $holders, $callback ) {
  252. $this->nextId = 0;
  253. $this->markers = array();
  254. $this->parser = $parser;
  255. $this->holders = $holders;
  256. $this->callback = $callback;
  257. }
  258. function addMarker($titleText, $paramText) {
  259. $id = $this->nextId++;
  260. $this->markers[$id] = array( $titleText, $paramText );
  261. return "<!-- LINKMARKER $id -->";
  262. }
  263. function findMarker( $string ) {
  264. return (bool) preg_match('/<!-- LINKMARKER [0-9]+ -->/', $string );
  265. }
  266. function expand( $string ) {
  267. return StringUtils::delimiterReplaceCallback( "<!-- LINKMARKER ", " -->", array( &$this, 'callback' ), $string );
  268. }
  269. function callback( $m ) {
  270. $id = intval($m[1]);
  271. if( !array_key_exists($id, $this->markers) ) return $m[0];
  272. $args = $this->markers[$id];
  273. array_unshift( $args, $this );
  274. array_unshift( $args, $this->holders );
  275. array_unshift( $args, $this->parser );
  276. return call_user_func_array( $this->callback, $args );
  277. }
  278. }