<?php
namespace XF\Str;
use XF\Template\Templater;
use function chr, count, intval, is_int, is_string, strlen, strval;
class Formatter
{
protected $censorRules = [];
protected $censorChar = '*';
protected $censorCache = null;
protected $smilieTranslate = [];
protected $smilieReverse = [];
/**
* @var callable|null
*/
protected $smilieHtmlPather = null;
/**
* @var callable|null
*/
protected $proxyHandler;
protected $htmlPlaceholderId = 0;
/**
* @var EmojiFormatter|null
*/
protected $emojiFormatter;
public function censorText($string, $censorChar = null)
{
if ($string === null)
{
return '';
}
if ($censorChar !== null)
{
$map = $this->buildCensorMap($this->censorRules, $censorChar);
}
else
{
if ($this->censorCache === null)
{
$this->censorCache = $this->buildCensorMap($this->censorRules, $this->censorChar);
}
$map = $this->censorCache;
}
if ($map)
{
$string = preg_replace(
array_keys($map),
$map,
$string
);
}
return $string;
}
public function setCensorRules(array $censorRules, $censorChar)
{
$this->censorRules = $censorRules;
$this->censorChar = $censorChar;
}
protected function buildCensorMap(array $censor, $censorCharacter)
{
$map = [];
foreach ($censor AS $key => $word)
{
if (is_string($key) || !isset($word['regex']) || !isset($word['replace']))
{
// old format or broken
continue;
}
$regex = $word['regex'];
$replace = $word['replace'];
$map[$regex] = is_int($replace) ? str_repeat($censorCharacter, $replace) : $replace;
}
return $map;
}
public function replacePhrasePlaceholders($string, \XF\Language $language = null)
{
if (!preg_match_all(
'#\{phrase:(\w+)\}#iU', $string, $phraseMatches, PREG_SET_ORDER
))
{
return $string;
}
if (!$language)
{
$language = \XF::language();
}
$replacements = [];
foreach ($phraseMatches AS $phraseMatch)
{
$replacements[$phraseMatch[0]] = $language->phrase($phraseMatch[1]);
}
return strtr($string, $replacements);
}
public function replacePhraseSyntax($value, \XF\Language $language = null)
{
if (!preg_match_all(
'#\{\{\s*phrase\(("|\')([a-z0-9_.():,]+)\\1(,\s*\{([^}]+)\})?\s*\)\s*\}\}#iU',
$value, $phraseMatches, PREG_SET_ORDER
))
{
return $value;
}
if (!$language)
{
$language = \XF::language();
}
$replacements = [];
foreach ($phraseMatches AS $phraseMatch)
{
$phraseParams = [];
if (!empty($phraseMatch[4]))
{
preg_match_all('#("|\')(\w+)\\1\s*:\s*("|\')(.*)\\3#siU',
$phraseMatch[4], $paramMatches, PREG_SET_ORDER
);
foreach ($paramMatches AS $paramMatch)
{
$phraseParams[$paramMatch[2]] = $paramMatch[4];
}
}
$replacements[$phraseMatch[0]] = $language->phrase($phraseMatch[2], $phraseParams);
}
if (count($replacements) == 1 && key($replacements) == $value)
{
return current($replacements);
}
return $replacements ? strtr($value, $replacements) : $value;
}
public function addSmilies(array $smilies)
{
foreach ($smilies AS $smilie)
{
foreach ($smilie['smilieText'] AS $text)
{
$this->smilieTranslate[$text] = "\0" . $smilie['smilie_id'] . "\0";
}
$this->smilieReverse[$smilie['smilie_id']] = $smilie;
}
}
public function getSmilieStrings()
{
return array_keys($this->smilieTranslate);
}
public function setSmilieHtmlPather(callable $pather = null)
{
$this->smilieHtmlPather = $pather;
}
public function replaceSmiliesInText($text, $replaceCallback, $escapeCallback = null)
{
if ($this->smilieTranslate)
{
$text = strtr($text, $this->smilieTranslate);
}
if ($escapeCallback)
{
/** @var callable $escapeCallback */
$text = $escapeCallback($text);
}
if ($this->smilieTranslate)
{
$reverse = $this->smilieReverse;
$text = preg_replace_callback('#\0(\d+)\0#', function($match) use ($reverse, $replaceCallback)
{
$id = $match[1];
return isset($reverse[$id]) ? $replaceCallback($id, $reverse[$id]) : '';
}, $text);
}
return $text;
}
protected $smilieCache = [];
public function replaceSmiliesHtml($text)
{
$cache = &$this->smilieCache;
$replace = function($id, $smilie) use (&$cache)
{
if (isset($cache[$id]))
{
return $cache[$id];
}
$html = $this->getDefaultSmilieHtml($id, $smilie);
$cache[$id] = $html;
return $html;
};
return $this->replaceSmiliesInText($text, $replace, 'htmlspecialchars');
}
public function getDefaultSmilieHtml($id, array $smilie)
{
$smilieTitle = htmlspecialchars($smilie['title']);
$smilieText = htmlspecialchars(reset($smilie['smilieText']));
$pather = $this->smilieHtmlPather;
if (empty($smilie['sprite_params']))
{
$url = htmlspecialchars($pather ? $pather($smilie['image_url'], 'base') : $smilie['image_url']);
$srcSet = '';
if (!empty($smilie['image_url_2x']))
{
$url2x = htmlspecialchars($pather ? $pather($smilie['image_url_2x'], 'base') : $smilie['image_url_2x']);
$srcSet = 'srcset="' . $url . ' 1x, ' . $url2x . ' 2x"';
}
return '<img src="' . $url . '" ' . $srcSet . ' class="smilie" loading="lazy" alt="' . $smilieText
. '" title="' . $smilieTitle . ' ' . $smilieText . '" '
. 'data-shortname="' . $smilieText . '" />';
}
else
{
// embed a data URI to avoid a request that doesn't respect paths fully
return '<img src="' . Templater::TRANSPARENT_IMG_URI . '" class="smilie smilie--sprite smilie--sprite' . $id . '" alt="' . $smilieText
. '" title="' . $smilieTitle . ' ' . $smilieText . '" loading="lazy" '
. 'data-shortname="' . $smilieText . '" />';
}
}
public function convertStructuredTextToHtml($string, $nl2br = true)
{
$string = $this->censorText($string);
$string = \XF::escapeString($string);
$string = $this->getEmojiFormatter()->formatEmojiToImage($string);
$string = $this->autoLinkStructuredText($string);
$string = $this->linkStructuredTextMentions($string);
if ($nl2br)
{
$string = nl2br($string);
}
return $string;
}
public function moveHtmlToPlaceholders($string, &$restorerClosure)
{
$placeholders = [];
$string = preg_replace_callback(
'#<[^>]*>#si',
function (array $match) use (&$placeholders, &$placeholderPosition)
{
$placeholder = "\x1A" . $this->htmlPlaceholderId . "\x1A";
$placeholders[$placeholder] = $match[0];
$this->htmlPlaceholderId++;
return $placeholder;
},
$string
);
$restorerClosure = function($string) use ($placeholders)
{
return strtr($string, $placeholders);
};
return $string;
}
public function removeHtmlPlaceholders($string)
{
return preg_replace("#\x1A\\d+\x1A#", '', $string);
}
public function moveHtmlEntitiesToPlaceholders($string, &$restorerClosure)
{
$placeholders = [];
$string = preg_replace_callback(
'/&(?:[a-z\d]+|#\d+|#x[a-f\d]+);/i',
function ($match) use (&$placeholders, &$placeholderPosition)
{
$placeholder = "\x1A" . $this->htmlPlaceholderId . "\x1A";
$placeholders[$placeholder] = $match[0];
$this->htmlPlaceholderId++;
return $placeholder;
},
$string
);
$restorerClosure = function($string) use ($placeholders)
{
$string = preg_replace("/(\x1A)<[^>]*>(\d+)<[^>]*>(\x1A)/", "$1$2$3", $string);
$string = strtr($string, $placeholders);
// sanity check in case the manipulation has left a placeholder
$string = preg_replace("/\x1A.*\x1A/U", '', $string);
return $string;
};
return $string;
}
public function autoLinkStructuredText($string)
{
$string = $this->moveHtmlToPlaceholders($string, $restorePlaceholders);
$string = preg_replace_callback(
'#(?<=[^a-z0-9@-]|^)(https?://|www\.)[^\s"<>{}`\x1A]+#i',
function (array $match)
{
$url = $this->removeHtmlPlaceholders($match[0]);
$url = htmlspecialchars_decode($url, ENT_QUOTES);
$link = $this->prepareAutoLinkedUrl($url);
if (!$link['url'])
{
return htmlspecialchars($url, ENT_QUOTES, 'utf-8');
}
$linkInfo = $this->getLinkClassTarget($link['url']);
$classAttr = $linkInfo['class'] ? " class=\"$linkInfo[class]\"" : '';
$targetAttr = $linkInfo['target'] ? " target=\"$linkInfo[target]\"" : '';
$noFollowAttr = $linkInfo['trusted'] ? '' : ' rel="nofollow"';
return '<a href="' . htmlspecialchars($link['url'], ENT_QUOTES, 'utf-8')
. "\"{$classAttr}{$noFollowAttr}{$targetAttr}>"
. htmlspecialchars($link['linkText'], ENT_QUOTES, 'utf-8') . '</a>'
. htmlspecialchars($link['suffixText'], ENT_QUOTES, 'utf-8');
},
$string
);
$string = $restorePlaceholders($string);
return $string;
}
public function convertStructuredTextLinkToBbCode($string)
{
$string = preg_replace_callback(
'#(?<=[^a-z0-9@/\.-]|^)(?<!\]\(|url=(?:"|\')|url\]|url\sunfurl=(?:"|\')true(?:"|\')\]|img\])(https?://|www\.)[^\s"<>{}`]+#iu',
function (array $match)
{
$link = $this->prepareAutoLinkedUrl($match[0]);
return '[URL]' . $link['url'] . '[/URL]' . $link['suffixText'];
},
$string
);
return $string;
}
public function getLinkClassTarget($url)
{
$target = '_blank';
$class = 'link link--external';
$type = 'external';
$schemeMatch = true;
$urlInfo = @parse_url($url);
if ($urlInfo)
{
if (empty($urlInfo['host']))
{
$isInternal = true;
}
else
{
$request = \XF::app()->request();
$host = $urlInfo['host'] . (!empty($urlInfo['port']) ? ":$urlInfo[port]" : '');
$isInternal = ($host == $request->getHost());
$scheme = (!empty($urlInfo['scheme']) ? strtolower($urlInfo['scheme']) : 'http');
$schemeMatch = $scheme == ($request->isSecure() ? 'https' : 'http');
}
if ($isInternal)
{
$target = '';
$class = 'link link--internal';
$type = 'internal';
}
}
return [
'class' => $class,
'target' => $target,
'type' => $type,
'trusted' => $type == 'internal',
'local' => $type == 'internal' && $schemeMatch
];
}
public function prepareAutoLinkedUrl($url, array $options = [])
{
$options = array_replace([
'processTrailers' => true
], $options);
$suffixText = '';
if (preg_match('/&(?:quot|gt|lt);/i', $url, $match, PREG_OFFSET_CAPTURE))
{
$suffixText = substr($url, $match[0][1]);
$url = substr($url, 0, $match[0][1]);
}
$linkText = $url;
if (strpos($url, '://') === false)
{
$url = 'http://' . $url;
}
if ($options['processTrailers'])
{
do
{
$matchedTrailer = false;
$lastChar = substr($url, -1);
switch ($lastChar)
{
case ')':
case ']':
$closer = $lastChar;
$opener = $lastChar == ']' ? '[' : '(';
if (substr_count($url, $closer) == substr_count($url, $opener))
{
break;
}
// break missing intentionally
case '(':
case '[':
case '.':
case ',':
case '!':
case ':':
case "'":
$suffixText = $lastChar . $suffixText;
$url = substr($url, 0, -1);
$linkText = substr($linkText, 0, -1);
$matchedTrailer = true;
break;
}
}
while ($matchedTrailer);
}
if (preg_match('/proxy\.php\?\w+=(http[^&]+)&/i', $url, $match))
{
// proxy link of some sort, adjust to the original one
$proxiedUrl = urldecode($match[1]);
if (preg_match('/./su', $proxiedUrl))
{
if ($proxiedUrl == $linkText)
{
$linkText = $proxiedUrl;
}
$url = $proxiedUrl;
}
}
if (!\XF::app()->validator('Url')->isValid($url))
{
$url = null;
}
return [
'url' => $url,
'linkText' => $linkText,
'suffixText' => $suffixText
];
}
public function linkStructuredTextMentions($string)
{
$string = $this->moveHtmlToPlaceholders($string, $restorePlaceholders);
$string = preg_replace_callback(
MentionFormatter::STRUCTURED_MENTION_REGEX,
function(array $match)
{
$userId = intval($match[1]);
$username = $this->removeHtmlPlaceholders($match[3]);
$username = htmlspecialchars($username, ENT_QUOTES, 'utf-8', false);
$link = \XF::app()->router('public')->buildLink('full:members', ['user_id' => $userId]);
return sprintf('<a href="%s" class="username" data-user-id="%d" data-username="%s" data-xf-init="member-tooltip">%s</a>',
htmlspecialchars($link), $userId, $username, $username
);
},
$string
);
$string = $restorePlaceholders($string);
return $string;
}
public function convertStructuredTextMentionsToBbCode($string)
{
$string = preg_replace_callback(
MentionFormatter::STRUCTURED_MENTION_REGEX,
function(array $match)
{
$userId = intval($match[1]);
$username = htmlspecialchars($match[3], ENT_QUOTES, 'utf-8', false);
return '[USER=' . $userId . ']' . $username . '[/USER]';
},
$string
);
return $string;
}
public function getProxiedUrlIfActive($type, $url)
{
return $this->getProxiedUrlIfActiveExtended($type, $url);
}
public function getProxiedUrlIfActiveExtended($type, $url, array $options = [])
{
if (!$this->proxyHandler)
{
return null;
}
$handler = $this->proxyHandler;
return $handler($type, $url, $options);
}
public function setProxyHandler(callable $handler = null)
{
$this->proxyHandler = $handler;
}
public function getProxyHandler()
{
return $this->proxyHandler;
}
public function splitLongWords($string, $breakLength, $inserter = null)
{
$breakLength = intval($breakLength);
if ($breakLength < 1 || $breakLength > strlen($string)) // strlen isn't completely accurate, but this is an optimization
{
return $string;
}
if ($inserter === null)
{
$inserter = chr(0xE2) . chr(0x80) . chr(0x8B); // UTF-8 for zero width space
}
return preg_replace('#[^\s]{' . $breakLength . '}(?=[^\s])#u', '$0' . $inserter, $string);
}
public function wholeWordTrim($string, $maxLength, $offset = 0, $ellipsis = '...')
{
$ellipsisLen = strlen($ellipsis);
if ($offset)
{
$string = preg_replace('/^\S*\s+/s', '', utf8_substr($string, $offset));
if ($maxLength > 0)
{
$maxLength = max(1, $maxLength - $ellipsisLen);
}
}
$strLength = utf8_strlen($string);
if ($maxLength > 0 && $strLength > $maxLength)
{
$maxLength -= $ellipsisLen;
if ($maxLength > 0)
{
$string = utf8_substr($string, 0, $maxLength);
$string = strrev(preg_replace('/^\S*\s+/s', '', strrev($string)));
$string = rtrim($string, ',.!?:;') . $ellipsis;
}
else if ($maxLength <= 0)
{
// too short with the ellipsis, can't really display anything
$string = $ellipsis;
$offset = 0;
}
}
if ($offset)
{
$string = $ellipsis . $string;
}
return $string;
}
/**
* Trims a string to a whole word, attempting to leave BB code markup but minimize its impact.
* This will replace BB code markup with a token that is much shorter (and not breakable within it)
* and trimming will be done on the resulting string. Afterwards, the token will be replaced with the
* original BB code markup value.
*
* @param string $string
* @param int $maxLength
* @param int $offset
* @param string $ellipsis
*
* @return string
*/
public function wholeWordTrimBbCode(
string $string,
int $maxLength,
int $offset = 0,
string $ellipsis = '...'
): string
{
$tokens = [];
$string = preg_replace_callback(
'#(\[\w+(?:=[^\]]*)?+\]|\[\w+(?:\s?\w+="[^"]*")+\]|\[/\w+\])#si',
function ($match) use (&$tokens)
{
$tokenId = count($tokens);
$token = "\x1A" . $tokenId . "\x1A";
$tokens[$tokenId] = $match[0];
return $token;
},
$string
);
$string = $this->wholeWordTrim($string, $maxLength, $offset, $ellipsis);
$string = preg_replace_callback(
"#\x1A(\d+)\x1A#",
function ($match) use ($tokens)
{
return $tokens[$match[1]] ?? '';
},
$string
);
$string = str_replace("\x1A", '', $string);
return $string;
}
public function wholeWordTrimAroundTerm($string, $maxLength, $term, $ellipsis = '...')
{
$stringLength = utf8_strlen($string);
if ($stringLength > $maxLength)
{
$term = strval($term);
if ($term !== '')
{
// TODO: slightly more intelligent search term matching, breaking up multiple words etc.
$termPosition = utf8_strpos(utf8_strtolower($string), utf8_strtolower($term));
}
else
{
$termPosition = false;
}
if ($termPosition !== false)
{
$startPos = $termPosition + utf8_strlen($term); // add term length to term start position
$startPos -= intval($maxLength / 2); // count back half the max characters
$startPos = max(0, $startPos); // don't overflow the beginning
$startPos = min($startPos, $stringLength - $maxLength); // don't overflow the end
}
else
{
$startPos = 0;
}
$string = $this->wholeWordTrim($string, $maxLength, $startPos, $ellipsis);
}
return $string;
}
public function highlightTermForHtml($string, $term, $class = 'textHighlight')
{
$string = $this->moveHtmlEntitiesToPlaceholders(
htmlspecialchars($string),
$restorePlaceholders
);
$term = trim(preg_replace('#((^|\s)[+|-]|[/()"~^])#', ' ', strval($term)));
if ($term !== '')
{
$string = preg_replace(
'/(?<!\\x1A|\w)(' . preg_replace('#\s+#', '|', preg_quote(htmlspecialchars($term), '/')) . ')/siu',
'<em class="' . htmlspecialchars($class) . '">\1</em>',
\XF::escapeString($string)
);
}
else
{
$string = \XF::escapeString($string);
}
$string = $restorePlaceholders($string);
return $string;
}
public function stripBbCode($string, array $options = [])
{
$options = array_merge([
'stripQuote' => false,
'hideUnviewable' => true
], $options);
if ($options['stripQuote'])
{
$parts = preg_split('#(\[quote[^\]]*\]|\[/quote\])#i', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
$string = '';
$quoteLevel = 0;
foreach ($parts AS $i => $part)
{
if ($i % 2 == 0)
{
// always text, only include if not inside quotes
if ($quoteLevel == 0)
{
$string .= rtrim($part) . "\n";
}
}
else
{
// quote start/end
if ($part[1] == '/')
{
// close tag, down a level if open
if ($quoteLevel)
{
$quoteLevel--;
}
}
else
{
// up a level
$quoteLevel++;
}
}
}
}
// replaces unviewable tags with a text representation
$string = str_replace('[*]', '', $string);
$string = preg_replace(
'#\[(attach|media|img|spoiler|ispoiler)[^\]]*\].*\[/\\1\]#siU',
$options['hideUnviewable'] ? '' : '[\\1]',
$string
);
// split the string into possible delimiters and text; even keys (from 0) are strings, odd are delimiters
$parts = preg_split('#(\[\w+(?:=[^\]]*)?+\]|\[\w+(?:\s?\w+="[^"]*")+\]|\[/\w+\])#si', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
$total = count($parts);
if ($total < 2)
{
return trim($string);
}
$closes = [];
$skips = [];
$newString = '';
// first pass: find all the closing tags and note their keys
for ($i = 1; $i < $total; $i += 2)
{
if (preg_match("#^\\[/(\w+)]#i", $parts[$i], $match))
{
$closes[strtolower($match[1])][$i] = $i;
}
}
// second pass: look for all the text elements and any opens, then find
// the first corresponding close that comes after it and remove it.
// if we find that, don't display the open or that close
for ($i = 0; $i < $total; $i++)
{
$part = $parts[$i];
if ($i % 2 == 0)
{
// string part
$newString .= $part;
continue;
}
if (!empty($skips[$i]))
{
// known close
continue;
}
if (preg_match('/^\[(\w+)(?:=|\s?\w+="[^"]*"|\])/i', $part, $match))
{
$tagName = strtolower($match[1]);
if (!empty($closes[$tagName]))
{
do
{
$closeKey = reset($closes[$tagName]);
if ($closeKey)
{
unset($closes[$tagName][$closeKey]);
}
}
while ($closeKey && $closeKey < $i);
if ($closeKey)
{
// found a matching close after this tag
$skips[$closeKey] = true;
continue;
}
}
}
$newString .= $part;
}
return trim($newString);
}
public function stripStructuredText($string)
{
return preg_replace(
MentionFormatter::STRUCTURED_MENTION_REGEX,
'\\3',
$string
);
}
public function getBbCodeForQuote($bbCode, $context)
{
$bbCodeContainer = \XF::app()->bbCode();
$processor = $bbCodeContainer->processor()
->addProcessorAction('quotes', $bbCodeContainer->processorAction('quotes'))
->addProcessorAction('censor', $bbCodeContainer->processorAction('censor'));
return trim($processor->render($bbCode, $bbCodeContainer->parser(), $bbCodeContainer->rules($context)));
}
public function getBbCodeFromSelectionHtml($html)
{
// attempt to parse the selected HTML into BB code
$tags = '<' . implode('><', $this->getSelectionAllowedTags()) . '>';
$html = trim(strip_tags($html, $tags));
// handle CODE output and turn it back into BB code
$html = preg_replace_callback('/<code data-language="(\w+)">(.*)<\/code>/siU', function(array $matches)
{
return "[CODE=$matches[1]]" . str_replace("\n", '<br>', trim($matches[2])) . "[/CODE]";
}, $html);
// handle ICODE output to BB code
$html = preg_replace_callback('/<code class="bbCodeInline">(.*)<\/code>/siU', function(array $matches)
{
return "[ICODE]" . trim($matches[1]) . "[/ICODE]";
}, $html);
return $html;
}
protected function getSelectionAllowedTags()
{
return [
'a',
'b',
'br',
'code',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'img',
'li',
'ol',
'pre',
'span',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
'u',
'ul',
];
}
public function snippetString($string, $maxLength = 0, array $options = [])
{
$options = array_merge([
'term' => '',
'fromStart' => false,
'stripBbCode' => false,
'stripQuote' => false,
'hideUnviewable' => true,
'stripHtml' => false,
'stripPlainTag' => false,
'censor' => true
], $options);
if ($options['stripQuote'])
{
$options['stripBbCode'] = true;
$options['stripPlainTag'] = true;
}
if ($options['stripHtml'])
{
$string = strip_tags($string);
}
if ($options['stripBbCode'])
{
$string = $this->stripBbCode($string, ['stripQuote' => $options['stripQuote'], 'hideUnviewable' => $options['hideUnviewable']]);
}
if ($options['stripPlainTag'])
{
$string = $this->stripStructuredText($string);
}
if ($maxLength)
{
if ($options['fromStart'] || !$options['term'])
{
$string = $this->wholeWordTrim($string, $maxLength);
}
else
{
$string = $this->wholeWordTrimAroundTerm($string, $maxLength, $options['term']);
}
}
$string = trim($string);
if ($options['censor'])
{
$string = $this->censorText($string);
}
return $string;
}
public function createKeyValueSetFromString($string)
{
$values = [];
preg_match_all('/
^\s*
(?P<name>([^=\r\n])*?)
\s*=\s*
(?P<value>.*?)
\s*$
/mix', trim($string), $matches, PREG_SET_ORDER);
foreach ($matches AS $match)
{
$value = $this->replacePhraseSyntax($match['value']);
$values[$match['name']] = $value;
}
return $values;
}
public function camelCase($string, $glue = '_')
{
return \XF\Util\Php::camelCase($string, $glue);
}
public function fromCamelCase($string, $glue = '_')
{
return \XF\Util\Php::fromCamelCase($string, $glue);
}
/**
* @return MentionFormatter
*
* @throws \Exception
*/
public function getMentionFormatter()
{
$class = \XF::extendClass('XF\Str\MentionFormatter');
return new $class();
}
/**
* @param null $forceStyle
* @param bool $forceCdn
*
* @return \XF\Str\EmojiFormatter
* @throws \Exception
*/
public function getEmojiFormatter($forceStyle = null, $forceCdn = false)
{
$canCache = ($forceStyle === null && $forceCdn === false);
if (!$this->emojiFormatter || !$canCache)
{
$options = \XF::options();
$pather = \XF::app()->container('request.pather');
$config = [
'style' => $forceStyle ?: $options->emojiStyle,
'source' => $forceCdn ? 'cdn' : $options->emojiSource['source'],
'path' => $pather(trim($options->emojiSource['path'], '/') . '/', 'base'),
'uc_filename' => null
];
$class = \XF::extendClass('XF\Str\EmojiFormatter');
$formatter = new $class($config);
if (!$canCache)
{
return $formatter;
}
$this->emojiFormatter = $formatter;
}
return $this->emojiFormatter;
}
}