PageRenderTime 62ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/mods/bbcode/api.php

https://bitbucket.org/webop/webop-forum
PHP | 1362 lines | 827 code | 120 blank | 415 comment | 170 complexity | 2ac8616dbe5eb28be66650a54dc4d36c MD5 | raw file
Possible License(s): LGPL-2.1

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

  1. <?php
  2. ////////////////////////////////////////////////////////////////////////////////
  3. // //
  4. // Copyright (C) 2010 Phorum Development Team //
  5. // http://www.phorum.org //
  6. // //
  7. // This program is free software. You can redistribute it and/or modify //
  8. // it under the terms of either the current Phorum License (viewable at //
  9. // phorum.org) or the Phorum License that was distributed with this file //
  10. // //
  11. // This program is distributed in the hope that it will be useful, //
  12. // but WITHOUT ANY WARRANTY, without even the implied warranty of //
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. //
  14. // //
  15. // You should have received a copy of the Phorum License //
  16. // along with this program. //
  17. // //
  18. ////////////////////////////////////////////////////////////////////////////////
  19. if (!defined('PHORUM')) return;
  20. /**
  21. * This file holds the core parsing and rendering code for doing
  22. * Phorum BBcode processing. The parsing is split into separate functions,
  23. * to make it possible to cache parts of the processing steps. The steps
  24. * that are implemented are:
  25. *
  26. * - gathering information about all enabled tags;
  27. * - turning this tag information into parsing trees and tag information;
  28. * - splitting a text into bbcode tokens;
  29. * - rendering bbcode tokens into HTML.
  30. *
  31. * The first two steps are combined in the function
  32. * {@link bbcode_api_initparser()}. This function will rebuild the
  33. * tag information and parse tree when necessary by calling the
  34. * {@link bbcode_api_buildparser} function. The third step is
  35. * implemented by {@link bbcode_api_tokenize()}. The final fourth step
  36. * is handled by {@link bbcode_api_render()}.
  37. *
  38. * @todo Implement a good parsing rule for tags that take the inner content
  39. * and put that inside a new HTML tag. Those tags should not allow
  40. * nested BBcode tags when the content is used. It is no problem when
  41. * a bbcode argument is used, so that should still be allowed. Because
  42. * this sounds terribly confusing, here are some examples for the
  43. * distinct cases:
  44. *
  45. * [url]http://some.url[tag]nested data[/tag][/url]
  46. * For this case, the parser would turn [tag]nested data[/tag] into
  47. * <some>html tag</some>. Then, the parser would see:
  48. * [url]http://some.url<some>html tag</some>[/url]
  49. * When using this content for <a href="...">, this would introduce
  50. * HTML code inside the href, possibly opening options for XSS.
  51. *
  52. * [url=http://some.url][tag]nested data[/tag][/url]
  53. * This case is okay. The content here is put between <a href> and </a>
  54. * in the rendered output. This case should still be valid after
  55. * implementing the new parsing rule.
  56. */
  57. // --------------------------------------------------------------------
  58. // Constants for the BBcode tag descriptions
  59. // --------------------------------------------------------------------
  60. /**
  61. * BBcode tag description field: a description (HTML format) for the tag,
  62. * for displaying in the BBcode module settings screen. This can also show an
  63. * example use case for clarifying the exact use to the admin.
  64. */
  65. define('BBCODE_INFO_DESCRIPTION', 1);
  66. /**
  67. * BBcode tag description field: whether there is an editor tool available
  68. * (supported by the Editor Tools module) for the tag, which can be enabled
  69. * in the module settings screen.
  70. */
  71. define('BBCODE_INFO_HASEDITORTOOL', 2);
  72. /**
  73. * BBcode tag description field: the default setting to use for the tag in
  74. * the BBcode module settings screen. Possible values to use are:
  75. * 0) the tag is disabled, 1) the tag is enabled, but not the editor tool
  76. * for the tag, 2) both the tag and its editor tool are enabled.
  77. */
  78. define('BBCODE_INFO_DEFAULTSTATE', 3);
  79. /**
  80. * BBcode tag description field: an array of allowed arguments.
  81. * The keys in this array are the arguments that are available.
  82. * The values are the default values to use for the arguments.
  83. * If a tag is implemented that allows for assigning a value to the
  84. * tag name, then this tag name needs to be added to the argument array.
  85. * For example the [url] tag, which can look like [url=http://www.example.com]
  86. * needs the argument array ('url' => '') to allow for the URL assignment.
  87. */
  88. define('BBCODE_INFO_ARGS', 4);
  89. /**
  90. * BBcode tag description field: an array of arguments that must be replaced
  91. * in the static replace tags (BBCODE_INFO_REPLACEOPEN and
  92. * BBCODE_INFO_REPLACECLOSE). In these tags, a string like %argname% can
  93. * be used to define where the tag should go. The argument value will be
  94. * HTML escaped before putting it in the tag, so it cannot be abused for XSS.
  95. */
  96. define('BBCODE_INFO_REPLACEARGS', 5);
  97. /**
  98. * BBcode tag description field: for simple tags that only need a static
  99. * replacement, a replacement string for the opening tag can be provided
  100. * through this field. For example the [b] tag that uses the static
  101. * HTML open tag "<b>".
  102. */
  103. define('BBCODE_INFO_REPLACEOPEN', 6);
  104. /**
  105. * BBcode tag description field: for simple tags that only need a static
  106. * replacement, a replacement string for the closing tag can be provided
  107. * through this field. For example the [b] tag that uses the static
  108. * HTML close tag "</b>".
  109. */
  110. define('BBCODE_INFO_REPLACECLOSE', 7);
  111. /**
  112. * BBcode tag description field: for complex tags that need argument parsing
  113. * or other processing logic, a callback function can be defined. This
  114. * callback function will be called when the close tag is reached. The callback
  115. * function will get two arguments: the content that is contained by the
  116. * tag and the tag information array. The function will have to return the
  117. * rendered output for the tag.
  118. */
  119. define('BBCODE_INFO_CALLBACK', 8);
  120. /**
  121. * BBcod tag description field: whether the tag is a tag that only needs
  122. * an open tag. For example the [hr] tag, which does not use a closing [/hr].
  123. */
  124. define('BBCODE_INFO_OPENONLY', 9);
  125. /**
  126. * BBcode tag description field: whether the tag is a tag that does not use
  127. * a real argument=value style argment, but for which the full argument part
  128. * of the tag is used as an argument value for the main tag. This is for
  129. * example used for the [quote] tag to make both [quote=JohnDoe] and
  130. * [quote JohnDoe] behave the same way. Note that the value will be stored
  131. * in the "array" argument field, so you will need to setup the field
  132. * {@link BBCODE_INFO_ARGS} as an array ('quote' => '') to make it work.
  133. */
  134. define('BBCODE_INFO_VALUEONLY', 10);
  135. /**
  136. * BBcode tag description field: whether to strip the first break after
  137. * the closing tag. For Phorum this means stripping the <phorum break>
  138. * that is used internally for preserving line breaks.
  139. */
  140. define('BBCODE_INFO_STRIPBREAK', 11);
  141. /**
  142. * BBcode tag description field: an array of parent containers for the tag.
  143. * This will make sure that the tag is always a direct child of one of the
  144. * provided container tags. This is for example used for formatting lists,
  145. * where the item [*] tag should always be inside a [list] parent. For this
  146. * setup, the [*] tag needs the parent array ('list'). If one of the parent
  147. * tags is enabled in the configuration, the child tag automatically is
  148. * enabled as well. Note that this makes adding the child tag to the module's
  149. * settings screen for enabling/disabling futile.
  150. */
  151. define('BBCODE_INFO_PARENTS', 12);
  152. /**
  153. * BBcode tag description field: the name of the tag. If the tag [foo] has
  154. * to be implemented, the value of this field would be "foo". This field
  155. * is autogenerated by the parse tree preparation code in
  156. * {@link bbcode_api_buildparser()}.
  157. */
  158. define('BBCODE_INFO_TAG', 13);
  159. /**
  160. * BBcode tag description field: this one is used internally by the parser.
  161. * It is used to parse tag arguments. It is autogenerated by the parse
  162. * tree preparation code in {@link bbcode_api_buildparser()}.
  163. */
  164. define('BBCODE_INFO_ARGPARSETREE', 14);
  165. // --------------------------------------------------------------------
  166. // Functions
  167. // --------------------------------------------------------------------
  168. /**
  169. * Build a list of all available BBcode tags.
  170. *
  171. * This will setup a list of all standard BBcode tags that are supported
  172. * and all tags that are added by other modules that implement them through
  173. * this module's "bbcode_register" hook.
  174. *
  175. * @return array
  176. * An array of bbcode tag information arrays, ready for use
  177. * by the {@link bbcode_api_buildparser()} function.
  178. */
  179. function bbcode_api_initparser($force = FALSE)
  180. {
  181. global $PHORUM;
  182. /**
  183. * [hook]
  184. * bbcode_register
  185. *
  186. * [description]
  187. * This hook is implemented by the BBcode module in the file
  188. * <literal>mods/bbcode/api.php</literal>. It allows modules to
  189. * provide extra or override existing BBcode tag descriptions.<sbr/>
  190. * <sbr/>
  191. * <b>Warning:</b> do not delete tags from the list, e.g. removing
  192. * a tag based on the login status for a user. That would throw off
  193. * and invalidate the caching mechanisms. If you need to have some tag
  194. * act differently for different users, then override the behavior for
  195. * the tag using a callback function and implement the logic in the
  196. * callback function.
  197. *
  198. * [category]
  199. * Module hooks
  200. *
  201. * [when]
  202. * This hook is called from the function
  203. * <literal>bbcode_api_initparser()</literal> in the BBcode
  204. * module file <literal>mods/bbcode/api.php</literal>.
  205. *
  206. * [input]
  207. * An array of tag description arrays.
  208. * The keys in this array are tag names. The values are arrays
  209. * describing the tags. For examples of what these tag descriptions
  210. * look like, please take a look at the file
  211. * <literal>mods/bbcode/builtin_tags.php</literal>.
  212. *
  213. * [output]
  214. * The same array as the one that was used for the hook call
  215. * arguments, possibly updated with new or updated tags.
  216. */
  217. $tags = array();
  218. if (isset($PHORUM['hooks']['bbcode_register'])) {
  219. $tags = phorum_hook('bbcode_register', $tags);
  220. }
  221. // Build a cache key for the current state of the tag list
  222. // and module settings. Include the file modification time for some
  223. // of the module files to let changes in the module code force a
  224. // parsing info update.
  225. $cachekey = md5(
  226. filemtime(__FILE__) .
  227. filemtime(dirname(__FILE__) . '/builtin_tags.php') .
  228. serialize(isset($PHORUM['mod_bbcode'])?$PHORUM['mod_bbcode']:time()) .
  229. serialize($tags)
  230. );
  231. // If no cached parsing data is available for the current state of the
  232. // tag list, then rebuild this data and store it in the database.
  233. if ($force ||
  234. !isset($PHORUM['mod_bbcode_parser']['cachekey']) ||
  235. $PHORUM['mod_bbcode_parser']['cachekey'] != $cachekey)
  236. {
  237. // First, build a full list of tags by merging the builtin tags
  238. // with the ones that the modules provided.
  239. require_once('./mods/bbcode/builtin_tags.php');
  240. $combinedtags = $GLOBALS['PHORUM']['MOD_BBCODE']['BUILTIN'];
  241. foreach ($tags as $tagname => $tag) {
  242. $combinedtags[$tagname] = $tag;
  243. }
  244. // Build the parser information.
  245. list ($taginfo, $parsetree) = bbcode_api_buildparser($combinedtags);
  246. // Store the parser information in the database.
  247. $PHORUM['mod_bbcode_parser'] = array(
  248. 'cachekey' => $cachekey,
  249. 'taginfo' => $taginfo,
  250. 'parsetree' => $parsetree
  251. );
  252. phorum_db_update_settings(array(
  253. 'mod_bbcode_parser' => $PHORUM['mod_bbcode_parser']
  254. ));
  255. }
  256. }
  257. /**
  258. * Process tags and prepare them for efficient use by the parser function.
  259. *
  260. * @return array
  261. * An array containing two elements. The first element is an array
  262. * containing all detail information about the available tags,
  263. * indexed by tag name. The second element is a parsetree, that is
  264. * used by the {@link bbcode_api_tokenize()} function for parsing
  265. * tags and their arguments.
  266. */
  267. function bbcode_api_buildparser($tags)
  268. {
  269. global $PHORUM;
  270. // The configured list of activated BBcode tags. For missing configuration
  271. // settings, the default settings as defined in the builtin_tags.php
  272. // script will be used.
  273. $enabled = isset($PHORUM['mod_bbcode']['enabled'])
  274. ? $PHORUM['mod_bbcode']['enabled'] : array();
  275. // Prepare the tag information and parsing tree.
  276. $taginfo = array();
  277. $parsetree = array('/' => array());
  278. foreach ($tags as $tagname => $tag)
  279. {
  280. // Check for tags that should be enabled, because their container
  281. // parent is enabled.
  282. if (!empty($tag[BBCODE_INFO_PARENTS])) {
  283. $enabled[$tagname] = FALSE;
  284. foreach ($tag[BBCODE_INFO_PARENTS] as $parent) {
  285. if (isset($enabled[$parent]) && $enabled[$parent]) {
  286. $enabled[$tagname] = TRUE;
  287. break;
  288. }
  289. }
  290. }
  291. // Skip disabled tags.
  292. if ((isset($enabled[$tagname]) && !$enabled[$tagname]) ||
  293. (!isset($enabled[$tagname]) && !$tag[BBCODE_INFO_DEFAULTSTATE])) {
  294. continue;
  295. }
  296. // Set default values for missing tag information fields.
  297. if (!isset($tag[BBCODE_INFO_OPENONLY])) {
  298. $tag[BBCODE_INFO_OPENONLY] = FALSE;
  299. }
  300. if (!isset($tag[BBCODE_INFO_PARENTS])) {
  301. $tag[BBCODE_INFO_PARENTS] = NULL;
  302. }
  303. if (!isset($tag[BBCODE_INFO_REPLACEOPEN])) {
  304. $tag[BBCODE_INFO_REPLACEOPEN] = '';
  305. }
  306. if (!isset($tag[BBCODE_INFO_REPLACECLOSE])) {
  307. $tag[BBCODE_INFO_REPLACECLOSE] = '';
  308. }
  309. if (!isset($tag[BBCODE_INFO_CALLBACK])) {
  310. $tag[BBCODE_INFO_CALLBACK] = NULL;
  311. }
  312. if (!isset($tag[BBCODE_INFO_VALUEONLY])) {
  313. $tag[BBCODE_INFO_VALUEONLY] = FALSE;
  314. }
  315. if (!isset($tag[BBCODE_INFO_REPLACEARGS])) {
  316. $tag[BBCODE_INFO_REPLACEARGS] = NULL;
  317. }
  318. if (!isset($tag[BBCODE_INFO_HASEDITORTOOL])) {
  319. $tag[BBCODE_INFO_HASEDITORTOOL] = FALSE;
  320. }
  321. if (!isset($tag[BBCODE_INFO_STRIPBREAK])) {
  322. $tag[BBCODE_INFO_STRIPBREAK] = FALSE;
  323. }
  324. // Remove fields that are not needed by the parser or module.
  325. // These are only used by the module's settings screen.
  326. unset($tag[BBCODE_INFO_DESCRIPTION]);
  327. unset($tag[BBCODE_INFO_DEFAULTSTATE]);
  328. // Add fields that are static.
  329. $tag[BBCODE_INFO_TAG] = $tagname;
  330. // Add the tag arguments to the argument parse tree for this tag.
  331. $argparsetree = NULL;
  332. if (isset($tag[BBCODE_INFO_ARGS]))
  333. {
  334. $argparsetree = array();
  335. foreach ($tag[BBCODE_INFO_ARGS] as $argname => $default) {
  336. $node =& $argparsetree;
  337. $arglen = strlen($argname);
  338. for ($i=0; $i<$arglen; $i++)
  339. {
  340. $l = $argname[$i];
  341. if (!isset($node[$l]))
  342. {
  343. $node[$l] = array();
  344. }
  345. $node =& $node[$l];
  346. }
  347. $node['arg'] = TRUE;
  348. }
  349. }
  350. $tag[BBCODE_INFO_ARGPARSETREE] = $argparsetree;
  351. // Add the tag name to the tag name parse tree.
  352. $node =& $parsetree;
  353. $closenode =& $parsetree['/'];
  354. $taglen = strlen($tagname);
  355. for ($i=0; $i<$taglen; $i++)
  356. {
  357. $l = $tagname[$i];
  358. if (!isset($node[$l]))
  359. {
  360. $node[$l] = array();
  361. // Do not add tag closing data for tags that can only have
  362. // an opening node.
  363. if (empty($tag[BBCODE_INFO_OPENONLY])) {
  364. $closenode[$l] = array();
  365. }
  366. }
  367. $node =& $node[$l];
  368. // Do not add tag closing data for tags that can only have
  369. // an opening node.
  370. if (empty($tag[BBCODE_INFO_OPENONLY])) {
  371. $closenode =& $closenode[$l];
  372. }
  373. }
  374. // Add the tag to the information array.
  375. $taginfo[$tagname] = $tag;
  376. // We add tag token templates as the end points in the tree.
  377. // These will be used by the tokenizer as the base token that is
  378. // pushed onto the token array. The tokenizer can update data for
  379. // this template (i.e. tag arguments and token stack level).
  380. // Add the open tag token template to the parse tree.
  381. $node['tag'] = array(
  382. $tag[BBCODE_INFO_TAG],
  383. FALSE
  384. );
  385. // If the tag takes arguments, add them to the token template as well.
  386. if (isset($tag[BBCODE_INFO_ARGS])) {
  387. $node['tag'][] = $tag[BBCODE_INFO_ARGS];
  388. }
  389. // For tags that require a close tag, add a closing tag
  390. // token template to the parse tree.
  391. if (empty($tag[BBCODE_INFO_OPENONLY])) {
  392. $closenode['tag'] = array(
  393. $tag[BBCODE_INFO_TAG],
  394. TRUE
  395. );
  396. }
  397. }
  398. return array($taginfo, $parsetree);
  399. }
  400. /**
  401. * Tokenize the provided text into BBcode tokens.
  402. *
  403. * The data that is returned by this function can be processed by the
  404. * {@link bbcode_api_render()} function to render the final HTML code.
  405. *
  406. * @param string $text
  407. * The text that has to be parsed. This *must* be a HTML escaped text.
  408. * The parser code does not perform any HTML escaping on its own.
  409. * This requirement is based on Phorum's defensive nature of fully
  410. * HTML escaping the body text before doing any processing on it. This
  411. * way, processing code will have to consciously HTML unescape body text
  412. * that must be shown as plain HTML code.
  413. *
  414. * @return array
  415. * An array of tokens. Each item can either be a string, in which a
  416. * text token was processed, or an array describing a tag. The tag
  417. * description is a tag info array as used internally by the BBcode
  418. * code, enriched with an extra field "is_closetag" which tells
  419. * whether the opening or close tag is being handled.
  420. */
  421. function bbcode_api_tokenize($text)
  422. {
  423. global $PHORUM;
  424. // Initialize the variables that are used during tokenizing.
  425. $cursor = 0;
  426. $maxpos = strlen($text) - 1;
  427. $state = 1;
  428. $is_closetag = 0;
  429. $current_tag = NULL;
  430. $current_tagname = NULL;
  431. $current_arg = NULL;
  432. $current_val = NULL;
  433. $current_token = NULL;
  434. $stack = array();
  435. $stackidx = 0;
  436. $opentags = array();
  437. $autoclosed = array();
  438. $tokens = array();
  439. $tokenidx = 0;
  440. $text_start = 0;
  441. $text_end = 0;
  442. $taginfo = $PHORUM['mod_bbcode_parser']['taginfo'];
  443. // The big outer loop. This one lets the parser run over the
  444. // full text that has to be parsed.
  445. for (;;)
  446. {
  447. // Leave this loop if we are at the end of the text.
  448. if ($cursor > $maxpos) break;
  449. // ------------------------------------------------------------------
  450. // 1: find the tag starting character "[" in the text
  451. // ------------------------------------------------------------------
  452. if ($state == 1)
  453. {
  454. // Find the length of the chunk up to the first "[" character.
  455. $pos = strpos($text, "[", $cursor);
  456. if ($pos === FALSE) break;
  457. // Handle BBcode tag escaping if this feature is enabled.
  458. // Check if the "[" character is escaped by prepending it with
  459. // a backslash. If it is, then we do not process the bbcode tag.
  460. // Instead we hide the backslash and continue searching for
  461. // the next "[" character.
  462. if (!empty($PHORUM['mod_bbcode']['enable_bbcode_escape']) &&
  463. $pos > 0 && $text[$pos-1] == '\\')
  464. {
  465. $tokens[++$tokenidx] = array(
  466. 'TEXTNODE', $text_start, $pos - 1 - $text_start
  467. );
  468. $text_start = $pos;
  469. $cursor = $pos+1;
  470. $text_end = $cursor;
  471. continue;
  472. }
  473. // Move on to the possible tagname.
  474. // +1 is for skipping the "[" character that we found.
  475. $chunklen = $pos - $cursor;
  476. $cursor += $chunklen + 1;
  477. if ($cursor > $maxpos) break;
  478. $state = 2;
  479. // Update the pointer to the end of the active text node.
  480. // Here we move it to the position before the "[" character,
  481. // because if we are at an actual tag right now, we do not want
  482. // to include data from the tag into the text node.
  483. $text_end = $cursor - 1;
  484. }
  485. // ------------------------------------------------------------------
  486. // 2: check for a valid tag name after the "[" char
  487. // ------------------------------------------------------------------
  488. if ($state == 2)
  489. {
  490. // check if we can find a valid tag name, by walking the tag
  491. // name parse tree.
  492. $node = $PHORUM['mod_bbcode_parser']['parsetree'];
  493. $current_tag = NULL;
  494. for (;;)
  495. {
  496. if ($cursor > $maxpos) break 2;
  497. $l = strtolower($text[$cursor++]);
  498. // As long as we find matching nodes in the parse tree,
  499. // we keep walking it.
  500. if (isset($node[$l])) {
  501. $node = $node[$l];
  502. }
  503. // When we hit the end of the tree, we check if there is
  504. // a separator character after the possible tag name.
  505. // If that is the case and we can find the token template for
  506. // a tag in the parse tree, then we go on with the tag that
  507. // we found. Otherwise, we go back to searching a new tag
  508. // opening character.
  509. else
  510. {
  511. // Did we find a tag?
  512. if (!isset($node['tag'])) { $state = 1; continue 2; }
  513. $is_closetag = !empty($node['tag'][1]); // 1 = close tag?
  514. // Find the current character that our cursor is on.
  515. $l = $text[--$cursor];
  516. // For close tags, we need a closing square bracket here.
  517. if ($is_closetag && $l != ']') { $state = 1; continue 2; }
  518. // For open tags, we need a space, equal sign or
  519. // closing square bracket here.
  520. if ($l != ' ' && $l != '=' && $l != ']') {
  521. $state = 1; continue 2;
  522. }
  523. // Checks passed. This might just be a valid tag!
  524. $current_tagname = $node['tag'][0]; // 0 = tag name
  525. $current_tag = $taginfo[$current_tagname];
  526. $current_token = $node['tag'];
  527. break;
  528. }
  529. }
  530. // Check if we're not handling an open tag for a tag type that
  531. // needs to be child of a specific parent tag type, but for which
  532. // there is no open parent available.
  533. if (!$is_closetag &&
  534. !empty($current_tag[BBCODE_INFO_PARENTS]))
  535. {
  536. $found_parent = FALSE;
  537. foreach ($current_tag[BBCODE_INFO_PARENTS] as $p) {
  538. if (!empty($opentags[$p])) {
  539. $found_parent = TRUE;
  540. break;
  541. }
  542. }
  543. if (!$found_parent) {
  544. $state = 1;
  545. continue;
  546. }
  547. }
  548. // Check if we're handling a closing tag for a tag that
  549. // we autoclosed or did not open before. If so, then we
  550. // want to skip this tag totally.
  551. if ($is_closetag && (
  552. !empty($autoclosed[$current_tagname]) ||
  553. empty($opentags[$current_tagname])))
  554. {
  555. // If there is text in the text node building up to
  556. // the tag that we are skipping, then add this to
  557. // the parsed data. The skipped tag string is not
  558. // included this way.
  559. if ($text_end != $text_start)
  560. {
  561. $tokens[++$tokenidx] = array(
  562. 'TEXTNODE', $text_start, $text_end - $text_start
  563. );
  564. }
  565. // One automatically closed tag is accounted for now.
  566. if (!empty($autoclosed[$current_tagname])) {
  567. $autoclosed[$current_tagname]--;
  568. }
  569. // Stale close tag. We include the stale tag string
  570. // in the current text node.
  571. else
  572. {
  573. // Create a new textnode or add to the existing one.
  574. if ($tokenidx && $tokens[$tokenidx][0] == 'TEXTNODE') {
  575. $tokens[$tokenidx][2] += ($cursor - $text_end + 1);
  576. } else {
  577. $tokens[++$tokenidx] = array(
  578. 'TEXTNODE', $text_start, $cursor - $text_end + 1
  579. );
  580. }
  581. }
  582. // Continue searching for a new tag, right after the close tag.
  583. $text_start = ++$cursor;
  584. $state = 1;
  585. continue;
  586. }
  587. // All checks were okay. We can process the tag. If the next
  588. // character in the text is an equal sign, then we are looking
  589. // at a possible [tag=value] tag, where a value is assigned to
  590. // the tag directly. For value only tags, a space is allowed as
  591. // well for this ([tag value]).
  592. if ($text[$cursor] == '=' ||
  593. ($text[$cursor] == ' ' && $current_tag[BBCODE_INFO_VALUEONLY]))
  594. {
  595. $cursor++;
  596. if ($cursor > $maxpos) break;
  597. // To accept values that are assigned to the tag name, the
  598. // argument list definition must contain the name of the tag
  599. // itself as a possible argument.
  600. if (isset($current_tag[BBCODE_INFO_ARGS][$current_tagname])) {
  601. // Switch to argument value parsing.
  602. $current_arg = $current_tagname;
  603. $state = 4;
  604. } else {
  605. $state = 1;
  606. continue;
  607. }
  608. }
  609. else
  610. {
  611. // The start of a tag was found. Continue looking
  612. // for the closing character or tag arguments.
  613. $state = 3;
  614. }
  615. }
  616. // ------------------------------------------------------------------
  617. // 3: find the end of tag char "]" or a new tag argument
  618. // ------------------------------------------------------------------
  619. if ($state == 3)
  620. {
  621. // Handle closing of a tag.
  622. if ($text[$cursor] == ']')
  623. {
  624. // If there is text in the text node building up to the
  625. // tag that we just ended, then add this to the parsed data.
  626. if ($text_end != $text_start)
  627. {
  628. $tokens[++$tokenidx] = array(
  629. 'TEXTNODE', $text_start, $text_end - $text_start
  630. );
  631. }
  632. // Handle closing tags.
  633. if ($current_token[1]) // 1 = is close tag
  634. {
  635. $opentags[$current_tagname]--;
  636. // To assure proper tag nesting, we make use of our
  637. // tag stack to find the accompanying open tag for
  638. // the current close tag. If we find that there are
  639. // open tags before the accompanying open tag for our
  640. // current tag, then we implicitly close those tags.
  641. while ($stackidx > 0)
  642. {
  643. $toptoken = $stack[$stackidx--];
  644. $topname = $toptoken[0]; // 0 = tag name
  645. // Keep track if the current top tag matches the
  646. // currently processed close tag.
  647. $found_matching_open_tag = FALSE;
  648. if ($topname == $current_tag[BBCODE_INFO_TAG]) {
  649. $found_matching_open_tag = TRUE;
  650. }
  651. // The current top tag does not match the currently
  652. // processed close tag. We'll close the top tag and
  653. // flag it as autoclosed.
  654. else
  655. {
  656. $opentags[$topname]--;
  657. $autoclosed[$topname] =
  658. isset($autoclosed[$topname])
  659. ? $autoclosed[$topname] + 1 : 1;
  660. }
  661. // Add the close tag to the parsed data.
  662. $toptoken[1] = TRUE; // 1 = is close tag
  663. $tokens[++$tokenidx] = $toptoken;
  664. if ($found_matching_open_tag) break;
  665. }
  666. // Strip trailing break after the close tag, if the tag
  667. // is configured to do so.
  668. if ($current_tag[BBCODE_INFO_STRIPBREAK])
  669. {
  670. // First, skip any white space character that we find.
  671. $peekcursor = $cursor + 1;
  672. while (isset($text[$peekcursor]) &&
  673. ($text[$peekcursor] == " " ||
  674. $text[$peekcursor] == "\n" ||
  675. $text[$peekcursor] == "\r")) {
  676. $peekcursor++;
  677. }
  678. // Check for a Phorum break and strip if we find one.
  679. if (isset($text[$peekcursor]) &&
  680. substr($text,$peekcursor,14) == '<phorum break>') {
  681. $cursor = $peekcursor + 13;
  682. }
  683. }
  684. }
  685. // Handle opening tags.
  686. else
  687. {
  688. // Take care of parent constraints.
  689. if (!empty($current_tag[BBCODE_INFO_PARENTS]))
  690. {
  691. while (!in_array(
  692. $stack[$stackidx][0], // 0 = tag name
  693. $current_tag[BBCODE_INFO_PARENTS]
  694. )) {
  695. $token = $stack[$stackidx--];
  696. $opentags[$token[0]]--; // 0 = tag name
  697. $autoclosed[$token[0]] =
  698. isset($autoclosed[$token[0]])
  699. ? $autoclosed[$token[0]] + 1 : 1;
  700. // Add the tag close to the parsed data.
  701. $token[1] = TRUE; // 1 = is close tag
  702. $tokens[++$tokenidx] = $token;
  703. }
  704. }
  705. $opentags[$current_tagname] =
  706. isset($opentags[$current_tagname]) ?
  707. $opentags[$current_tagname] + 1 : 1;
  708. $stack[++$stackidx] = $current_token;
  709. if (!empty($autoclosed[$current_tagname])) {
  710. $autoclosed[$current_tagname]--;
  711. }
  712. $tokens[++$tokenidx] = $current_token;
  713. // Strip trailing break after the open tag, if the tag
  714. // is configured to do so and it is open only.
  715. if ($current_tag[BBCODE_INFO_STRIPBREAK] &&
  716. $current_tag[BBCODE_INFO_OPENONLY]) {
  717. // First, skip any white space character that we find.
  718. $peekcursor = $cursor + 1;
  719. while (isset($text[$peekcursor]) &&
  720. ($text[$peekcursor] == " " ||
  721. $text[$peekcursor] == "\n" ||
  722. $text[$peekcursor] == "\r")) {
  723. $peekcursor++;
  724. }
  725. // Check for a Phorum break and strip if we find one.
  726. if (isset($text[$peekcursor]) &&
  727. substr($text,$peekcursor,14) == '<phorum break>') {
  728. $cursor = $peekcursor + 13;
  729. }
  730. }
  731. }
  732. $cursor++;
  733. $text_start = $text_end = $cursor;
  734. if ($cursor > $maxpos) break;
  735. $state = 1;
  736. continue;
  737. }
  738. // If the current tag does not take arguments, then it is
  739. // apparently wrong and we can continue searching for the next tag.
  740. // We can also continue if there is no space, indicating the start
  741. // for a new argument.
  742. elseif ($text[$cursor] != ' ' ||
  743. empty($current_tag[BBCODE_INFO_ARGPARSETREE])) {
  744. $state = 1;
  745. continue;
  746. }
  747. // Skip multiple spaces.
  748. while (isset($text[$cursor]) && $text[$cursor] == ' ') $cursor++;
  749. if ($cursor > $maxpos) break;
  750. // If we ended up at the end of the bbcode tag by now, then
  751. // restart parsing state 3 to handle this.
  752. if ($text[$cursor] == ']') {
  753. $state = 3;
  754. continue;
  755. }
  756. // Check if we can find a valid argument.
  757. $node = $current_tag[BBCODE_INFO_ARGPARSETREE];
  758. $current_arg = '';
  759. for (;;)
  760. {
  761. if ($cursor > $maxpos) break 2;
  762. $l = strtolower($text[$cursor++]);
  763. // Walk the argument parse tree, until we cannot find
  764. // a matching character anymore.
  765. if (isset($node[$l])) {
  766. $current_arg .= $l;
  767. $node = $node[$l];
  768. continue;
  769. }
  770. // The arguments must be followed by one of " ", "=" or "]".
  771. // Also check if we really found the end of an argument here.
  772. if (($l != ' ' && $l != '=' && $l != ']') ||
  773. !isset($node['arg'])) {
  774. $state = 1;
  775. break 2;
  776. }
  777. // Argument found.
  778. $cursor--;
  779. break;
  780. }
  781. // Check if there is a value assignment for the argument.
  782. // If not, then we asume it's a boolean flag, which we
  783. // set to TRUE.
  784. if ($text[$cursor] == ' ' || $text[$cursor] == ']')
  785. {
  786. if ($text[$cursor] == ' ') $cursor++;
  787. $current_token[2][$current_arg] = TRUE; // 2 = args
  788. $state = 3;
  789. continue;
  790. }
  791. // There is an equal sign after the argument (checked in
  792. // earlier code). Read in the argument value.
  793. else
  794. {
  795. $cursor++;
  796. if ($cursor > $maxpos) break;
  797. $state = 4;
  798. }
  799. }
  800. // ------------------------------------------------------------------
  801. // 4: parse a value for a tag argument
  802. // ------------------------------------------------------------------
  803. if ($state == 4)
  804. {
  805. $current_val = '';
  806. // Handle &quot; style quotes.
  807. $forcequote = NULL;
  808. if ($text[$cursor] == '&') {
  809. if (substr($text, $cursor, 6) == '&quot;') {
  810. $forcequote = '&';
  811. $cursor += 5;
  812. if ($cursor > $maxpos) break;
  813. }
  814. }
  815. // Handle quoted values.
  816. if ($forcequote !== NULL ||
  817. $text[$cursor] == '"' ||
  818. $text[$cursor] == "'")
  819. {
  820. $quote = $forcequote === NULL ? $text[$cursor] : $forcequote;
  821. $mask = '\\'.$quote;
  822. $cursor++;
  823. if ($cursor > $maxpos) break;
  824. for (;;)
  825. {
  826. $chunklen = strcspn($text, $mask, $cursor);
  827. if ($chunklen > 0) {
  828. $current_val .= substr($text, $cursor, $chunklen);
  829. $cursor += $chunklen;
  830. }
  831. // Leave the main loop if we are at the end of the text.
  832. // We can look ahead one character here. If there are no
  833. // more characters after the one we found using the $mask,
  834. // then this can in no way become a valid
  835. // tag argument value.
  836. if (($cursor + 1) > $maxpos) break 2;
  837. // Handle escaped characters.
  838. if ($text[$cursor] == '\\')
  839. {
  840. $cursor++;
  841. $current_val .= $text[$cursor];
  842. $cursor++;
  843. if ($cursor > $maxpos) break 2;
  844. }
  845. // Handle end of quoted arguments.
  846. else
  847. {
  848. // Handle &quot; style quotes.
  849. if ($quote == '&') {
  850. if (substr($text, $cursor, 6) == '&quot;') {
  851. $cursor += 5;
  852. } else {
  853. $current_val .= $quote;
  854. $cursor++;
  855. continue;
  856. }
  857. }
  858. // Add the argument to the token arguments.
  859. $current_token[2][$current_arg] = $current_val; // 2 = args
  860. $cursor++;
  861. $state = 3;
  862. break;
  863. }
  864. }
  865. continue;
  866. }
  867. // Handle unquoted values.
  868. else
  869. {
  870. // Value only tag arguments run till the closing ] character.
  871. if ($current_tag[BBCODE_INFO_VALUEONLY]) {
  872. $pos = strpos($text, "]", $cursor);
  873. if ($pos === FALSE) break;
  874. $chunklen = $pos - $cursor;
  875. } else {
  876. // Unquoted arguments end at " " or "]".
  877. $chunklen = strcspn($text, " ]", $cursor);
  878. }
  879. $current_val = substr($text, $cursor, $chunklen);
  880. // Add the argument to the token arguments.
  881. $current_token[2][$current_arg] = $current_val; // 2 = args
  882. $cursor += $chunklen;
  883. if ($cursor > $maxpos) break;
  884. $state = 3;
  885. continue;
  886. }
  887. }
  888. }
  889. // Add trailing text node to the parsed data.
  890. $text_end = $maxpos + 1;
  891. if ($text_start != $text_end) {
  892. $tokens[++$tokenidx] = array(
  893. 'TEXTNODE', $text_start, $text_end - $text_start
  894. );
  895. }
  896. // Close tags that weren't explicitly closed.
  897. while ($stackidx > 0)
  898. {
  899. $token = $stack[$stackidx--];
  900. $opentags[$token[0]]--; // 0 = tag name
  901. $autoclosed[$token[0]] =
  902. isset($autoclosed[$token[0]])
  903. ? $autoclosed[$token[0]] + 1 : 1;
  904. // Add the tag close to the parsed data.
  905. $token[1] = TRUE; // 1 = is close tag
  906. $tokens[++$tokenidx] = $token;
  907. }
  908. return $tokens;
  909. }
  910. /**
  911. * Render the tokens that are returned from {@link bbcode_api_tokenize()}
  912. * into HTML code.
  913. *
  914. * @param string $text
  915. * The text that was parsed by {@link bbcode_api_tokenize()}.
  916. *
  917. * @param array $tokens
  918. * The tokens as returned by the {@link bbcode_api_tokenize()} function.
  919. *
  920. * @param array $message
  921. * The message that is being parsed. This message is passed on to
  922. * tag handling callback functions (BBCODE_INFO_CALLBACK), to provide
  923. * context information to the callback.
  924. * This argument is treated as a reference argument, making it possible
  925. * for the tag callback to do the same. This can be useful for things
  926. * like passing information for a later hook (e.g. format_fixup).
  927. *
  928. * @return string
  929. * The rendered HTML code.
  930. */
  931. function bbcode_api_render($text, $tokens, &$message)
  932. {
  933. global $PHORUM;
  934. $buffers = array(0 => '');
  935. $bufferidx = 0;
  936. $taginfo = $PHORUM['mod_bbcode_parser']['taginfo'];
  937. foreach ($tokens as $token)
  938. {
  939. // Add a standard text node.
  940. if ($token[0] == 'TEXTNODE')
  941. {
  942. $buffers[$bufferidx] .= substr($text, $token[1], $token[2]);
  943. }
  944. // Add a bbcode tag node.
  945. else
  946. {
  947. // Retrieve the configuration for this token's tag.
  948. $tag = $taginfo[$token[0]]; // 0 = tag name
  949. // Handle closing tag.
  950. if ($token[1]) // 1 = is close tag
  951. {
  952. // A callback function is defined for this tag. Now we are
  953. // at the closing tag, we call this callback function.
  954. // We provide the content within this tag and the arguments
  955. // that were used for the tag as the call arguments.
  956. if (isset($tag[BBCODE_INFO_CALLBACK]))
  957. {
  958. $buffers[$bufferidx-1] .= call_user_func_array(
  959. $tag[BBCODE_INFO_CALLBACK],
  960. array(
  961. $buffers[$bufferidx],
  962. isset($token[2]) ? $token[2] : NULL, // 2 = args
  963. &$message
  964. )
  965. );
  966. unset($buffers[$bufferidx]);
  967. $bufferidx--;
  968. }
  969. // This tag has a defined close tag string in its config.
  970. elseif ($tag[BBCODE_INFO_REPLACECLOSE] !== NULL)
  971. {
  972. // Run argument replacement on the close tag string.
  973. if (!empty($tag[BBCODE_INFO_REPLACEARGS]))
  974. {
  975. foreach ($tag[BBCODE_INFO_REPLACEARGS] as $key) {
  976. $tag[BBCODE_INFO_REPLACECLOSE] = str_replace(
  977. '%'.$key.'%',
  978. htmlspecialchars(
  979. $token[2][$key], // 2 = args
  980. ENT_COMPAT, $PHORUM["DATA"]["HCHARSET"]
  981. ),
  982. $tag[BBCODE_INFO_REPLACECLOSE]
  983. );
  984. }
  985. }
  986. $buffers[$bufferidx] .= $tag[BBCODE_INFO_REPLACECLOSE];
  987. }
  988. else
  989. {
  990. $buffers[$bufferidx] .=
  991. "{No close tag definition for " . $token[0] . "}";
  992. }
  993. }
  994. // Tag open
  995. else
  996. {
  997. // Callbacks are always run at the close tag. We setup a new
  998. // buffer for this tag's contents, so we can hand the full
  999. // content over to the tag handling function when we get
  1000. // to the closing tag.
  1001. if (isset($tag[BBCODE_INFO_CALLBACK]))
  1002. {
  1003. $bufferidx++;
  1004. $buffers[$bufferidx] = '';
  1005. }
  1006. // This tag has a defined open tag string in its config.
  1007. elseif ($tag[BBCODE_INFO_REPLACEOPEN] !== NULL)
  1008. {
  1009. // Run argument replacement on the open tag string.
  1010. if (!empty($tag[BBCODE_INFO_REPLACEARGS]))
  1011. {
  1012. foreach ($tag[BBCODE_INFO_REPLACEARGS] as $key) {
  1013. $tag[BBCODE_INFO_REPLACEOPEN] = str_replace(
  1014. '%'.$key.'%',
  1015. htmlspecialchars(
  1016. $token[2][$key], // 2 = args
  1017. ENT_COMPAT, $PHORUM["DATA"]["HCHARSET"]
  1018. ),
  1019. $tag[BBCODE_INFO_REPLACEOPEN]
  1020. );
  1021. }
  1022. }
  1023. $buffers[$bufferidx] .= $tag[BBCODE_INFO_REPLACEOPEN];
  1024. }
  1025. else
  1026. {
  1027. $buffers[$bufferidx] .=
  1028. "{No open tag definition for " . $token[0] . "}";
  1029. }
  1030. }
  1031. }
  1032. }
  1033. return $buffers[0];
  1034. }
  1035. // --------------------------------------------------------------------
  1036. // Tag handler functions
  1037. // --------------------------------------------------------------------
  1038. function bbcode_email_handler($content, $args, $message)
  1039. {
  1040. if ($args['email'] == '') {
  1041. if (strpos($content, '<') !== FALSE ||
  1042. strpos($content, '"') !== FALSE ||
  1043. strpos($content, '>') !== FALSE)
  1044. $content = preg_replace('/[<">].*[<">]/', '', $content);
  1045. $args['email'] = $content;
  1046. }
  1047. $append = '';
  1048. if ($args['subject'] != '')
  1049. {
  1050. // Decode the HTML entities in the subject.
  1051. // Use a fallback function for PHP versions prior to 5.1.0.
  1052. if (function_exists('htmlspecialchars_decode')) {
  1053. $subject = htmlspecialchars_decode($args['subject']);
  1054. } else {
  1055. $subject = strtr(
  1056. $args['subject'],
  1057. array_flip(get_html_translation_table(HTML_SPECIALCHARS))
  1058. );
  1059. }
  1060. // Recode it using urlencoding, so we can put it in the URL.
  1061. $append = '?subject='.rawurlencode($subject);
  1062. }
  1063. // Obfuscate against mail address harvesting by spammers.
  1064. $email = bbcode_html_encode($args['email']);
  1065. $content = bbcode_html_encode($content);
  1066. return "<a href=\"mailto:$email$append\">$content</a>";
  1067. }
  1068. function bbcode_html_encode($string)
  1069. {
  1070. $ret_string = "";
  1071. $len = strlen( $string );
  1072. for( $x = 0;$x < $len;$x++ ) {
  1073. $ord = ord( $string[$x] );
  1074. $ret_string .= "&#$ord;";
  1075. }
  1076. return $ret_string;
  1077. }
  1078. function bbcode_img_handler($content, $args, $message)
  1079. {
  1080. if ($args['img'] == '') {
  1081. if (strpos($content, '<') !== FALSE ||
  1082. strpos($content, '"') !== FALSE ||
  1083. strpos($content, '>') !== FALSE)
  1084. $content = preg_replace('/[<">].*[<">]/', '', $content);
  1085. $args['img'] = $content;
  1086. }
  1087. if (!preg_match('!^\w+://!', $args['img'])) {
  1088. $args['img'] = 'http://'.$args['img'];
  1089. }
  1090. $append = '';
  1091. if ($args['size'] != '') {
  1092. if (strstr($args['size'], 'x'))
  1093. {
  1094. list ($w,$h) = explode('x', $args['size']);
  1095. settype($w, 'int');
  1096. settype($h, 'int');
  1097. $append = "width=\"$w\" height=\"$h\"";
  1098. }
  1099. else
  1100. {
  1101. settype($args['size'], 'int');
  1102. $append = 'width="'.$args['size'].'"';
  1103. }
  1104. }
  1105. return "<img src=\"{$args['img']}\" class=\"bbcode\" border=\"0\" $append/>";
  1106. }
  1107. function bbcode_url_handler($content, $args, $message)
  1108. {
  1109. global $PHORUM;
  1110. // Setup special URL options.
  1111. static $extratags = NULL;
  1112. static $show_full_urls = FALSE;
  1113. $settings = $PHORUM['mod_bbcode'];
  1114. if ($extratags === NULL)
  1115. {
  1116. $extratags = '';
  1117. if (!empty($settings['links_in_new_window'])) {
  1118. $extratags .= 'target="_blank" ';
  1119. }
  1120. if (!empty($settings['show_full_urls'])) {
  1121. $show_full_urls = TRUE;
  1122. }
  1123. }
  1124. $strip_url = FALSE;
  1125. if ($args['url'] == '') {
  1126. if (strpos($content, '<') !== FALSE ||
  1127. strpos($content, '"') !== FALSE ||
  1128. strpos($content, '>') !== FALSE)
  1129. $content = preg_replace('/[<">].*[<">]/', '', $content);
  1130. $args['url'] = $content;
  1131. $strip_url = TRUE;
  1132. }
  1133. if (!preg_match('!^\w+://!', $args['url'])) {
  1134. $args['url'] = 'http://'.$args['url'];
  1135. }
  1136. // we need the full url for nofollow handling
  1137. $nofollow='';
  1138. if (!empty($settings['rel_no_follow'])) {
  1139. if($settings['rel_no_follow'] == 1) {
  1140. // always add nofollow
  1141. $nofollow .= ' rel="nofollow"';
  1142. } else {
  1143. // check for defined urls
  1144. $follow = false;
  1145. $check_urls = array();
  1146. if(!empty($settings['follow_urls'])) {
  1147. $check_urls = explode(",",$settings['follow_urls']);
  1148. }
  1149. $check_urls[]=$PHORUM['http_path'];
  1150. foreach($check_urls as $check_url) {
  1151. // the url has to start with one of these URLs
  1152. if(stripos($args['url'],$check_url) === 0) {
  1153. $follow = true;
  1154. break;
  1155. }
  1156. }

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