max number of attributes * @var array $ValidTags */ private static $ValidTags = [ 'b' => 0, 'u' => 0, 'i' => 0, 's' => 0, '*' => 0, '#' => 0, #'ch' => 0, #'uch' => 0, 'artist' => 0, 'user' => 0, 'n' => 0, 'inlineurl' => 0, 'inlinesize' => 1, 'headline' => 1, 'align' => 1, 'color' => 1, 'colour' => 1, 'size' => 1, 'url' => 1, 'img' => 1, 'quote' => 1, 'pre' => 1, 'code' => 1, 'tex' => 0, 'hide' => 1, 'spoiler' => 1, 'plain' => 0, 'important' => 0, 'torrent' => 0, 'rule' => 0, 'embed' => 0, ]; /** * Array of smilies; code => image file in STATIC_SERVER/common/smileys * @var array $Smileys */ private static $Smileys = []; /** * Processed version of the $Smileys array, see {@link smileys} * @var array $ProcessedSmileys */ private static $ProcessedSmileys = []; /** * Whether or not to turn images into URLs (used inside [quote] tags). * This is an integer reflecting the number of levels we're doing that * transition, i.e. images will only be displayed as images if $NoImg <= 0. * By setting this variable to a negative number you can delay the * transition to a deeper level of quotes. * @var int $NoImg */ private static $NoImg = 0; /** * Internal counter for the level of recursion in to_html * @var int $Levels */ private static $Levels = 0; /** * The maximum amount of nesting allowed (exclusive) * In reality n-1 nests are shown. * @var int $MaximumNests */ private static $MaximumNests = 10; /** * Used to detect and disable parsing (e.g. TOC) within quotes * @var int $InQuotes */ private static $InQuotes = 0; /** * Used to [hide] quote trains starting with the specified depth (inclusive) * @var int $NestsBeforeHide * * This defaulted to 5 but was raised to 10 to effectively "disable" it until * an optimal number of nested [quote] tags is chosen. The variable $MaximumNests * effectively overrides this variable, if $MaximumNests is less than the value * of $NestsBeforeHide. */ private static $NestsBeforeHide = 10; /** * Array of headlines for Table of Contents (TOC) * @var array $HeadLines */ private static $Headlines; /** * Counter for making headline URLs unique * @var int $HeadLines */ private static $HeadlineID = 0; /** * Depth * @var array $HeadlineLevels */ private static $HeadlineLevels = array('1', '2', '3', '4'); /** * TOC enabler * @var bool $TOC */ public static $TOC = false; /** * Output BBCode as XHTML * * This used to suck and now it only blows. * If $Str isn't BBcode, call Parsedown in safe mode. * Otherwise, continue with the default parser. * * @param string $Str BBCode text * @param bool $OutputTOC Ouput TOC near (above) text * @param int $Min See {@link parse_toc} * @return string */ public static function full_format($Str, $OutputTOC = true, $Min = 3) { $ENV = ENV::go(); # Here's the magic pattern: if (!preg_match( "/$ENV->BBCODE_REGEX/s", $Str )) { $Parsedown = new ParsedownExtra(); $Parsedown->setSafeMode(true); $Str = html_entity_decode($Str, ENT_QUOTES | ENT_HTML5, 'UTF-8'); # Markdown ToC not happening yet # Shouldn't parse_toc() output HTML /* self::$Headlines = []; if (self::$TOC) { $List = []; foreach (count(1, 6) as $x) { array_push( $List, preg_replace( "/(\#{$x})([^#].*)\1/i", "$2", $Str ) ); } } */ return $P = $Parsedown->text($Str); /* return $P = ((self::$TOC && $OutputTOC) ? self::parse_toc($Min) : null) . $Parsedown->text($Str); */ } else { global $Debug; $Debug->set_flag('BBCode start'); self::$Headlines = []; $Str = display_str($Str); # Checkboxes: broken and stupid /* $Str = preg_replace('/\[\\[(ch|uch)]\]/i', '', $Str); $Str = preg_replace('/\[ch\]/i', '[ch][/ch]', $Str); $Str = preg_replace('/\[uch\]/i', '[uch][/uch]', $Str); */ // Inline links $URLPrefix = '(\[url\]|\[url\=|\[img\=|\[img\])'; $Str = preg_replace('/'.$URLPrefix.'\s+/i', '$1', $Str); $Str = preg_replace('/(?set_flag('BBCode end'); return $HTML; } } /** * strip_bbcode() */ public static function strip_bbcode($Str) { $Str = display_str($Str); // Inline links $Str = preg_replace( '/(? $i) { $Array[$ArrayPos] = substr($Str, $i, $TagPos - $i); ++$ArrayPos; $i = $TagPos; } // 2) See if it's a [[wiki-link]] or an ordinary tag, and get the tag name if (!empty($Tag[4][0])) { // Wiki-link $WikiLink = true; $TagName = substr($Tag[4][0], 2, -2); $Attrib = ''; } else { // 3) If it's not a wiki link: $WikiLink = false; $TagName = strtolower(substr($Tag[2][0], 1)); // 3a) check it against the self::$ValidTags array to see if it's actually a tag and not [bullshit] if (!isset(self::$ValidTags[$TagName])) { $Array[$ArrayPos] = substr($Str, $i, ($TagPos - $i) + strlen($Tag[0][0])); $i = $TagPos + strlen($Tag[0][0]); ++$ArrayPos; continue; } $MaxAttribs = self::$ValidTags[$TagName]; // 3b) Get the attribute, if it exists [name=attribute] if (!empty($Tag[3][0])) { $Attrib = substr($Tag[3][0], 1); } else { $Attrib = ''; } } // 4) Move the pointer past the end of the tag $i = $TagPos + strlen($Tag[0][0]); // 5) Find out where the tag closes (beginning of [/tag]) // Unfortunately, BBCode doesn't have nice standards like XHTML // [*], [img=...], and http:// follow different formats // Thus, we have to handle these before we handle the majority of tags // 5a) Different for different types of tag. Some tags don't close, others are weird like [*] if ($TagName === 'img' && !empty($Tag[3][0])) { //[img=...] $Block = ''; // Nothing inside this tag // Don't need to touch $i } elseif ($TagName === 'inlineurl') { // We did a big replace early on to turn http:// into [inlineurl]http:// // Let's say the block can stop at a newline or a space $CloseTag = strcspn($Str, " \n\r", $i); if ($CloseTag === false) { // Block finishes with URL $CloseTag = $Len; } if (preg_match('/[!,.?:]+$/', substr($Str, $i, $CloseTag), $Match)) { $CloseTag -= strlen($Match[0]); } $URL = substr($Str, $i, $CloseTag); if (substr($URL, -1) === ')' && substr_count($URL, '(') < substr_count($URL, ')')) { $CloseTag--; $URL = substr($URL, 0, -1); } $Block = $URL; // Get the URL // strcspn returns the number of characters after the offset $i, not after the beginning of the string // Therefore, we use += instead of the = everywhere else $i += $CloseTag; // 5d) Move the pointer past the end of the [/close] tag. } elseif ($WikiLink === true || $TagName === 'n') { // Don't need to do anything - empty tag with no closing } elseif ($TagName === '*' || $TagName === '#') { // We're in a list. Find where it ends $NewLine = $i; do { // Look for \n[*] $NewLine = strpos($Str, "\n", $NewLine + 1); } while ($NewLine !== false && substr($Str, $NewLine + 1, 3) === "[$TagName]"); $CloseTag = $NewLine; if ($CloseTag === false) { // Block finishes with list $CloseTag = $Len; } $Block = substr($Str, $i, $CloseTag - $i); // Get the list $i = $CloseTag; // 5d) Move the pointer past the end of the [/close] tag. } else { // 5b) If it's a normal tag, it may have versions of itself nested inside $CloseTag = $i - 1; $InTagPos = $i - 1; $NumInOpens = 0; $NumInCloses = -1; $InOpenRegex = '/\[('.$TagName.')'; if ($MaxAttribs > 0) { $InOpenRegex .= "(=[^\n'\"\[\]]+)?"; } $InOpenRegex .= '\]/i'; // Every time we find an internal open tag of the same type, search for the next close tag // (as the first close tag won't do - it's been opened again) do { $CloseTag = strpos($StrLC, "[/$TagName]", $CloseTag + 1); if ($CloseTag === false) { $CloseTag = $Len; break; } else { $NumInCloses++; // Majority of cases } // Is there another open tag inside this one? $OpenTag = preg_match($InOpenRegex, $Str, $InTag, PREG_OFFSET_CAPTURE, $InTagPos + 1); if (!$OpenTag || $InTag[0][1] > $CloseTag) { break; } else { $InTagPos = $InTag[0][1]; $NumInOpens++; } } while ($NumInOpens > $NumInCloses); // Find the internal block inside the tag $Block = substr($Str, $i, $CloseTag - $i); // 5c) Get the contents between [open] and [/close] and call it the block $i = $CloseTag + strlen($TagName) + 3; // 5d) Move the pointer past the end of the [/close] tag } // 6) Depending on what type of tag we're dealing with, create an array with the attribute and block switch ($TagName) { case 'inlineurl': $Array[$ArrayPos] = array('Type' => 'inlineurl', 'Attr' => $Block, 'Val' => ''); break; case 'url': $Array[$ArrayPos] = array('Type' => 'img', 'Attr' => $Attrib, 'Val' => $Block); if (empty($Attrib)) { // [url]http://...[/url] - always set URL to attribute $Array[$ArrayPos] = array('Type' => 'url', 'Attr' => $Block, 'Val' => ''); } else { $Array[$ArrayPos] = array('Type' => 'url', 'Attr' => $Attrib, 'Val' => self::parse($Block)); } break; case 'quote': $Array[$ArrayPos] = array('Type' => 'quote', 'Attr' => self::parse($Attrib), 'Val' => self::parse($Block)); break; case 'img': case 'image': if (empty($Block)) { $Block = $Attrib; } $Array[$ArrayPos] = array('Type' => 'img', 'Val' => $Block); break; case 'aud': case 'mp3': case 'audio': if (empty($Block)) { $Block = $Attrib; } $Array[$ArrayPos] = array('Type' => 'aud', 'Val' => $Block); break; case 'user': $Array[$ArrayPos] = array('Type' => 'user', 'Val' => $Block); break; case 'artist': $Array[$ArrayPos] = array('Type' => 'artist', 'Val' => $Block); break; case 'torrent': $Array[$ArrayPos] = array('Type' => 'torrent', 'Val' => $Block); break; case 'tex': $Array[$ArrayPos] = array('Type' => 'tex', 'Val' => $Block); break; case 'rule': $Array[$ArrayPos] = array('Type' => 'rule', 'Val' => $Block); break; case 'pre': case 'code': case 'plain': $Block = strtr($Block, array('[inlineurl]' => '')); $Callback = function ($matches) { $n = $matches[2]; $text = ''; if ($n < 5 && $n > 0) { $e = str_repeat('=', $matches[2] + 1); $text = $e . $matches[3] . $e; } return $text; }; $Block = preg_replace_callback('/\[(headline)\=(\d)\](.*?)\[\/\1\]/i', $Callback, $Block); $Block = preg_replace('/\[inlinesize\=3\](.*?)\[\/inlinesize\]/i', '====$1====', $Block); $Block = preg_replace('/\[inlinesize\=5\](.*?)\[\/inlinesize\]/i', '===$1===', $Block); $Block = preg_replace('/\[inlinesize\=7\](.*?)\[\/inlinesize\]/i', '==$1==', $Block); $Array[$ArrayPos] = array('Type' => $TagName, 'Val' => $Block); break; case 'spoiler': case 'hide': $Array[$ArrayPos] = array('Type' => 'hide', 'Attr' => $Attrib, 'Val' => self::parse($Block)); break; case 'embed': $Array[$ArrayPos] = array('Type' => 'embed', 'Val' => $Block); break; case '#': case '*': $Array[$ArrayPos] = array('Type' => 'list'); $Array[$ArrayPos]['Val'] = explode("[$TagName]", $Block); $Array[$ArrayPos]['ListType'] = $TagName === '*' ? 'ul' : 'ol'; $Array[$ArrayPos]['Tag'] = $TagName; foreach ($Array[$ArrayPos]['Val'] as $Key => $Val) { $Array[$ArrayPos]['Val'][$Key] = self::parse(trim($Val)); } break; case 'n': $ArrayPos--; break; // n serves only to disrupt bbcode (backwards compatibility - use [pre]) default: if ($WikiLink === true) { $Array[$ArrayPos] = array('Type' => 'wiki','Val' => $TagName); } else { // Basic tags, like [b] or [size=5] $Array[$ArrayPos] = array('Type' => $TagName, 'Val' => self::parse($Block)); if (!empty($Attrib) && $MaxAttribs > 0) { $Array[$ArrayPos]['Attr'] = strtolower($Attrib); } } } $ArrayPos++; // 7) Increment array pointer, start again (past the end of the [/close] tag) } return $Array; } /** * Generates a navigation list for TOC * @param int $Min Minimum number of headlines required for a TOC list */ public static function parse_toc($Min = 3) { if (count(self::$Headlines) > $Min) { $list = '', $level); $list .= "\n\n"; return $list; } } /** * Generates the list items and proper depth * * First check if the item should be higher than the current level * - Close the list and previous lists * * Then check if the item should go lower than the current level * - If the list doesn't open on level one, use the Offset * - Open appropriate sub lists * * Otherwise the item is on the same as level as the previous item * * @param int $ItemLevel Current item level * @param int $Level Current list level * @param str $List reference to an XHTML string * @param int $i Iterator digit * @param int $Offset If the list doesn't start at level 1 */ private static function headline_level(&$ItemLevel, &$Level, &$List, $i, &$Offset) { if ($ItemLevel < $Level) { $diff = $Level - $ItemLevel; $List .= '' . str_repeat('', $diff); } elseif ($ItemLevel > $Level) { $diff = $ItemLevel - $Level; if ($Offset > 0) { $List .= str_repeat('
    1. ', $Offset - 2); } if ($ItemLevel > 1) { $List .= $i === 0 ? '
    2. ' : ''; $List .= "\n
        \n"; } } else { $List .= $i > 0 ? '' : '
      1. '; } } /* * Hax prevention * * That's the original comment on this. * Most likely this was implemented to avoid anyone nesting enough elements * to reach PHP's memory limit as nested elements are solved recursively. * Original value of 10, it is now replaced in favor of $MaximumNests. * * If this line is ever executed then something is, in fact, being haxed. * The if before the block type switch for different tags should always be limiting ahead of this line. */ private static function to_html($Array) { self::$Levels++; // Hax prevention, breaks upon exceeding nests if (self::$Levels > self::$MaximumNests) { return $Block['Val']; } $Str = ''; foreach ($Array as $Block) { if (is_string($Block)) { $Str .= self::smileys($Block); continue; } if (self::$Levels < self::$MaximumNests) { switch ($Block['Type']) { case 'b': $Str .= ''.self::to_html($Block['Val']).''; break; case 'u': $Str .= ''.self::to_html($Block['Val']).''; break; case 'i': $Str .= ''.self::to_html($Block['Val']).""; break; case 's': $Str .= ''.self::to_html($Block['Val']).''; break; case 'important': $Str .= ''.self::to_html($Block['Val']).''; break; case 'user': $Str .= ''.$Block['Val'].''; break; case 'artist': $Str .= ''.$Block['Val'].''; break; case 'rule': $Rule = trim(strtolower($Block['Val'])); if ($Rule[0] !== 'r' && $Rule[0] !== 'h') { $Rule = 'r'.$Rule; } $Str .= ''.preg_replace('/[aA-zZ]/', '', $Block['Val']).''; break; case 'torrent': $Pattern = '/('.SITE_DOMAIN.'\/torrents\.php.*[\?&]id=)?(\d+)($|&|\#).*/i'; $Matches = []; if (preg_match($Pattern, $Block['Val'], $Matches)) { if (isset($Matches[2])) { $GroupID = $Matches[2]; $Groups = Torrents::get_groups(array($GroupID), true, true, false); if ($Groups[$GroupID]) { $Group = $Groups[$GroupID]; $Str .= Artists::display_artists($Group['Artists']).''; } else { $Str .= '[torrent]'.str_replace('[inlineurl]', '', $Block['Val']).'[/torrent]'; } } } else { $Str .= '[torrent]'.str_replace('[inlineurl]', '', $Block['Val']).'[/torrent]'; } break; case 'wiki': $Str .= ''.$Block['Val'].''; break; case 'tex': $Str .= ''.$Block['Val'].''; break; case 'plain': $Str .= $Block['Val']; break; case 'pre': $Str .= '
        '.$Block['Val'].'
        '; break; case 'code': $Str .= ''.$Block['Val'].''; break; /* case 'ch': $Str .= ''; break; */ /* case 'uch': $Str .= ''; break; */ case 'list': $Str .= "<$Block[ListType] class=\"postlist\">"; foreach ($Block['Val'] as $Line) { $Str .= '
      2. '.self::to_html($Line).'
      3. '; } $Str .= ''; break; case 'align': $ValidAttribs = array('left', 'center', 'right'); if (!in_array($Block['Attr'], $ValidAttribs)) { $Str .= '[align='.$Block['Attr'].']'.self::to_html($Block['Val']).'[/align]'; } else { $Str .= '
        '.self::to_html($Block['Val']).'
        '; } break; case 'color': case 'colour': $ValidAttribs = array('aqua', 'black', 'blue', 'fuchsia', 'green', 'grey', 'lime', 'maroon', 'navy', 'olive', 'purple', 'red', 'silver', 'teal', 'white', 'yellow'); if (!in_array($Block['Attr'], $ValidAttribs) && !preg_match('/^#[0-9a-f]{6}$/', $Block['Attr'])) { $Str .= '[color='.$Block['Attr'].']'.self::to_html($Block['Val']).'[/color]'; } else { $Str .= ''.self::to_html($Block['Val']).''; } break; case 'headline': $text = self::to_html($Block['Val']); $raw = self::raw_text($Block['Val']); if (!in_array($Block['Attr'], self::$HeadlineLevels)) { $Str .= sprintf('%1$s%2$s%1$s', str_repeat('=', $Block['Attr'] + 1), $text); } else { $id = '_' . crc32($raw . self::$HeadlineID); if (self::$InQuotes === 0) { self::$Headlines[] = array($Block['Attr'], $raw, $id); } $Str .= sprintf('%2$s', ($Block['Attr'] + 2), $text, $id); self::$HeadlineID++; } break; case 'inlinesize': case 'size': $ValidAttribs = array('1', '2', '3', '4', '5', '6', '7', '8', '9', '10'); if (!in_array($Block['Attr'], $ValidAttribs)) { $Str .= '[size='.$Block['Attr'].']'.self::to_html($Block['Val']).'[/size]'; } else { $Str .= ''.self::to_html($Block['Val']).''; } break; case 'quote': self::$NoImg++; // No images inside quote tags self::$InQuotes++; if (self::$InQuotes === self::$NestsBeforeHide) { // Put quotes that are nested beyond the specified limit in [hide] tags. $Str .= 'Older quotes: Show'; $Str .= '
        '; // Ensure new line after quote train hiding } self::$NoImg--; self::$InQuotes--; break; case 'hide': $Str .= ''.(($Block['Attr']) ? $Block['Attr'] : 'Hidden text').': Show'; $Str .= ''; break; case 'img': if (self::$NoImg > 0 && self::valid_url($Block['Val'])) { $Str .= ''.$Block['Val'].' (image)'; break; } if (!self::valid_url($Block['Val'], '\.(jpe?g|gif|png|bmp|tiff)')) { $Str .= '[img]'.$Block['Val'].'[/img]'; } else { $LocalURL = self::local_url($Block['Val']); if ($LocalURL) { $Str .= ''.$Block['Val'].''; } else { $Str .= ''.$Block['Val'].''; } } break; case 'aud': if (self::$NoImg > 0 && self::valid_url($Block['Val'])) { $Str .= ''.$Block['Val'].' (audio)'; break; } if (!self::valid_url($Block['Val'], '\.(mp3|ogg|wav)')) { $Str .= '[aud]'.$Block['Val'].'[/aud]'; } else { // todo: Proxy this for staff? $Str .= ''; } break; case 'url': // Make sure the URL has a label if (empty($Block['Val'])) { $Block['Val'] = $Block['Attr']; $NoName = true; // If there isn't a Val for this } else { $Block['Val'] = self::to_html($Block['Val']); $NoName = false; } if (!self::valid_url($Block['Attr'])) { $Str .= '[url='.$Block['Attr'].']'.$Block['Val'].'[/url]'; } else { $LocalURL = self::local_url($Block['Attr']); if ($LocalURL) { if ($NoName) { $Block['Val'] = substr($LocalURL, 1); } $Str .= ''.$Block['Val'].''; } else { $Str .= ''.$Block['Val'].''; } } break; case 'inlineurl': if (!self::valid_url($Block['Attr'], '', true)) { $Array = self::parse($Block['Attr']); $Block['Attr'] = $Array; $Str .= self::to_html($Block['Attr']); } else { $LocalURL = self::local_url($Block['Attr']); if ($LocalURL) { $Str .= ''.substr($LocalURL, 1).''; } else { $Str .= ''.$Block['Attr'].''; } } break; case 'embed': $Val = str_replace(' ', '', $Block['Val']); if (self::valid_url($Val) && substr($Val, -4) === 'webm') { $Str .= ''; } break; } } } self::$Levels--; return $Str; } /** * raw_text() */ private static function raw_text($Array) { $Str = ''; foreach ($Array as $Block) { if (is_string($Block)) { $Str .= $Block; continue; } switch ($Block['Type']) { case 'headline': break; case 'b': case 'u': case 'i': case 's': case 'color': case 'size': case 'quote': case 'align': $Str .= self::raw_text($Block['Val']); break; case 'tex': // Since this will never strip cleanly, just remove it break; case 'artist': case 'user': case 'wiki': case 'pre': case 'code': case 'aud': case 'img': $Str .= $Block['Val']; break; case 'list': foreach ($Block['Val'] as $Line) { $Str .= $Block['Tag'].self::raw_text($Line); } break; case 'url': // Make sure the URL has a label if (empty($Block['Val'])) { $Block['Val'] = $Block['Attr']; } else { $Block['Val'] = self::raw_text($Block['Val']); } $Str .= $Block['Val']; break; case 'inlineurl': if (!self::valid_url($Block['Attr'], '', true)) { $Array = self::parse($Block['Attr']); $Block['Attr'] = $Array; $Str .= self::raw_text($Block['Attr']); } else { $Str .= $Block['Attr']; } break; } } return $Str; } /** * smileys() */ private static function smileys($Str) { return $Str; /* if (!empty(G::$LoggedUser['DisableSmileys'])) { return $Str; } if (count(self::$ProcessedSmileys) === 0 && count(self::$Smileys) > 0) { foreach (self::$Smileys as $Key => $Val) { self::$ProcessedSmileys[$Key] = ''; } reset(self::$ProcessedSmileys); } $Str = strtr($Str, self::$ProcessedSmileys); return $Str; */ } } /* // Uncomment this part to test the class via command line: function display_str($Str) { return $Str; } function check_perms($Perm) { return true; } $Str = "hello [pre]http://anonym.to/?http://whatshirts.portmerch.com/ ====hi==== ===hi=== ==hi==[/pre] ====hi==== hi"; echo Text::full_format($Str); echo "\n" */