BioTorrents.de’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 42KB

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