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

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