Oppaitime's version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

text.class.php 45KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. <?php
  2. class Text
  3. {
  4. /**
  5. * Array of valid tags; tag => max number of attributes
  6. * @var array $ValidTags
  7. */
  8. private static $ValidTags = array('b'=>0, 'u'=>0, 'i'=>0, 's'=>0, '*'=>0, '#'=>0, 'ch'=>0, 'uch'=>0, 'artist'=>0, 'user'=>0, 'n'=>0, 'inlineurl'=>0, 'inlinesize'=>1, 'headline'=>1, 'align'=>1, 'color'=>1, 'colour'=>1, 'size'=>1, 'url'=>1, 'img'=>1, 'quote'=>1, 'pre'=>1, 'code'=>1, 'tex'=>0, 'hide'=>1, 'spoiler' => 1, 'plain'=>0, 'important'=>0, 'torrent'=>0, 'rule'=>0, 'embed'=>0,
  9. );
  10. /**
  11. * Array of smilies; code => image file in STATIC_SERVER/common/smileys
  12. * @var array $Smileys
  13. */
  14. private static $Smileys = array(
  15. ':angry:' => 'angry.gif',
  16. ':-D' => 'biggrin.gif',
  17. ':D' => 'biggrin.gif',
  18. ':|' => 'blank.gif',
  19. ':-|' => 'blank.gif',
  20. ':blush:' => 'blush.gif',
  21. ':cool:' => 'cool.gif',
  22. ':&#39;(' => 'crying.gif',
  23. ':crying:' => 'crying.gif',
  24. '&gt;.&gt;' => 'eyesright.gif',
  25. ':frown:' => 'frown.gif',
  26. '&lt;3' => 'heart.gif',
  27. ':unsure:' => 'hmm.gif',
  28. //':\\' => 'hmm.gif',
  29. ':whatlove:' => 'ilu.gif',
  30. ':lol:' => 'laughing.gif',
  31. ':loveflac:' => 'loveflac.gif',
  32. ':flaclove:' => 'loveflac.gif',
  33. ':ninja:' => 'ninja.gif',
  34. ':no:' => 'no.gif',
  35. ':nod:' => 'nod.gif',
  36. ':ohno:' => 'ohnoes.gif',
  37. ':ohnoes:' => 'ohnoes.gif',
  38. ':omg:' => 'omg.gif',
  39. ':o' => 'ohshit.gif',
  40. ':O' => 'ohshit.gif',
  41. ':paddle:' => 'paddle.gif',
  42. ':(' => 'sad.gif',
  43. ':-(' => 'sad.gif',
  44. ':shifty:' => 'shifty.gif',
  45. ':sick:' => 'sick.gif',
  46. ':)' => 'smile.gif',
  47. ':-)' => 'smile.gif',
  48. ':sorry:' => 'sorry.gif',
  49. ':thanks:' => 'thanks.gif',
  50. ':P' => 'tongue.gif',
  51. ':p' => 'tongue.gif',
  52. ':-P' => 'tongue.gif',
  53. ':-p' => 'tongue.gif',
  54. ':wave:' => 'wave.gif',
  55. ';-)' => 'wink.gif',
  56. ':wink:' => 'wink.gif',
  57. ':creepy:' => 'creepy.gif',
  58. ':worried:' => 'worried.gif',
  59. ':wtf:' => 'wtf.gif',
  60. ':wub:' => 'wub.gif',
  61. ':ban:' => 'onion_ban.gif',
  62. ':oy:' => 'onion_oy.gif',
  63. ':laugh:' => 'onion_laugh.gif',
  64. ':snicker:' => 'onion_snicker.gif',
  65. ':barf:' => 'onion_barf.gif',
  66. ':dies:' => 'onion_dies.gif',
  67. ':shiver:' => 'onion_shiver.gif',
  68. ':frozen:' => 'onion_frozen.gif',
  69. ':relax:' => 'onion_relax.gif',
  70. ':hurry:' => 'onion_hurry.gif',
  71. ':whistle:' => 'onion_whistle.gif',
  72. ':negligent:' => 'onion_negligent.gif',
  73. ':nice:' => 'onion_nice.gif',
  74. ':giveup:' => 'onion_giveup.gif',
  75. ':hi:' => 'onion_hi.gif',
  76. ':bye:' => 'onion_bye.gif',
  77. ':dizzy:' => 'onion_dizzy.gif',
  78. ':evil:' => 'onion_evil.gif',
  79. ':distressed:' => 'onion_distressed.gif',
  80. ':dunno:' => 'onion_dunno.gif',
  81. ':sick:' => 'onion_sick.gif',
  82. ':why:' => 'onion_why.gif',
  83. ':full:' => 'onion_full.gif',
  84. ':zzz:' => 'onion_zzz.gif',
  85. ':whisper:' => 'onion_whisper.gif',
  86. ':love:' => 'onion_love.gif',
  87. ':nuts:' => 'onion_nuts.gif',
  88. ':please:' => 'onion_please.gif',
  89. ':unforgiven:' => 'onion_unforgiven.gif',
  90. ':miserable:' => 'onion_miserable.gif',
  91. ':donotwant:' => 'onion_donotwant.gif',
  92. ':furious:' => 'onion_furious.gif',
  93. ':bow:' => 'onion_bow.gif',
  94. ':perfect:' => 'onion_perfect.gif',
  95. ':puzzled:' => 'onion_puzzled.gif',
  96. ':tired:' => 'onion_tired.gif',
  97. ':angry:' => 'onion_angry.gif',
  98. ':inlove:' => 'onion_inlove.gif',
  99. ':fever:' => 'onion_fever.gif',
  100. ':warn:' => 'onion_warn.gif',
  101. ':shy:' => 'onion_shy.gif',
  102. ':flattered:' => 'onion_flattered.gif',
  103. ':attention:' => 'onion_attention.gif',
  104. ':payup:' => 'onion_payup.gif',
  105. ':exhausted:' => 'onion_exhausted.gif',
  106. ':nosepick:' => 'onion_nosepick.gif',
  107. ':blush:' => 'onion_blush.gif',
  108. ':pissed:' => 'onion_pissed.gif',
  109. ':omg:' => 'onion_omg.gif',
  110. ':xd:' => 'onion_xd.gif',
  111. ':petrified:' => 'onion_petrified.gif',
  112. ':innocence:' => 'onion_innocence.gif',
  113. ':tantrum:' => 'onion_tantrum.gif',
  114. ':cry:' => 'onion_cry.gif',
  115. ':hero:' => 'onion_hero.gif',
  116. ':abandoned:' => 'onion_abandoned.gif',
  117. ':notagain:' => 'onion_notagain.gif',
  118. ':wait:' => 'onion_wait.gif',
  119. ':runaway:' => 'onion_runaway.gif',
  120. ':surprised:' => 'onion_surprised.gif',
  121. ':moe:' => 'onion_moe.gif',
  122. );
  123. /**
  124. * Processed version of the $Smileys array, see {@link smileys}
  125. * @var array $ProcessedSmileys
  126. */
  127. private static $ProcessedSmileys = [];
  128. /**
  129. * Whether or not to turn images into URLs (used inside [quote] tags).
  130. * This is an integer reflecting the number of levels we're doing that
  131. * transition, i.e. images will only be displayed as images if $NoImg <= 0.
  132. * By setting this variable to a negative number you can delay the
  133. * transition to a deeper level of quotes.
  134. * @var int $NoImg
  135. */
  136. private static $NoImg = 0;
  137. /**
  138. * Internal counter for the level of recursion in to_html
  139. * @var int $Levels
  140. */
  141. private static $Levels = 0;
  142. /**
  143. * The maximum amount of nesting allowed (exclusive)
  144. * In reality n-1 nests are shown.
  145. * @var int $MaximumNests
  146. */
  147. private static $MaximumNests = 10;
  148. /**
  149. * Used to detect and disable parsing (e.g. TOC) within quotes
  150. * @var int $InQuotes
  151. */
  152. private static $InQuotes = 0;
  153. /**
  154. * Used to [hide] quote trains starting with the specified depth (inclusive)
  155. * @var int $NestsBeforeHide
  156. *
  157. * This defaulted to 5 but was raised to 10 to effectively "disable" it until
  158. * an optimal number of nested [quote] tags is chosen. The variable $MaximumNests
  159. * effectively overrides this variable, if $MaximumNests is less than the value
  160. * of $NestsBeforeHide.
  161. */
  162. private static $NestsBeforeHide = 10;
  163. /**
  164. * Array of headlines for Table Of Contents (TOC)
  165. * @var array $HeadLines
  166. */
  167. private static $Headlines;
  168. /**
  169. * Counter for making headline URLs unique
  170. * @var int $HeadLines
  171. */
  172. private static $HeadlineID = 0;
  173. /**
  174. * Depth
  175. * @var array $HeadlineLevels
  176. */
  177. private static $HeadlineLevels = array('1', '2', '3', '4');
  178. /**
  179. * TOC enabler
  180. * @var bool $TOC
  181. */
  182. public static $TOC = false;
  183. /**
  184. * Output BBCode as XHTML
  185. * @param string $Str BBCode text
  186. * @param bool $OutputTOC Ouput TOC near (above) text
  187. * @param int $Min See {@link parse_toc}
  188. * @return string
  189. */
  190. public static function full_format($Str, $OutputTOC = true, $Min = 3)
  191. {
  192. global $Debug;
  193. $Debug->set_flag('BBCode start');
  194. $Str = display_str($Str);
  195. self::$Headlines = [];
  196. $Str = preg_replace('/\[\\[(ch|uch)]\]/i', '', $Str);
  197. $Str = preg_replace('/\[ch\]/i', '[ch][/ch]', $Str);
  198. $Str = preg_replace('/\[uch\]/i', '[uch][/uch]', $Str);
  199. //Inline links
  200. $URLPrefix = '(\[url\]|\[url\=|\[img\=|\[img\])';
  201. $Str = preg_replace('/'.$URLPrefix.'\s+/i', '$1', $Str);
  202. $Str = preg_replace('/(?<!'.$URLPrefix.')http(s)?:\/\//i', '$1[inlineurl]http$2://', $Str);
  203. $Str = preg_replace('/\[embed\]\[inlineurl\]/', '[embed]', $Str);
  204. // For anonym.to and archive.org links, remove any [inlineurl] in the middle of the link
  205. $Str = preg_replace_callback('/(?<=\[inlineurl\]|'.$URLPrefix.')(\S*\[inlineurl\]\S*)/m', function ($matches) {
  206. return str_replace("[inlineurl]", "", $matches[0]);
  207. }, $Str);
  208. if (self::$TOC) {
  209. $Str = preg_replace('/(\={5})([^=].*)\1/i', '[headline=4]$2[/headline]', $Str);
  210. $Str = preg_replace('/(\={4})([^=].*)\1/i', '[headline=3]$2[/headline]', $Str);
  211. $Str = preg_replace('/(\={3})([^=].*)\1/i', '[headline=2]$2[/headline]', $Str);
  212. $Str = preg_replace('/(\={2})([^=].*)\1/i', '[headline=1]$2[/headline]', $Str);
  213. } else {
  214. $Str = preg_replace('/(\={4})([^=].*)\1/i', '[inlinesize=3]$2[/inlinesize]', $Str);
  215. $Str = preg_replace('/(\={3})([^=].*)\1/i', '[inlinesize=5]$2[/inlinesize]', $Str);
  216. $Str = preg_replace('/(\={2})([^=].*)\1/i', '[inlinesize=7]$2[/inlinesize]', $Str);
  217. }
  218. $HTML = nl2br(self::to_html(self::parse($Str)));
  219. if (self::$TOC && $OutputTOC) {
  220. $HTML = self::parse_toc($Min) . $HTML;
  221. }
  222. $Debug->set_flag('BBCode end');
  223. return $HTML;
  224. }
  225. public static function strip_bbcode($Str)
  226. {
  227. $Str = display_str($Str);
  228. //Inline links
  229. $Str = preg_replace('/(?<!(\[url\]|\[url\=|\[img\=|\[img\]))http(s)?:\/\//i', '$1[inlineurl]http$2://', $Str);
  230. return nl2br(self::raw_text(self::parse($Str)));
  231. }
  232. private static function valid_url($Str, $Extension = '', $Inline = false)
  233. {
  234. $Regex = '/^';
  235. $Regex .= '(https?|ftps?|irc):\/\/'; // protocol
  236. $Regex .= '(\w+(:\w+)?@)?'; // user:pass@
  237. $Regex .= '(';
  238. $Regex .= '(([0-9]{1,3}\.){3}[0-9]{1,3})|'; // IP or...
  239. $Regex .= '(([a-z0-9\-\_]+\.)+\w{2,6})'; // sub.sub.sub.host.com
  240. $Regex .= ')';
  241. $Regex .= '(:[0-9]{1,5})?'; // port
  242. $Regex .= '\/?'; // slash?
  243. $Regex .= '(\/?[0-9a-z\-_.,\?&=@~%\/:;()+|!#]+)*'; // /file
  244. if (!empty($Extension)) {
  245. $Regex.=$Extension;
  246. }
  247. // query string
  248. if ($Inline) {
  249. $Regex .= '(\?([0-9a-z\-_.,%\/\@~&=:;()+*\^$!#|?]|\[\d*\])*)?';
  250. } else {
  251. $Regex .= '(\?[0-9a-z\-_.,%\/\@[\]~&=:;()+*\^$!#|?]*)?';
  252. }
  253. $Regex .= '(#[a-z0-9\-_.,%\/\@[\]~&=:;()+*\^$!]*)?'; // #anchor
  254. $Regex .= '$/i';
  255. return preg_match($Regex, $Str, $Matches);
  256. }
  257. public static function local_url($Str)
  258. {
  259. $URLInfo = parse_url($Str);
  260. if (!$URLInfo) {
  261. return false;
  262. }
  263. $Host = $URLInfo['host'];
  264. // If for some reason your site does not require subdomains or contains a directory in the SITE_DOMAIN, revert to the line below.
  265. if ($Host == SITE_DOMAIN || $Host == 'www.'.SITE_DOMAIN) {
  266. if (empty($URLInfo['port']) && preg_match('/(\S+\.)*'.SITE_DOMAIN.'/', $Host)) {
  267. $URL = '';
  268. if (!empty($URLInfo['path'])) {
  269. $URL .= ltrim($URLInfo['path'], '/'); // Things break if the path starts with '//'
  270. }
  271. if (!empty($URLInfo['query'])) {
  272. $URL .= "?$URLInfo[query]";
  273. }
  274. if (!empty($URLInfo['fragment'])) {
  275. $URL .= "#$URLInfo[fragment]";
  276. }
  277. return $URL ? "/$URL" : false;
  278. } else {
  279. return false;
  280. }
  281. }
  282. }
  283. /*
  284. How parsing works
  285. Parsing takes $Str, breaks it into blocks, and builds it into $Array.
  286. Blocks start at the beginning of $Str, when the parser encounters a [, and after a tag has been closed.
  287. This is all done in a loop.
  288. EXPLANATION OF PARSER LOGIC
  289. 1) Find the next tag (regex)
  290. 1a) If there aren't any tags left, write everything remaining to a block and return (done parsing)
  291. 1b) If the next tag isn't where the pointer is, write everything up to there to a text block.
  292. 2) See if it's a [[wiki-link]] or an ordinary tag, and get the tag name
  293. 3) If it's not a wiki link:
  294. 3a) check it against the self::$ValidTags array to see if it's actually a tag and not [bullshit]
  295. If it's [not a tag], just leave it as plaintext and move on
  296. 3b) Get the attribute, if it exists [name=attribute]
  297. 4) Move the pointer past the end of the tag
  298. 5) Find out where the tag closes (beginning of [/tag])
  299. 5a) Different for different types of tag. Some tags don't close, others are weird like [*]
  300. 5b) If it's a normal tag, it may have versions of itself nested inside - e.g.:
  301. [quote=bob]*
  302. [quote=joe]I am a redneck!**[/quote]
  303. Me too!
  304. ***[/quote]
  305. If we're at the position *, the first [/quote] tag is denoted by **.
  306. However, our quote tag doesn't actually close there. We must perform
  307. a loop which checks the number of opening [quote] tags, and make sure
  308. they are all closed before we find our final [/quote] tag (***).
  309. 5c) Get the contents between [open] and [/close] and call it the block.
  310. In many cases, this will be parsed itself later on, in a new parse() call.
  311. 5d) Move the pointer past the end of the [/close] tag.
  312. 6) Depending on what type of tag we're dealing with, create an array with the attribute and block.
  313. In many cases, the block may be parsed here itself. Stick them in the $Array.
  314. 7) Increment array pointer, start again (past the end of the [/close] tag)
  315. */
  316. private static function parse($Str)
  317. {
  318. $i = 0; // Pointer to keep track of where we are in $Str
  319. $Len = strlen($Str);
  320. $Array = [];
  321. $ArrayPos = 0;
  322. $StrLC = strtolower($Str);
  323. while ($i < $Len) {
  324. $Block = '';
  325. // 1) Find the next tag (regex)
  326. // [name(=attribute)?]|[[wiki-link]]
  327. $IsTag = preg_match("/((\[[a-zA-Z*#]+)(=(?:[^\n'\"\[\]]|\[\d*\])+)?\])|(\[\[[^\n\"'\[\]]+\]\])/", $Str, $Tag, PREG_OFFSET_CAPTURE, $i);
  328. // 1a) If there aren't any tags left, write everything remaining to a block
  329. if (!$IsTag) {
  330. // No more tags
  331. $Array[$ArrayPos] = substr($Str, $i);
  332. break;
  333. }
  334. // 1b) If the next tag isn't where the pointer is, write everything up to there to a text block.
  335. $TagPos = $Tag[0][1];
  336. if ($TagPos > $i) {
  337. $Array[$ArrayPos] = substr($Str, $i, $TagPos - $i);
  338. ++$ArrayPos;
  339. $i = $TagPos;
  340. }
  341. // 2) See if it's a [[wiki-link]] or an ordinary tag, and get the tag name
  342. if (!empty($Tag[4][0])) { // Wiki-link
  343. $WikiLink = true;
  344. $TagName = substr($Tag[4][0], 2, -2);
  345. $Attrib = '';
  346. } else { // 3) If it's not a wiki link:
  347. $WikiLink = false;
  348. $TagName = strtolower(substr($Tag[2][0], 1));
  349. //3a) check it against the self::$ValidTags array to see if it's actually a tag and not [bullshit]
  350. if (!isset(self::$ValidTags[$TagName])) {
  351. $Array[$ArrayPos] = substr($Str, $i, ($TagPos - $i) + strlen($Tag[0][0]));
  352. $i = $TagPos + strlen($Tag[0][0]);
  353. ++$ArrayPos;
  354. continue;
  355. }
  356. $MaxAttribs = self::$ValidTags[$TagName];
  357. // 3b) Get the attribute, if it exists [name=attribute]
  358. if (!empty($Tag[3][0])) {
  359. $Attrib = substr($Tag[3][0], 1);
  360. } else {
  361. $Attrib = '';
  362. }
  363. }
  364. // 4) Move the pointer past the end of the tag
  365. $i = $TagPos + strlen($Tag[0][0]);
  366. // 5) Find out where the tag closes (beginning of [/tag])
  367. // Unfortunately, BBCode doesn't have nice standards like XHTML
  368. // [*], [img=...], and http:// follow different formats
  369. // Thus, we have to handle these before we handle the majority of tags
  370. //5a) Different for different types of tag. Some tags don't close, others are weird like [*]
  371. if ($TagName == 'img' && !empty($Tag[3][0])) { //[img=...]
  372. $Block = ''; // Nothing inside this tag
  373. // Don't need to touch $i
  374. } elseif ($TagName == 'inlineurl') { // We did a big replace early on to turn http:// into [inlineurl]http://
  375. // Let's say the block can stop at a newline or a space
  376. $CloseTag = strcspn($Str, " \n\r", $i);
  377. if ($CloseTag === false) { // block finishes with URL
  378. $CloseTag = $Len;
  379. }
  380. if (preg_match('/[!,.?:]+$/', substr($Str, $i, $CloseTag), $Match)) {
  381. $CloseTag -= strlen($Match[0]);
  382. }
  383. $URL = substr($Str, $i, $CloseTag);
  384. if (substr($URL, -1) == ')' && substr_count($URL, '(') < substr_count($URL, ')')) {
  385. $CloseTag--;
  386. $URL = substr($URL, 0, -1);
  387. }
  388. $Block = $URL; // Get the URL
  389. // strcspn returns the number of characters after the offset $i, not after the beginning of the string
  390. // Therefore, we use += instead of the = everywhere else
  391. $i += $CloseTag; // 5d) Move the pointer past the end of the [/close] tag.
  392. } elseif ($WikiLink == true || $TagName == 'n') {
  393. // Don't need to do anything - empty tag with no closing
  394. } elseif ($TagName === '*' || $TagName === '#') {
  395. // We're in a list. Find where it ends
  396. $NewLine = $i;
  397. do { // Look for \n[*]
  398. $NewLine = strpos($Str, "\n", $NewLine + 1);
  399. } while ($NewLine !== false && substr($Str, $NewLine + 1, 3) == "[$TagName]");
  400. $CloseTag = $NewLine;
  401. if ($CloseTag === false) { // block finishes with list
  402. $CloseTag = $Len;
  403. }
  404. $Block = substr($Str, $i, $CloseTag - $i); // Get the list
  405. $i = $CloseTag; // 5d) Move the pointer past the end of the [/close] tag.
  406. } else {
  407. //5b) If it's a normal tag, it may have versions of itself nested inside
  408. $CloseTag = $i - 1;
  409. $InTagPos = $i - 1;
  410. $NumInOpens = 0;
  411. $NumInCloses = -1;
  412. $InOpenRegex = '/\[('.$TagName.')';
  413. if ($MaxAttribs > 0) {
  414. $InOpenRegex .= "(=[^\n'\"\[\]]+)?";
  415. }
  416. $InOpenRegex .= '\]/i';
  417. // Every time we find an internal open tag of the same type, search for the next close tag
  418. // (as the first close tag won't do - it's been opened again)
  419. do {
  420. $CloseTag = strpos($StrLC, "[/$TagName]", $CloseTag + 1);
  421. if ($CloseTag === false) {
  422. $CloseTag = $Len;
  423. break;
  424. } else {
  425. $NumInCloses++; // Majority of cases
  426. }
  427. // Is there another open tag inside this one?
  428. $OpenTag = preg_match($InOpenRegex, $Str, $InTag, PREG_OFFSET_CAPTURE, $InTagPos + 1);
  429. if (!$OpenTag || $InTag[0][1] > $CloseTag) {
  430. break;
  431. } else {
  432. $InTagPos = $InTag[0][1];
  433. $NumInOpens++;
  434. }
  435. } while ($NumInOpens > $NumInCloses);
  436. // Find the internal block inside the tag
  437. $Block = substr($Str, $i, $CloseTag - $i); // 5c) Get the contents between [open] and [/close] and call it the block.
  438. $i = $CloseTag + strlen($TagName) + 3; // 5d) Move the pointer past the end of the [/close] tag.
  439. }
  440. // 6) Depending on what type of tag we're dealing with, create an array with the attribute and block.
  441. switch ($TagName) {
  442. case 'inlineurl':
  443. $Array[$ArrayPos] = array('Type'=>'inlineurl', 'Attr'=>$Block, 'Val'=>'');
  444. break;
  445. case 'url':
  446. $Array[$ArrayPos] = array('Type'=>'img', 'Attr'=>$Attrib, 'Val'=>$Block);
  447. if (empty($Attrib)) { // [url]http://...[/url] - always set URL to attribute
  448. $Array[$ArrayPos] = array('Type'=>'url', 'Attr'=>$Block, 'Val'=>'');
  449. } else {
  450. $Array[$ArrayPos] = array('Type'=>'url', 'Attr'=>$Attrib, 'Val'=>self::parse($Block));
  451. }
  452. break;
  453. case 'quote':
  454. $Array[$ArrayPos] = array('Type'=>'quote', 'Attr'=>self::parse($Attrib), 'Val'=>self::parse($Block));
  455. break;
  456. case 'img':
  457. case 'image':
  458. if (empty($Block)) {
  459. $Block = $Attrib;
  460. }
  461. $Array[$ArrayPos] = array('Type'=>'img', 'Val'=>$Block);
  462. break;
  463. case 'aud':
  464. case 'mp3':
  465. case 'audio':
  466. if (empty($Block)) {
  467. $Block = $Attrib;
  468. }
  469. $Array[$ArrayPos] = array('Type'=>'aud', 'Val'=>$Block);
  470. break;
  471. case 'user':
  472. $Array[$ArrayPos] = array('Type'=>'user', 'Val'=>$Block);
  473. break;
  474. case 'artist':
  475. $Array[$ArrayPos] = array('Type'=>'artist', 'Val'=>$Block);
  476. break;
  477. case 'torrent':
  478. $Array[$ArrayPos] = array('Type'=>'torrent', 'Val'=>$Block);
  479. break;
  480. case 'tex':
  481. $Array[$ArrayPos] = array('Type'=>'tex', 'Val'=>$Block);
  482. break;
  483. case 'rule':
  484. $Array[$ArrayPos] = array('Type'=>'rule', 'Val'=>$Block);
  485. break;
  486. case 'pre':
  487. case 'code':
  488. case 'plain':
  489. $Block = strtr($Block, array('[inlineurl]' => ''));
  490. $Callback = function ($matches) {
  491. $n = $matches[2];
  492. $text = '';
  493. if ($n < 5 && $n > 0) {
  494. $e = str_repeat('=', $matches[2] + 1);
  495. $text = $e . $matches[3] . $e;
  496. }
  497. return $text;
  498. };
  499. $Block = preg_replace_callback('/\[(headline)\=(\d)\](.*?)\[\/\1\]/i', $Callback, $Block);
  500. $Block = preg_replace('/\[inlinesize\=3\](.*?)\[\/inlinesize\]/i', '====$1====', $Block);
  501. $Block = preg_replace('/\[inlinesize\=5\](.*?)\[\/inlinesize\]/i', '===$1===', $Block);
  502. $Block = preg_replace('/\[inlinesize\=7\](.*?)\[\/inlinesize\]/i', '==$1==', $Block);
  503. $Array[$ArrayPos] = array('Type'=>$TagName, 'Val'=>$Block);
  504. break;
  505. case 'spoiler':
  506. case 'hide':
  507. $Array[$ArrayPos] = array('Type'=>'hide', 'Attr'=>$Attrib, 'Val'=>self::parse($Block));
  508. break;
  509. case 'embed':
  510. $Array[$ArrayPos] = array('Type'=>'embed', 'Val'=>$Block);
  511. break;
  512. case '#':
  513. case '*':
  514. $Array[$ArrayPos] = array('Type'=>'list');
  515. $Array[$ArrayPos]['Val'] = explode("[$TagName]", $Block);
  516. $Array[$ArrayPos]['ListType'] = $TagName === '*' ? 'ul' : 'ol';
  517. $Array[$ArrayPos]['Tag'] = $TagName;
  518. foreach ($Array[$ArrayPos]['Val'] as $Key => $Val) {
  519. $Array[$ArrayPos]['Val'][$Key] = self::parse(trim($Val));
  520. }
  521. break;
  522. case 'n':
  523. $ArrayPos--;
  524. break; // n serves only to disrupt bbcode (backwards compatibility - use [pre])
  525. default:
  526. if ($WikiLink == true) {
  527. $Array[$ArrayPos] = array('Type'=>'wiki','Val'=>$TagName);
  528. } else {
  529. // Basic tags, like [b] or [size=5]
  530. $Array[$ArrayPos] = array('Type'=>$TagName, 'Val'=>self::parse($Block));
  531. if (!empty($Attrib) && $MaxAttribs > 0) {
  532. $Array[$ArrayPos]['Attr'] = strtolower($Attrib);
  533. }
  534. }
  535. }
  536. $ArrayPos++; // 7) Increment array pointer, start again (past the end of the [/close] tag)
  537. }
  538. return $Array;
  539. }
  540. /**
  541. * Generates a navigation list for TOC
  542. * @param int $Min Minimum number of headlines required for a TOC list
  543. */
  544. public static function parse_toc($Min = 3)
  545. {
  546. if (count(self::$Headlines) > $Min) {
  547. $list = '<ol class="navigation_list">';
  548. $i = 0;
  549. $level = 0;
  550. $off = 0;
  551. foreach (self::$Headlines as $t) {
  552. $n = (int)$t[0];
  553. if ($i === 0 && $n > 1) {
  554. $off = $n - $level;
  555. }
  556. self::headline_level($n, $level, $list, $i, $off);
  557. $list .= sprintf('<li><a href="#%2$s">%1$s</a>', $t[1], $t[2]);
  558. $level = $t[0];
  559. $off = 0;
  560. $i++;
  561. }
  562. $list .= str_repeat('</li></ol>', $level);
  563. $list .= "\n\n";
  564. return $list;
  565. }
  566. }
  567. /**
  568. * Generates the list items and proper depth
  569. *
  570. * First check if the item should be higher than the current level
  571. * - Close the list and previous lists
  572. *
  573. * Then check if the item should go lower than the current level
  574. * - If the list doesn't open on level one, use the Offset
  575. * - Open appropriate sub lists
  576. *
  577. * Otherwise the item is on the same as level as the previous item
  578. *
  579. * @param int $ItemLevel Current item level
  580. * @param int $Level Current list level
  581. * @param str $List reference to an XHTML string
  582. * @param int $i Iterator digit
  583. * @param int $Offset If the list doesn't start at level 1
  584. */
  585. private static function headline_level(&$ItemLevel, &$Level, &$List, $i, &$Offset)
  586. {
  587. if ($ItemLevel < $Level) {
  588. $diff = $Level - $ItemLevel;
  589. $List .= '</li>' . str_repeat('</ol></li>', $diff);
  590. } elseif ($ItemLevel > $Level) {
  591. $diff = $ItemLevel - $Level;
  592. if ($Offset > 0) {
  593. $List .= str_repeat('<li><ol>', $Offset - 2);
  594. }
  595. if ($ItemLevel > 1) {
  596. $List .= $i === 0 ? '<li>' : '';
  597. $List .= "\n<ol>\n";
  598. }
  599. } else {
  600. $List .= $i > 0 ? '</li>' : '<li>';
  601. }
  602. }
  603. private static function to_html($Array)
  604. {
  605. self::$Levels++;
  606. /*
  607. * Hax prevention
  608. * That's the original comment on this.
  609. * Most likely this was implemented to avoid anyone nesting enough
  610. * elements to reach PHP's memory limit as nested elements are
  611. * solved recursively.
  612. * Original value of 10, it is now replaced in favor of
  613. * $MaximumNests.
  614. * If this line is ever executed then something is, infact
  615. * being haxed as the if before the block type switch for different
  616. * tags should always be limiting ahead of this line.
  617. * (Larger than vs. smaller than.)
  618. */
  619. if (self::$Levels > self::$MaximumNests) {
  620. return $Block['Val']; // Hax prevention, breaks upon exceeding nests.
  621. }
  622. $Str = '';
  623. foreach ($Array as $Block) {
  624. if (is_string($Block)) {
  625. $Str .= self::smileys($Block);
  626. continue;
  627. }
  628. if (self::$Levels < self::$MaximumNests) {
  629. switch ($Block['Type']) {
  630. case 'b':
  631. $Str .= '<strong>'.self::to_html($Block['Val']).'</strong>';
  632. break;
  633. case 'u':
  634. $Str .= '<span style="text-decoration: underline;">'.self::to_html($Block['Val']).'</span>';
  635. break;
  636. case 'i':
  637. $Str .= '<span style="font-style: italic;">'.self::to_html($Block['Val'])."</span>";
  638. break;
  639. case 's':
  640. $Str .= '<span style="text-decoration: line-through;">'.self::to_html($Block['Val']).'</span>';
  641. break;
  642. case 'important':
  643. $Str .= '<strong class="important_text">'.self::to_html($Block['Val']).'</strong>';
  644. break;
  645. case 'user':
  646. $Str .= '<a href="user.php?action=search&amp;search='.urlencode($Block['Val']).'">'.$Block['Val'].'</a>';
  647. break;
  648. case 'artist':
  649. $Str .= '<a href="artist.php?artistname='.urlencode(Format::undisplay_str($Block['Val'])).'">'.$Block['Val'].'</a>';
  650. break;
  651. case 'rule':
  652. $Rule = trim(strtolower($Block['Val']));
  653. if ($Rule[0] != 'r' && $Rule[0] != 'h') {
  654. $Rule = 'r'.$Rule;
  655. }
  656. $Str .= '<a href="rules.php?p=upload#'.urlencode(Format::undisplay_str($Rule)).'">'.preg_replace('/[aA-zZ]/', '', $Block['Val']).'</a>';
  657. break;
  658. case 'torrent':
  659. $Pattern = '/('.SITE_DOMAIN.'\/torrents\.php.*[\?&]id=)?(\d+)($|&|\#).*/i';
  660. $Matches = [];
  661. if (preg_match($Pattern, $Block['Val'], $Matches)) {
  662. if (isset($Matches[2])) {
  663. $GroupID = $Matches[2];
  664. $Groups = Torrents::get_groups(array($GroupID), true, true, false);
  665. if ($Groups[$GroupID]) {
  666. $Group = $Groups[$GroupID];
  667. $Str .= Artists::display_artists($Group['Artists']).'<a href="torrents.php?id='.$GroupID;
  668. if (preg_match('/torrentid=(\d+)/i', $Block['Val'], $Matches)) {
  669. $Str .= '&torrentid='.$Matches[1];
  670. }
  671. $Str .= '"';
  672. if (!isset($LoggedUser['CoverArt']) || $LoggedUser['CoverArt']) {
  673. $Str .= ' data-cover="'.ImageTools::process($Group['WikiImage'], 'thumb').'"';
  674. }
  675. $Name = empty($Group['Name']) ? (empty($Group['NameRJ']) ? $Group['NameJP'] : $Group['NameRJ']) : $Group['Name'];
  676. $Str .= '>'.$Name.'</a>';
  677. } else {
  678. $Str .= '[torrent]'.str_replace('[inlineurl]', '', $Block['Val']).'[/torrent]';
  679. }
  680. }
  681. } else {
  682. $Str .= '[torrent]'.str_replace('[inlineurl]', '', $Block['Val']).'[/torrent]';
  683. }
  684. break;
  685. case 'wiki':
  686. $Str .= '<a href="wiki.php?action=article&amp;name='.urlencode($Block['Val']).'">'.$Block['Val'].'</a>';
  687. break;
  688. case 'tex':
  689. $Str .= '<img class="tex_img" style="vertical-align: middle;" src="'.STATIC_SERVER.'blank.gif" onload="if (this.src.substr(this.src.length - 9, this.src.length) == \'blank.gif\') { this.src = \''.ImageTools::process('https://chart.googleapis.com/chart?cht=tx&chf=bg,s,FFFFFF00&chl='.urlencode(mb_convert_encoding($Block['Val'], 'UTF-8', 'HTML-ENTITIES'))).'\'; }" alt="'.$Block['Val'].'" />';
  690. break;
  691. case 'plain':
  692. $Str .= $Block['Val'];
  693. break;
  694. case 'pre':
  695. $Str .= '<pre>'.$Block['Val'].'</pre>';
  696. break;
  697. case 'code':
  698. $Str .= '<code>'.$Block['Val'].'</code>';
  699. break;
  700. case 'ch':
  701. $Str .= '<input type="checkbox" checked="checked" disabled="disabled">';
  702. break;
  703. case 'uch':
  704. $Str .= '<input type="checkbox" disabled="disabled">';
  705. break;
  706. case 'list':
  707. $Str .= "<$Block[ListType] class=\"postlist\">";
  708. foreach ($Block['Val'] as $Line) {
  709. $Str .= '<li>'.self::to_html($Line).'</li>';
  710. }
  711. $Str .= '</'.$Block['ListType'].'>';
  712. break;
  713. case 'align':
  714. $ValidAttribs = array('left', 'center', 'right');
  715. if (!in_array($Block['Attr'], $ValidAttribs)) {
  716. $Str .= '[align='.$Block['Attr'].']'.self::to_html($Block['Val']).'[/align]';
  717. } else {
  718. $Str .= '<div style="text-align: '.$Block['Attr'].';">'.self::to_html($Block['Val']).'</div>';
  719. }
  720. break;
  721. case 'color':
  722. case 'colour':
  723. $ValidAttribs = array('aqua', 'black', 'blue', 'fuchsia', 'green', 'grey', 'lime', 'maroon', 'navy', 'olive', 'purple', 'red', 'silver', 'teal', 'white', 'yellow');
  724. if (!in_array($Block['Attr'], $ValidAttribs) && !preg_match('/^#[0-9a-f]{6}$/', $Block['Attr'])) {
  725. $Str .= '[color='.$Block['Attr'].']'.self::to_html($Block['Val']).'[/color]';
  726. } else {
  727. $Str .= '<span style="color: '.$Block['Attr'].';">'.self::to_html($Block['Val']).'</span>';
  728. }
  729. break;
  730. case 'headline':
  731. $text = self::to_html($Block['Val']);
  732. $raw = self::raw_text($Block['Val']);
  733. if (!in_array($Block['Attr'], self::$HeadlineLevels)) {
  734. $Str .= sprintf('%1$s%2$s%1$s', str_repeat('=', $Block['Attr'] + 1), $text);
  735. } else {
  736. $id = '_' . crc32($raw . self::$HeadlineID);
  737. if (self::$InQuotes === 0) {
  738. self::$Headlines[] = array($Block['Attr'], $raw, $id);
  739. }
  740. $Str .= sprintf('<h%1$d id="%3$s">%2$s</h%1$d>', ($Block['Attr'] + 2), $text, $id);
  741. self::$HeadlineID++;
  742. }
  743. break;
  744. case 'inlinesize':
  745. case 'size':
  746. $ValidAttribs = array('1', '2', '3', '4', '5', '6', '7', '8', '9', '10');
  747. if (!in_array($Block['Attr'], $ValidAttribs)) {
  748. $Str .= '[size='.$Block['Attr'].']'.self::to_html($Block['Val']).'[/size]';
  749. } else {
  750. $Str .= '<span class="size'.$Block['Attr'].'">'.self::to_html($Block['Val']).'</span>';
  751. }
  752. break;
  753. case 'quote':
  754. self::$NoImg++; // No images inside quote tags
  755. self::$InQuotes++;
  756. if (self::$InQuotes == self::$NestsBeforeHide) { //Put quotes that are nested beyond the specified limit in [hide] tags.
  757. $Str .= '<strong>Older quotes</strong>: <a class="spoilerButton">Show</a>';
  758. $Str .= '<blockquote class="hidden spoiler">';
  759. }
  760. if (!empty($Block['Attr'])) {
  761. $Exploded = explode('|', self::to_html($Block['Attr']));
  762. if (isset($Exploded[1]) && (is_numeric($Exploded[1]) || (in_array($Exploded[1][0], array('a', 't', 'c', 'r')) && is_numeric(substr($Exploded[1], 1))))) {
  763. // the part after | is either a number or starts with a, t, c or r, followed by a number (forum post, artist comment, torrent comment, collage comment or request comment, respectively)
  764. $PostID = trim($Exploded[1]);
  765. $Str .= '<a data-quote-jump="'.$PostID.'"><strong class="quoteheader">'.$Exploded[0].'</strong> wrote: </a>';
  766. } else {
  767. $Str .= '<strong class="quoteheader">'.$Exploded[0].'</strong> wrote: ';
  768. }
  769. }
  770. $Str .= '<blockquote>'.self::to_html($Block['Val']).'</blockquote>';
  771. if (self::$InQuotes == self::$NestsBeforeHide) { //Close quote the deeply nested quote [hide].
  772. $Str .= '</blockquote><br />'; // Ensure new line after quote train hiding
  773. }
  774. self::$NoImg--;
  775. self::$InQuotes--;
  776. break;
  777. case 'hide':
  778. $Str .= '<strong>'.(($Block['Attr']) ? $Block['Attr'] : 'Hidden text').'</strong>: <a class="spoilerButton">Show</a>';
  779. $Str .= '<blockquote class="hidden spoiler">'.self::to_html($Block['Val']).'</blockquote>';
  780. break;
  781. case 'img':
  782. if (self::$NoImg > 0 && self::valid_url($Block['Val'])) {
  783. $Str .= '<a rel="noreferrer" target="_blank" href="'.$Block['Val'].'">'.$Block['Val'].'</a> (image)';
  784. break;
  785. }
  786. if (!self::valid_url($Block['Val'], '\.(jpe?g|gif|png|bmp|tiff)')) {
  787. $Str .= '[img]'.$Block['Val'].'[/img]';
  788. } else {
  789. $LocalURL = self::local_url($Block['Val']);
  790. if ($LocalURL) {
  791. $Str .= '<img class="scale_image lightbox-init" alt="'.$Block['Val'].'" src="'.$LocalURL.'" />';
  792. } else {
  793. $Str .= '<img class="scale_image lightbox-init" alt="'.$Block['Val'].'" src="'.ImageTools::process($Block['Val']).'" />';
  794. }
  795. }
  796. break;
  797. case 'aud':
  798. if (self::$NoImg > 0 && self::valid_url($Block['Val'])) {
  799. $Str .= '<a rel="noreferrer" target="_blank" href="'.$Block['Val'].'">'.$Block['Val'].'</a> (audio)';
  800. break;
  801. }
  802. if (!self::valid_url($Block['Val'], '\.(mp3|ogg|wav)')) {
  803. $Str .= '[aud]'.$Block['Val'].'[/aud]';
  804. } else {
  805. //TODO: Proxy this for staff?
  806. $Str .= '<audio controls="controls" src="'.$Block['Val'].'"><a rel="noreferrer" target="_blank" href="'.$Block['Val'].'">'.$Block['Val'].'</a></audio>';
  807. }
  808. break;
  809. case 'url':
  810. // Make sure the URL has a label
  811. if (empty($Block['Val'])) {
  812. $Block['Val'] = $Block['Attr'];
  813. $NoName = true; // If there isn't a Val for this
  814. } else {
  815. $Block['Val'] = self::to_html($Block['Val']);
  816. $NoName = false;
  817. }
  818. if (!self::valid_url($Block['Attr'])) {
  819. $Str .= '[url='.$Block['Attr'].']'.$Block['Val'].'[/url]';
  820. } else {
  821. $LocalURL = self::local_url($Block['Attr']);
  822. if ($LocalURL) {
  823. if ($NoName) {
  824. $Block['Val'] = substr($LocalURL, 1);
  825. }
  826. $Str .= '<a href="'.$LocalURL.'">'.$Block['Val'].'</a>';
  827. } else {
  828. $Str .= '<a rel="noreferrer" target="_blank" href="'.$Block['Attr'].'">'.$Block['Val'].'</a>';
  829. }
  830. }
  831. break;
  832. case 'inlineurl':
  833. if (!self::valid_url($Block['Attr'], '', true)) {
  834. $Array = self::parse($Block['Attr']);
  835. $Block['Attr'] = $Array;
  836. $Str .= self::to_html($Block['Attr']);
  837. } else {
  838. $LocalURL = self::local_url($Block['Attr']);
  839. if ($LocalURL) {
  840. $Str .= '<a href="'.$LocalURL.'">'.substr($LocalURL, 1).'</a>';
  841. } else {
  842. $Str .= '<a rel="noreferrer" target="_blank" href="'.$Block['Attr'].'">'.$Block['Attr'].'</a>';
  843. }
  844. }
  845. break;
  846. case 'embed':
  847. $Val = str_replace(' ', '', $Block['Val']);
  848. if (self::valid_url($Val) && substr($Val, -4) == 'webm') {
  849. $Str .= '<video class="webm" preload controls><source src="'.ImageTools::process($Val).'" /></video>';
  850. }
  851. break;
  852. }
  853. }
  854. }
  855. self::$Levels--;
  856. return $Str;
  857. }
  858. private static function raw_text($Array)
  859. {
  860. $Str = '';
  861. foreach ($Array as $Block) {
  862. if (is_string($Block)) {
  863. $Str .= $Block;
  864. continue;
  865. }
  866. switch ($Block['Type']) {
  867. case 'headline':
  868. break;
  869. case 'b':
  870. case 'u':
  871. case 'i':
  872. case 's':
  873. case 'color':
  874. case 'size':
  875. case 'quote':
  876. case 'align':
  877. $Str .= self::raw_text($Block['Val']);
  878. break;
  879. case 'tex': //since this will never strip cleanly, just remove it
  880. break;
  881. case 'artist':
  882. case 'user':
  883. case 'wiki':
  884. case 'pre':
  885. case 'code':
  886. case 'aud':
  887. case 'img':
  888. $Str .= $Block['Val'];
  889. break;
  890. case 'list':
  891. foreach ($Block['Val'] as $Line) {
  892. $Str .= $Block['Tag'].self::raw_text($Line);
  893. }
  894. break;
  895. case 'url':
  896. // Make sure the URL has a label
  897. if (empty($Block['Val'])) {
  898. $Block['Val'] = $Block['Attr'];
  899. } else {
  900. $Block['Val'] = self::raw_text($Block['Val']);
  901. }
  902. $Str .= $Block['Val'];
  903. break;
  904. case 'inlineurl':
  905. if (!self::valid_url($Block['Attr'], '', true)) {
  906. $Array = self::parse($Block['Attr']);
  907. $Block['Attr'] = $Array;
  908. $Str .= self::raw_text($Block['Attr']);
  909. } else {
  910. $Str .= $Block['Attr'];
  911. }
  912. break;
  913. }
  914. }
  915. return $Str;
  916. }
  917. private static function smileys($Str)
  918. {
  919. if (!empty(G::$LoggedUser['DisableSmileys'])) {
  920. return $Str;
  921. }
  922. if (count(self::$ProcessedSmileys) == 0 && count(self::$Smileys) > 0) {
  923. foreach (self::$Smileys as $Key => $Val) {
  924. self::$ProcessedSmileys[$Key] = '<img src="'.STATIC_SERVER.'common/smileys/'.$Val.'" alt="" />';
  925. }
  926. reset(self::$ProcessedSmileys);
  927. }
  928. $Str = strtr($Str, self::$ProcessedSmileys);
  929. return $Str;
  930. }
  931. }
  932. /*
  933. // Uncomment this part to test the class via command line:
  934. function display_str($Str) {
  935. return $Str;
  936. }
  937. function check_perms($Perm) {
  938. return true;
  939. }
  940. $Str = "hello
  941. [pre]http://anonym.to/?http://whatshirts.portmerch.com/
  942. ====hi====
  943. ===hi===
  944. ==hi==[/pre]
  945. ====hi====
  946. hi";
  947. echo Text::full_format($Str);
  948. echo "\n"
  949. */