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 41KB

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