Xenforo mới thông báo rằng bản Xenforo 2.0.8 bị lỗi XSS
Để fix lỗi XSS bạn làm như sau
Mở src/XF/Str/Formatter.php và thay tất cả bằng
Mở src/XF/Template/Templater.php và thay tất cả bằng
Mọi thắc mắc vui lòng phản hồi bên dưới
Chúc các bạn thành công!
Để fix lỗi XSS bạn làm như sau
Mở src/XF/Str/Formatter.php và thay tất cả bằng
PHP:
<?php
namespace XF\Str;
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;
public function censorText($string, $censorChar = null)
{
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:([a-z0-9_]+)\}#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('#("|\')([a-z0-9_]+)\\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 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="' . $url2x . ' 2x"';
}
return '<img src="' . $url . '" ' . $srcSet . ' class="smilie" alt="' . $smilieText
. '" title="' . $smilieTitle . ' ' . $smilieText . '" />';
}
else
{
// embed a data URI to avoid a request that doesn't respect paths fully
$url = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
return '<img src="' . $url . '" class="smilie smilie--sprite smilie--sprite' . $id
. '" alt="' . $smilieText . '" title="' . $smilieTitle . ' ' . $smilieText . '" />';
}
}
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 autoLinkStructuredText($string)
{
$string = $this->moveHtmlToPlaceholders($string, $restorePlaceholders);
$string = preg_replace_callback(
'#(?<=[^a-z0-9@-]|^)(https?://|www\.)[^\s"<>{}`]+#i',
function (array $match)
{
$url = $this->removeHtmlPlaceholders($match[0]);
$url = htmlspecialchars_decode($url, ENT_QUOTES);
$link = $this->prepareAutoLinkedUrl($url);
if (!$link['url'])
{
return $url;
}
$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 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)
{
$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;
}
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\?[a-z0-9_]+=(http[^&]+)&/i', $url, $match))
{
// proxy link of some sort, adjust to the original one
$proxiedUrl = urldecode($match[1]);
if (preg_match('/./u', $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(
'#(?<=^|\s|[\](,]|--|@)@\[(\d+):(\'|"|"|)(.*)\\2\]#iU',
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 getProxiedUrlIfActive($type, $url)
{
if (!$this->proxyHandler)
{
return null;
}
$handler = $this->proxyHandler;
return $handler($type, $url);
}
public function setProxyHandler(callable $handler = null)
{
$this->proxyHandler = $handler;
}
public function getProxyHandler()
{
return $this->proxyHandler;
}
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;
}
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 -= $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);
}
return $string;
}
public function highlightTermForHtml($string, $term, $class = 'textHighlight')
{
$term = trim(preg_replace('#((^|\s)[+|-]|[/()"~^])#', ' ', strval($term)));
if ($term !== '')
{
return preg_replace(
'/(' . preg_replace('#\s+#', '|', preg_quote(htmlspecialchars($term), '/')) . ')/siu',
'<em class="' . htmlspecialchars($class) . '">\1</em>',
\XF::escapeString($string)
);
}
else
{
return \XF::escapeString($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)[^\]]*\].*\[/\\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('#(\[[a-z0-9_]+(?:=[^\]]*)?\]|\[/[a-z0-9_]+\])#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("#^\\[/([a-z0-9]+)]#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('/^\[([a-z0-9]+)(?:=|\])/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 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
$html = trim(strip_tags($html, '<b><i><u><a><img><span><ul><ol><li><pre><code><br>'));
// 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;
}
public function snippetString($string, $maxLength = 0, array $options = [])
{
$options = array_merge([
'term' => '',
'fromStart' => false,
'stripBbCode' => false,
'stripQuote' => false,
'stripHtml' => false,
'stripPlainTag' => false,
'censor' => true
], $options);
if ($options['stripQuote'])
{
$options['stripBbCode'] = true;
}
if ($options['stripHtml'])
{
$string = strip_tags($string);
}
else if ($options['stripPlainTag'])
{
$string = preg_replace(
'#(?<=^|\s|[\](,]|--|@)@\[(\d+):(\'|"|"|)(.*)\\2\]#iU',
'\\3',
$string
);
}
else if ($options['stripBbCode'])
{
$string = $this->stripBbCode($string, ['stripQuote' => $options['stripQuote']]);
}
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;
}
/**
* @return \XF\Str\MentionFormatter
*/
public function getMentionFormatter()
{
$class = \XF::extendClass('XF\Str\MentionFormatter');
return new $class();
}
}
Mở src/XF/Template/Templater.php và thay tất cả bằng
PHP:
<?php
namespace XF\Template;
use XF\App;
use XF\Language;
use XF\Mvc\Entity\AbstractCollection;
use XF\Mvc\Router;
use XF\Util\Arr;
class Templater
{
const MAX_EXECUTION_DEPTH = 50;
/**
* @var App
*/
protected $app;
/**
* @var Router
*/
protected $router;
protected $routerType;
/**
* @var \Closure
*/
protected $pather;
protected $jsBaseUrl = 'js';
/**
* @var Language
*/
protected $language;
protected $compiledPath;
protected $styleId = 0;
/**
* @var callable|null
*/
protected $cssValidator;
/**
* @var \XF\Style|null
*/
protected $style;
protected $filters = [];
protected $functions = [];
protected $tests = [];
protected $defaultParams = [];
protected $templateCache = [];
protected $jQueryVersion;
protected $jQuerySource = 'local';
protected $jsVersion = '';
protected $dynamicDefaultAvatars = true;
protected $mediaSites = [];
protected $groupStyles = [];
protected $userTitleLadder = [];
protected $userTitleLadderField = 'trophy_points';
protected $userBanners = [];
protected $userBannerConfig = [];
protected $widgetPositions = [];
/**
* @var WatcherInterface[]
*/
protected $watchers = [];
protected $currentTemplateType;
protected $currentTemplateName;
protected $wrapTemplateName = null;
protected $wrapTemplateParams = null;
protected $executionDepth = 0;
protected $templateErrors = [];
protected $escapeContext = 'html';
protected $includeCss = [];
protected $inlineCss = [];
protected $includeJs = [];
protected $inlineJs = [];
protected $sidebar = [];
protected $sideNav = [];
protected $uniqueIdCounter = 0;
protected $uniqueIdPrefix;
protected $uniqueIdFormat = '_xfUid-%s';
protected $avatarDefaultStylingCache = [];
protected $avatarLetterRegex = '/[^\(\)\{\}\[\]\<\>\-\.\+\:\=\*\!\|\^\/\\\\\'`"_,#~ ]/u';
public $pageParams = [];
protected $defaultFilters = [
'default' => 'filterDefault',
'censor' => 'filterCensor',
'currency' => 'filterCurrency',
'escape' => 'filterEscape',
'for_attr' => 'filterForAttr',
'file_size' => 'filterFileSize',
'first' => 'filterFirst',
'hex' => 'filterHex',
'host' => 'filterHost',
'ip' => 'filterIp',
'join' => 'filterJoin',
'json' => 'filterJson',
'last' => 'filterLast',
'nl2br' => 'filterNl2Br',
'nl2nl' => 'filterNl2Nl',
'number' => 'filterNumber',
'number_short' => 'filterNumberShort',
'pad' => 'filterPad',
'parens' => 'filterParens',
'pluck' => 'filterPluck',
'preescaped' => 'filterPreEscaped',
'raw' => 'filterRaw',
'replace' => 'filterReplace',
'split' => 'filterSplit',
'strip_tags' => 'filterStripTags',
'to_lower' => 'filterToLower',
'to_upper' => 'filterToUpper',
'url' => 'filterUrl',
'urlencode' => 'filterUrlencode',
'zerofill' => 'filterZeroFill',
];
protected $defaultFunctions = [
'anchor_target' => 'fnAnchorTarget',
'array_keys' => 'fnArrayKeys',
'array_merge' => 'fnArrayMerge',
'array_values' => 'fnArrayValues',
'attributes' => 'fnAttributes',
'avatar' => 'fnAvatar',
'base_url' => 'fnBaseUrl',
'bb_code' => 'fnBbCode',
'bb_code_type' => 'fnBbCodeType',
'button_icon' => 'fnButtonIcon',
'callable' => 'fnCallable',
'captcha' => 'fnCaptcha',
'ceil' => 'fnCeil',
'contains' => 'fnContains',
'copyright' => 'fnCopyright',
'core_js' => 'fnCoreJs',
'count' => 'fnCount',
'csrf_input' => 'fnCsrfInput',
'csrf_token' => 'fnCsrfToken',
'css_url' => 'fnCssUrl',
'date' => 'fnDate',
'date_from_format' => 'fnDateFromFormat',
'date_dynamic' => 'fnDateDynamic',
'date_time' => 'fnDateTime',
'debug_url' => 'fnDebugUrl',
'display_totals' => 'fnDisplayTotals',
'dump' => 'fnDump',
'dump_simple' => 'fnDumpSimple',
'empty' => 'fnEmpty',
'file_size' => 'fnFileSize',
'floor' => 'fnFloor',
'gravatar_url' => 'fnGravatarUrl',
'highlight' => 'fnHighlight',
'in_array' => 'fnInArray',
'is_array' => 'fnIsArray',
'is_addon_active' => 'fnIsAddonActive',
'is_editor_capable' => 'fnIsEditorCapable',
'is_toggled' => 'fnIsToggled',
'js_url' => 'fnJsUrl',
'last_pages' => 'fnLastPages',
'likes' => 'fnLikes',
'likes_content' => 'fnLikesContent',
'link' => 'fnLink',
'link_type' => 'fnLinkType',
'max_length' => 'fnMaxLength',
'media_sites' => 'fnMediaSites',
'mustache' => 'fnMustache',
'number' => 'fnNumber',
'named_colors' => 'fnNamedColors',
'page_description' => 'fnPageDescription',
'page_h1' => 'fnPageH1',
'page_nav' => 'fnPageNav',
'page_title' => 'fnPageTitle',
'parens' => 'fnParens',
'parse_less_color' => 'fnParseLessColor',
'prefix' => 'fnPrefix',
'prefix_group' => 'fnPrefixGroup',
'prefix_title' => 'fnPrefixTitle',
'property' => 'fnProperty',
'rand' => 'fnRand',
'range' => 'fnRange',
'redirect_input' => 'fnRedirectInput',
'repeat' => 'fnRepeat',
'repeat_raw' => 'fnRepeatRaw',
'show_ignored' => 'fnShowIgnored',
'smilie' => 'fnSmilie',
'snippet' => 'fnSnippet',
'strlen' => 'fnStrlen',
'structured_text' => 'fnStructuredText',
'templater' => 'fnTemplater',
'time' => 'fnTime',
'transparent_img' => 'fnTransparentImg',
'trim' => 'fnTrim',
'unique_id' => 'fnUniqueId',
'user_activity' => 'fnUserActivity',
'user_banners' => 'fnUserBanners',
'user_blurb' => 'fnUserBlurb',
'user_title' => 'fnUserTitle',
'username_link' => 'fnUsernameLink',
'username_link_email' => 'fnUsernameLinkEmail',
'widget_data' => 'fnWidgetData'
];
protected $defaultTests = [
'empty' => 'testEmpty'
];
protected $overlayClickOptions = [
'data-cache',
'data-overlay-config',
'data-force-flash-messages',
'data-follow-redirects'
];
public function __construct(App $app, Language $language, $compiledPath)
{
$this->app = $app;
$this->language = $language;
$this->compiledPath = $compiledPath;
$this->router = $app->router();
$this->pather = $app->container('request.pather');
$this->uniqueIdFormat = '_xfUid-%s-' . \XF::$time;
}
public function getTemplateFilePath($type, $name, $styleIdOverride = null)
{
return $this->compiledPath
. '/l' . $this->language->getId()
. '/s' . intval($styleIdOverride !== null ? $styleIdOverride : $this->styleId)
. '/' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $type)
. '/' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $name) . '.php';
}
protected function getTemplateDataFromSource($type, $name)
{
$file = $this->getTemplateFilePath($type, $name);
if (!file_exists($file))
{
return false;
}
return include($file);
}
public function getRouter()
{
if ($this->currentTemplateType && $this->currentTemplateType != $this->routerType)
{
$container = $this->app->container();
$type = $this->currentTemplateType;
/** @var \XF\Mvc\Router|null $router */
$router = isset($container['router.' . $type]) ? $container['router.' . $type] : null;
if ($router)
{
$this->router = $router;
$this->routerType = $type;
}
}
return $this->router;
}
public function getCssLoadUrl(array $templates, $includeValidation = true)
{
$url = 'css.php?css='
. urlencode(implode(',', $templates))
. '&s=' . $this->styleId
. '&l=' . $this->language->getId()
. '&d=' . ($this->style ? $this->style->getLastModified() : \XF::$time);
if ($includeValidation)
{
$validationKey = $this->getCssValidationKey($templates);
if ($validationKey)
{
$url .= '&k=' . urlencode($validationKey);
}
}
$pather = $this->pather;
return $pather($url, 'base');
}
public function getCssValidationKey(array $templates)
{
if ($this->cssValidator)
{
$cssValidator = $this->cssValidator;
return $cssValidator($templates);
}
else
{
return null;
}
}
public function getJsUrl($js)
{
if (preg_match('#^(/|[a-z]+:)#i', $js))
{
return $js;
}
if (!strpos($js, '_v='))
{
$js = $js . (strpos($js, '?') ? '&' : '?') . $this->getJsCacheBuster();
}
$pather = $this->pather;
return $pather("{$this->jsBaseUrl}/$js", 'base');
}
public function getJsCacheBuster()
{
return '_v=' . $this->jsVersion;
}
public function getDevJsUrl($addOnId, $js)
{
$url = 'js/devjs.php?addon_id=' . urlencode($addOnId) . '&js=' . urlencode($js);
$pather = $this->pather;
return $pather($url, 'base');
}
public function setLanguage(Language $language)
{
$this->language = $language;
}
public function getLanguage()
{
return $this->language;
}
public function setStyle(\XF\Style $style)
{
$this->style = $style;
$this->styleId = $style->getId();
}
public function getStyle()
{
return $this->style;
}
public function getStyleId()
{
return $this->styleId;
}
public function setCssValidator(callable $cssValidator)
{
$this->cssValidator = $cssValidator;
}
public function setJquerySource($version, $jQuerySource = null)
{
$this->jQueryVersion = $version;
$this->jQuerySource = $jQuerySource ?: $this->app->options()->jQuerySource;
}
public function setJsVersion($version)
{
$this->jsVersion = $version;
}
public function setJsBaseUrl($baseUrl)
{
$this->jsBaseUrl = rtrim($baseUrl, '/') ?: 'js';
}
public function setDynamicDefaultAvatars($dynamic)
{
$this->dynamicDefaultAvatars = $dynamic;
}
public function setMediaSites(array $mediaSites)
{
$this->mediaSites = $mediaSites;
}
public function setUserTitleLadder(array $ladder, $titleField = '')
{
$this->userTitleLadder = $ladder;
if ($titleField)
{
$this->userTitleLadderField = $titleField;
}
}
public function setUserBanners(array $banners, array $config = [])
{
$this->userBanners = $banners;
if ($config)
{
$this->userBannerConfig = $config;
}
}
public function setGroupStyles(array $styles)
{
$this->groupStyles = $styles;
}
public function setWidgetPositions(array $widgetPositions)
{
$this->widgetPositions = $widgetPositions;
}
public function addDefaultHandlers()
{
$this->addFilters($this->defaultFilters);
$this->addFunctions($this->defaultFunctions);
$this->addTests($this->defaultTests);
}
public function addFilters(array $filters)
{
$this->filters = array_merge($this->filters, $filters);
}
public function addFilter($name, $filter)
{
$this->filters[$name] = $filter;
}
public function addFunctions(array $functions)
{
$this->functions = array_merge($this->functions, $functions);
}
public function addFunction($name, $function)
{
$this->functions[$name] = $function;
}
public function addTests(array $tests)
{
$this->tests = array_merge($this->tests, $tests);
}
public function addTest($name, $test)
{
$this->tests[$name] = $test;
}
public function addDefaultParams(array $params)
{
$this->defaultParams = array_merge($this->defaultParams, $params);
}
public function addDefaultParam($name, $value)
{
$this->defaultParams[$name] = $value;
}
public function getTemplate($name, array $params = [])
{
return new Template($this, $name, $params);
}
public function addTemplateWatcher(WatcherInterface $watcher)
{
$this->watchers[] = $watcher;
}
public function hasWatcherActionedTemplates()
{
foreach ($this->watchers AS $watcher)
{
if ($watcher->hasActionedTemplates())
{
return true;
}
}
return false;
}
public function getTemplateTypeAndName($template)
{
$parts = explode(':', $template, 2);
if (count($parts) == 2)
{
return [$parts[0], $parts[1]];
}
else
{
return [$this->currentTemplateType, $parts[0]];
}
}
public function applyDefaultTemplateType($template)
{
list($type, $template) = $this->getTemplateTypeAndName($template);
if ($type)
{
$template = "$type:$template";
}
return $template;
}
/**
* @param string $type
* @param string $template
*
* @return \Closure
*/
public function getTemplateCode($type, $template)
{
$data = $this->getTemplateData($type, $template);
return $data['code'];
}
/**
* @param string $type
* @param string $template
* @param string $macro
*
* @return \Closure
*/
public function getTemplateMacro($type, $template, $macro)
{
$data = $this->getTemplateData($type, $template);
if (isset($data['macros'][$macro]))
{
return $data['macros'][$macro];
}
trigger_error("Macro $type:$template:$macro is unknown", E_USER_WARNING);
return function() { return ''; };
}
protected function getTemplateData($type, $template, $errorOnUnknown = true)
{
if (isset($this->templateCache[$type][$template]))
{
return $this->templateCache[$type][$template];
}
if (preg_match('#[^a-zA-Z0-9_.-]#', $template))
{
throw new \InvalidArgumentException("Template name '$template' contains invalid characters");
}
foreach ($this->watchers AS $watcher)
{
$watcher->watchTemplate($this, $type, $template);
}
$data = $this->getTemplateDataFromSource($type, $template);
if (!$data || !is_array($data) || !isset($data['code']))
{
if ($errorOnUnknown)
{
trigger_error("Template $type:$template is unknown", E_USER_WARNING);
}
$data = [
'macros' => [],
'code' => function() { return ''; },
'unknown' => true
];
}
$this->templateCache[$type][$template] = $data;
return $data;
}
public function callAdsMacro($position, array $arguments, array $globalVars)
{
$templateData = $this->getTemplateData('public', '_ads', false);
if (!isset($templateData['macros'][$position]))
{
return '';
}
else
{
return $this->callMacro('public:_ads', $position, $arguments, $globalVars);
}
}
public function callMacro($template, $name, array $arguments, array $globalVars)
{
if ($this->executionDepth >= self::MAX_EXECUTION_DEPTH)
{
trigger_error('Max template execution depth reached', E_USER_WARNING);
return '';
}
if (!$template)
{
$template = $this->currentTemplateName;
$type = $this->currentTemplateType;
}
else
{
list($type, $template) = $this->getTemplateTypeAndName($template);
}
if (!$type)
{
trigger_error('No template type was provided. Provide template name in type:name format.', E_USER_WARNING);
return '';
}
if (isset($globalVars['__globals']))
{
$globalVars = $globalVars['__globals'];
}
$this->app->fire(
'templater_macro_pre_render',
[$this, &$type, &$template, &$name, &$arguments, &$globalVars],
"$type:$template:$name"
);
$currentType = $this->currentTemplateType;
$currentName = $this->currentTemplateName;
$origWrapTemplateName = $this->wrapTemplateName;
$origWrapTemplateParams = $this->wrapTemplateParams;
$this->currentTemplateType = $type;
$this->currentTemplateName = $template;
$this->wrapTemplateName = null;
$this->wrapTemplateParams = null;
$this->executionDepth++;
set_error_handler([$this, 'handleTemplateError']);
try
{
$macro = $this->getTemplateMacro($type, $template, $name);
$output = $macro($this, $arguments, $globalVars);
}
catch (\Exception $e)
{
$errorPrefix = "$this->currentTemplateType:$this->currentTemplateName :: $name()";
$this->app->logException($e, false, "Macro $errorPrefix error: ");
if (\XF::$debugMode)
{
$message = $e->getMessage();
$file = $e->getFile() . ':' . $e->getLine();
$error = $e instanceof \XF\PrintableException ?
"$errorPrefix - $message"
: "$errorPrefix - $message in $file";
if (preg_match('/\.(css|less)$/i', $template))
{
$error = strtr($error, [
"'" => '',
'\\' => '/',
"\r" => '',
"\n" => " "
]);
$output = "
/** Error output **/
body:before
{
background-color: #ccc;
color: black;
font-weight: bold;
display: block;
padding: 10px;
margin: 10px;
border: solid 1px #aaa;
border-radius: 5px;
content: 'CSS error: " . $error . "';
}
";
}
else
{
$output = '<div class="error"><h3>Template Compilation Error</h3>'
. '<div>' . htmlspecialchars($error) . '</div></div>';
}
}
else
{
$output = '';
}
}
restore_error_handler();
if ($this->wrapTemplateName)
{
$output = $this->applyWrappedTemplate($output);
}
$this->currentTemplateName = $currentName;
$this->currentTemplateType = $currentType;
$this->wrapTemplateName = $origWrapTemplateName;
$this->wrapTemplateParams = $origWrapTemplateParams;
$this->executionDepth--;
$this->app->fire(
'templater_macro_post_render',
[$this, $type, $template, &$name, &$output],
"$type:$template:$name"
);
return $output;
}
public function renderMacro($template, $name, array $arguments = [])
{
return $this->callMacro($template, $name, $arguments, $this->defaultParams);
}
public function setupBaseParamsForMacro(array $parentVars, $isGlobal = false)
{
if (isset($parentVars['__globals']))
{
$globalVars = $parentVars['__globals'];
}
else
{
$globalVars = $parentVars;
}
$params = $isGlobal ? $globalVars : $this->defaultParams;
$params['__globals'] = $globalVars;
return $params;
}
public function mergeMacroArguments(array $expected, array $provided, array $baseParams)
{
foreach ($expected AS $argument => $value)
{
if (array_key_exists($argument, $provided))
{
$baseParams[$argument] = $provided[$argument];
}
else if ($value === '!')
{
throw new \LogicException("Macro argument $argument is required and no value was provided");
}
else
{
$baseParams[$argument] = $value;
}
}
return $baseParams;
}
public function extractIntoVarContainer(array &$varContainer, $source)
{
if (!$this->isTraversable($source))
{
return;
}
foreach ($source AS $k => $v)
{
$varContainer[$k] = $v;
}
}
public function wrapTemplate($template, array $params)
{
$template = $this->applyDefaultTemplateType($template);
$this->wrapTemplateName = $template;
$this->wrapTemplateParams = $params;
}
protected function applyWrappedTemplate($content)
{
if (!$this->wrapTemplateName)
{
return $content;
}
$template = $this->wrapTemplateName;
$params = $this->wrapTemplateParams;
$this->wrapTemplateName = null;
$this->wrapTemplateParams = null;
$params['innerContent'] = $this->preEscaped($content, 'html');
return $this->renderTemplate($template, $params, false);
}
public function filter($value, array $filters, $escape = true)
{
foreach ($filters AS $filter)
{
list($name, $arguments) = $filter;
$name = strtolower($name);
if (!isset($this->filters[$name]))
{
trigger_error("Filter $name is unknown", E_USER_WARNING);
continue;
}
$callable = $this->filters[$name];
if (is_string($callable))
{
$callable = [$this, $callable];
}
if ($arguments)
{
array_unshift($arguments, null);
array_unshift($arguments, $value);
array_unshift($arguments, $this);
$arguments[2] =& $escape;
}
else
{
$arguments = [$this, $value, &$escape];
}
$value = call_user_func_array($callable, $arguments);
}
return $escape ? $this->escape($value, $escape) : $value;
}
public function fn($name, array $arguments = [], $escape = true)
{
$name = strtolower($name);
if (!isset($this->functions[$name]))
{
trigger_error("Function $name is unknown", E_USER_WARNING);
return '';
}
$callable = $this->functions[$name];
if (is_string($callable))
{
$callable = [$this, $callable];
}
if ($arguments)
{
array_unshift($arguments, null);
array_unshift($arguments, $this);
$arguments[1] =& $escape;
}
else
{
$arguments = [$this, &$escape];
}
$value = call_user_func_array($callable, $arguments);
return $escape ? $this->escape($value) : $value;
}
public function test($value, $test, array $arguments = [])
{
if (!isset($this->tests[$test]))
{
trigger_error("Test $test is unknown", E_USER_WARNING);
return false;
}
$callable = $this->tests[$test];
if (is_string($callable))
{
$callable = [$this, $callable];
}
if ($arguments)
{
array_unshift($arguments, $value);
array_unshift($arguments, $this);
}
else
{
$arguments = [$this, $value];
}
return (bool)call_user_func_array($callable, $arguments);
}
public function arrayKey($var, $key)
{
return $var[$key];
}
public function isA($object, $class)
{
return ($object instanceof $class);
}
public function method($var, $fn, array $arguments = [])
{
if (!is_object($var))
{
$type = gettype($var);
trigger_error("Cannot call method $fn on a non-object ($type)", E_USER_WARNING);
return '';
}
$call = [$var, $fn];
if (!is_callable($call))
{
$class = get_class($var);
trigger_error("Method $fn is not callable on the given object ($class)", E_USER_WARNING);
return '';
}
return call_user_func_array($call, $arguments);
}
public function escape($value, $type = null)
{
if ($type === null || $type === true)
{
$type = $this->escapeContext;
}
return \XF::escapeString($value, $type);
}
public function modifySectionedHtml(array &$ref, $key, $html, $mode = 'replace')
{
if ($mode == 'delete')
{
if ($key)
{
unset($ref[$key]);
}
return;
}
$html = trim($html);
if (!strlen($html))
{
return;
}
$html = $this->preEscaped($html, 'html');
switch ($mode)
{
case 'prepend':
if ($key)
{
$ref = [$key => $html] + $ref;
}
else
{
array_unshift($ref, $html);
}
break;
case 'append':
if ($key)
{
unset($ref[$key]); // unset to ensure this goes at the end
$ref[$key] = $html;
}
else
{
$ref[] = $html;
}
break;
case 'replace':
default:
if ($key)
{
$ref[$key] = $html;
}
else
{
$ref[] = $html;
}
break;
}
}
public function modifySidebarHtml($key, $html, $mode = 'replace')
{
$this->modifySectionedHtml($this->sidebar, $key, $html, $mode);
}
public function getSidebarHtml()
{
return $this->sidebar;
}
public function modifySideNavHtml($key, $html, $mode = 'replace')
{
$this->modifySectionedHtml($this->sideNav, $key, $html, $mode);
}
public function getSideNavHtml()
{
return $this->sideNav;
}
public function includeCss($css)
{
list($type, $template) = $this->getTemplateTypeAndName($css);
if (!$type)
{
trigger_error('No template type was provided. Provide template name in type:name format.', E_USER_WARNING);
return;
}
$this->includeCss["$type:$template"] = true;
}
public function getIncludedCss(array $forceAppend = [])
{
$css = array_keys($this->includeCss);
sort($css);
return array_merge($css, $forceAppend);
}
public function inlineCss($css)
{
$this->inlineCss[] = $css;
}
public function getInlineCss()
{
return $this->inlineCss;
}
public function includeJs(array $options)
{
$options = array_replace([
'src' => null,
'addon' => null,
'min' => null,
'dev' => null,
'prod' => null
], $options);
$developmentConfig = $this->app->config('development');
$productionMode = empty($developmentConfig['fullJs']);
$src = $this->splitJsSrc($options['src']);
if ($productionMode)
{
if ($options['min'])
{
$src = array_map(function($path)
{
return preg_replace('(\.js$)', '.min.js', $path, 1);
}, $src);
}
$prod = $this->splitJsSrc($options['prod']);
$src = array_merge($src, $prod);
foreach ($src AS $path)
{
$url = $this->getJsUrl($path);
$this->includeJs[$url] = true;
}
}
else
{
$dev = $this->splitJsSrc($options['dev']);
$src = array_merge($src, $dev);
if ($options['addon'])
{
foreach ($src AS $path)
{
$url = $this->getDevJsUrl($options['addon'], $path);
$this->includeJs[$url] = true;
}
}
else
{
foreach ($src AS $path)
{
$url = $this->getJsUrl($path);
$this->includeJs[$url] = true;
}
}
}
}
protected function splitJsSrc($js)
{
if ($js)
{
return preg_split('/[, ]/', $js, -1, PREG_SPLIT_NO_EMPTY);
}
else
{
return [];
}
}
public function getIncludedJs()
{
return array_keys($this->includeJs);
}
public function inlineJs($js)
{
$this->inlineJs[] = $js;
}
public function getInlineJs()
{
return $this->inlineJs;
}
public function isTraversable($value)
{
return is_array($value) || ($value instanceof \Traversable);
}
public function isArrayAccessible($value)
{
return is_array($value) || ($value instanceof \ArrayAccess);
}
public function handleTemplateError($errorType, $errorString, $file, $line)
{
if ($errorType == E_NOTICE || $errorType == E_USER_NOTICE)
{
return;
}
if ($errorType & error_reporting())
{
$this->templateErrors[] = [
'template' => $this->currentTemplateType . ':' . $this->currentTemplateName,
'type' => $errorType,
'error' => $errorString,
'file' => $file,
'line' => $line
];
$e = new \ErrorException($errorString, 0, $errorType, $file, $line);
$this->app->logException($e, false, "Template error: ");
}
}
public function getTemplateErrors()
{
return $this->templateErrors;
}
public function isKnownTemplate($template)
{
$type = false;
if (strpos($template, ':') !== false)
{
list($type, $template) = explode(':', $template, 2);
}
if (!$type)
{
return false;
}
$data = $this->getTemplateData($type, $template, false);
return empty($data['unknown']) ? true : false;
}
/**
* @param string $template
* @param array $params
* @param bool $addDefaultParams
*
* @return string
*/
public function renderTemplate($template, array $params = [], $addDefaultParams = true)
{
if ($this->executionDepth >= self::MAX_EXECUTION_DEPTH)
{
trigger_error('Max template execution depth reached', E_USER_WARNING);
return '';
}
if ($addDefaultParams)
{
$params = array_merge($this->defaultParams, $params);
}
$type = false;
if (strpos($template, ':') !== false)
{
list($type, $template) = explode(':', $template, 2);
}
if (!$type)
{
trigger_error('No template type was provided. Provide template name in type:name format.', E_USER_WARNING);
return '';
}
$this->app->fire('templater_template_pre_render', [$this, &$type, &$template, &$params], "$type:$template");
$currentType = $this->currentTemplateType;
$currentName = $this->currentTemplateName;
$origWrapTemplateName = $this->wrapTemplateName;
$origWrapTemplateParams = $this->wrapTemplateParams;
$this->currentTemplateType = $type;
$this->currentTemplateName = $template;
$this->wrapTemplateName = null;
$this->wrapTemplateParams = null;
$this->executionDepth++;
set_error_handler([$this, 'handleTemplateError']);
try
{
$code = $this->getTemplateCode($type, $template);
$output = $code($this, $params);
}
catch (\Exception $e)
{
$errorPrefix = "$this->currentTemplateType:$this->currentTemplateName";
$this->app->logException($e, false, "Template $errorPrefix error: ");
if (\XF::$debugMode)
{
$message = $e->getMessage();
$file = $e->getFile() . ':' . $e->getLine();
$error = $e instanceof \XF\PrintableException ?
"$errorPrefix - $message"
: "$errorPrefix - $message in $file";
if (preg_match('/\.(css|less)$/i', $template))
{
$error = strtr($error, [
"'" => '',
'\\' => '/',
"\r" => '',
"\n" => " "
]);
$output = "
/** Error output **/
body:before
{
background-color: #ccc;
color: black;
font-weight: bold;
display: block;
padding: 10px;
margin: 10px;
border: solid 1px #aaa;
border-radius: 5px;
content: 'CSS error: " . $error . "';
}
";
}
else
{
$output = '<div class="error"><h3>Template Compilation Error</h3>'
. '<div>' . htmlspecialchars($error) . '</div></div>';
}
}
else
{
$output = '';
}
}
restore_error_handler();
if ($this->wrapTemplateName)
{
$output = $this->applyWrappedTemplate($output);
}
$this->currentTemplateType = $currentType;
$this->currentTemplateName = $currentName;
$this->wrapTemplateName = $origWrapTemplateName;
$this->wrapTemplateParams = $origWrapTemplateParams;
$this->executionDepth--;
$this->app->fire('templater_template_post_render', [$this, $type, $template, &$output], "$type:$template");
return $output;
}
public function includeTemplate($template, array $params = [])
{
$template = $this->applyDefaultTemplateType($template);
return $this->renderTemplate($template, $params);
}
public function callback($class, $method, $contents, array $params = [])
{
if (!\XF\Util\Php::validateCallbackPhrased($class, $method, $errorPhrase))
{
return $errorPhrase;
}
if (!\XF\Util\Php::nameIndicatesReadOnly($method))
{
return \XF::phrase('callback_method_x_does_not_appear_to_indicate_read_only', ['method' => $method]);
}
ob_start();
$output = call_user_func([$class, $method], $contents, $params, $this);
$output .= ob_get_clean();
return $output;
}
public function setPageParams(array $pageParams)
{
$this->pageParams = Arr::mapMerge($this->pageParams, $pageParams);
}
public function setPageParam($name, $value)
{
if (strpos($name, '.') === false)
{
$this->pageParams[$name] = $value;
return;
}
$ref =& $this->pageParams;
$hasValid = false;
foreach (explode('.', $name) AS $part)
{
if (!strlen($part))
{
continue;
}
if (!isset($ref[$part]) || !is_array($ref[$part]))
{
$ref[$part] = [];
}
$ref =& $ref[$part];
$hasValid = true;
}
if ($hasValid)
{
$ref = $value;
}
}
public function breadcrumb($value, $href, array $config)
{
if (!isset($this->pageParams['breadcrumbs']) || !is_array($this->pageParams['breadcrumbs']))
{
$this->pageParams['breadcrumbs'] = [];
}
$crumb = [
'value' => $value,
'href' => $href,
'attributes' => $config
];
$this->pageParams['breadcrumbs'][] = $crumb;
}
public function breadcrumbs(array $crumbs)
{
if (!$crumbs)
{
$this->pageParams['breadcrumbs'] = [];
return;
}
foreach ($crumbs AS $key => $crumb)
{
if (is_string($crumb) || $crumb instanceof \XF\Phrase)
{
$crumb = [
'href' => $key,
'value' => $crumb
];
}
if (!is_array($crumb))
{
trigger_error("Each breadcrumb must be an array", E_USER_WARNING);
continue;
}
if (!isset($crumb['value']))
{
trigger_error("Each breadcrumb provide a 'value' key", E_USER_WARNING);
continue;
}
if (!isset($crumb['href']))
{
trigger_error("Each breadcrumb provide a 'href' key", E_USER_WARNING);
continue;
}
$value = $crumb['value'];
$href = $crumb['href'];
unset($crumb['value'], $crumb['href']);
$this->breadcrumb($value, $href, $crumb);
}
}
public function button($contentHtml, array $options, $menuHtml = '', array $menuOptions = [])
{
$href = $this->processAttributeToRaw($options, 'href', '', true);
if ($href)
{
$element = 'a';
$type = '';
$href = ' href="' . $href . '"';
}
else
{
$element = 'button';
$type = $this->processAttributeToRaw($options, 'type', '', true);
if ($type)
{
$type = ' type="' . $type . '"';
}
else
{
$type = ' type="button"';
}
}
$overlay = $this->processAttributeToRaw($options, 'overlay', '', true);
if ($overlay)
{
$overlay = " data-xf-click=\"overlay\"";
}
$buttonClasses = 'button';
$icon = $this->processAttributeToRaw($options, 'icon');
if ($icon)
{
$buttonClasses .= ' button--icon button--icon--' . preg_replace('#[^a-zA-Z0-9_-]#', '', $icon);
}
if ($menuHtml)
{
$buttonClasses .= ' button--splitTrigger';
$menuClass = $this->processAttributeToRaw($menuOptions, 'class', ' %s', true);
$unhandledMenuAttrs = $this->processUnhandledAttributes($menuOptions);
$menuHtml = "<div class=\"menu{$menuClass}\" data-menu=\"menu\" aria-hidden=\"true\"{$unhandledMenuAttrs}>{$menuHtml}</div>";
}
$classAttr = $this->processAttributeToHtmlAttribute($options, 'class', $buttonClasses, true);
$button = strval($this->processAttributeToRaw($options, 'button'));
if (!$button)
{
$button = $contentHtml;
}
if (!$button && $icon)
{
$button = $this->getButtonPhraseFromIcon($icon);
}
$unhandledControlAttrs = $this->processUnhandledAttributes($options);
if ($menuHtml)
{
return "<span{$classAttr}><{$element}{$type}{$href} class=\"button-text\">{$button}</{$element}>"
. "<a class=\"button-menu\" data-xf-click=\"menu\" aria-expanded=\"false\" aria-haspopup=\"true\"></a>"
. $menuHtml
. "</span>";
}
else
{
return "<{$element}{$type}{$href}{$classAttr}{$overlay}{$unhandledControlAttrs}><span class=\"button-text\">{$button}</span></{$element}>";
}
}
public function widgetPosition($positionId, array $contextParams = [])
{
$widgetPositions = $this->widgetPositions;
if (!isset($widgetPositions[$positionId]))
{
return '';
}
$widgetContainer = $this->app->widget();
$widgets = $widgetContainer->position($positionId, $contextParams);
$output = '';
foreach ($widgets AS $widget)
{
$output .= $widget->render() . "\n";
}
return $output;
}
public function renderWidget($identifier, array $options = [], array $contextParams = [])
{
$options['context'] = $contextParams;
$widget = $this->app->widget()->widget($identifier, $options);
return $widget->render();
}
public function preEscaped($value, $type = null)
{
if ($type === null)
{
$type = $this->escapeContext;
}
return new \XF\PreEscaped($value, $type);
}
////////////////////// FUNCTIONS ////////////////////////
public function fnAnchorTarget($templater, &$escape, $hash)
{
$escape = false;
return '<span class="u-anchorTarget" id="' . htmlspecialchars($this->app->getRedirectHash($hash)) . '"></span>';
}
public function fnArrayKeys($templater, &$escape, $array)
{
if (!is_array($array))
{
$array = [];
}
return array_keys($array);
}
public function fnArrayMerge($templater, &$escape, $array)
{
$arrays = func_get_args();
unset($arrays[0]);
unset($arrays[1]);
return call_user_func_array('array_merge', $arrays);
}
public function fnArrayValues($templater, &$escape, $array)
{
if (!is_array($array))
{
$array = [];
}
return array_values($array);
}
public function fnAttributes($templater, &$escape, $attributes, array $skipAttrs = [])
{
if (is_array($attributes))
{
foreach ($skipAttrs AS $attr)
{
unset($attributes[$attr]);
}
$output = $this->processUnhandledAttributes($attributes);
}
else
{
$output = '';
}
$escape = false;
return $output;
}
public function fnAvatar($templater, &$escape, $user, $size, $canonical = false, $attributes = [])
{
$escape = false;
$forceType = $this->processAttributeToRaw($attributes, 'forcetype', '', true);
$noTooltip = $this->processAttributeToRaw($attributes, 'notooltip', '', false);
$update = $this->processAttributeToRaw($attributes, 'update', '');
if ($user instanceof \XF\Entity\User)
{
$username = $user->username;
if (isset($attributes['href']))
{
$href = $attributes['href'];
$noTooltip = true;
}
else
{
$linkPath = $this->currentTemplateType == 'admin' ? 'users/edit' : 'members';
$href = $this->getRouter()->buildLink(($canonical ? 'canonical:' : '') . $linkPath, $user);
if ($this->currentTemplateType == 'admin')
{
$noTooltip = true;
}
}
$userId = $user->user_id;
if (!$userId)
{
$href = null;
$noTooltip = true;
}
$hrefAttr = $href ? ' href="' . htmlspecialchars($href) . '"' : '';
$avatarType = $forceType ?: $user->getAvatarType();
$canUpdate = ((bool)$update && $user->user_id == \XF::visitor()->user_id && $user->canUploadAvatar());
}
else
{
if (isset($attributes['defaultname']))
{
$username = $attributes['defaultname'];
}
else
{
$username = null;
}
$hrefAttr = '';
$noTooltip = true;
$userId = 0;
$avatarType = 'default';
$canUpdate = false;
}
switch ($avatarType)
{
case 'gravatar':
case 'custom':
$src = $user->getAvatarUrl($size, $forceType, $canonical);
break;
case 'default':
default:
$src = null;
break;
}
$actualSize = $size;
if (!array_key_exists($size, $this->app->container('avatarSizeMap')))
{
$actualSize = 's';
}
$sizeClass = "avatar-u{$userId}-{$actualSize}";
$innerClass = $this->processAttributeToRaw($attributes, 'innerclass', ' %s', true);
$innerClassHtml = $sizeClass . $innerClass;
if ($src && $forceType != 'default')
{
$srcSet = $user->getAvatarUrl2x($size, $forceType, $canonical);
$itemprop = $this->processAttributeToRaw($attributes, 'itemprop', '%s', true);
$innerContent = '<img src="' . $src . '" ' . (!empty($srcSet) ? 'srcset="' . $srcSet . ' 2x"' : '')
. ' alt="' . htmlspecialchars($username) . '"'
. ' class="' . $innerClassHtml . '"'
. ($itemprop ? ' itemprop="' . $itemprop . '"' : '')
. ' />';
}
else
{
$innerContent = $this->getDynamicAvatarHtml($username, $innerClassHtml, $attributes);
}
$updateLink = '';
$updateLinkClass = '';
if ($canUpdate)
{
$updateLinkClass = ' avatar--updateLink';
$updateLink = '<div class="avatar-update">
<a href="' . htmlspecialchars($update) . '" data-xf-click="overlay">' . \XF::phrase('edit_avatar') . '</a>
</div>';
}
$class = $this->processAttributeToRaw($attributes, 'class', ' %s', true);
$xfInit = $this->processAttributeToRaw($attributes, 'data-xf-init', '', true);
if (!$noTooltip)
{
$xfInit = ltrim("$xfInit member-tooltip");
}
$xfInitAttr = $xfInit ? " data-xf-init=\"$xfInit\"" : '';
unset($attributes['defaultname'], $attributes['href'], $attributes['itemprop']);
$unhandledAttrs = $this->processUnhandledAttributes($attributes);
if ($hrefAttr)
{
$tag = 'a';
}
else
{
$tag = 'span';
}
return "<{$tag}{$hrefAttr} class=\"avatar avatar--{$size}{$updateLinkClass}{$class}\" data-user-id=\"{$userId}\"{$xfInitAttr}{$unhandledAttrs}>
$innerContent $updateLink
</{$tag}>";
}
protected function getDynamicAvatarHtml($username, $innerClassHtml, array &$outerAttributes)
{
if ($username && $this->dynamicDefaultAvatars)
{
return $this->getDefaultAvatarHtml($username, $innerClassHtml, $outerAttributes);
}
else
{
return $this->getFallbackAvatarHtml($innerClassHtml, $outerAttributes);
}
}
protected function getDefaultAvatarHtml($username, $innerClassHtml, array &$outerAttributes)
{
$styling = $this->getDefaultAvatarStyling($username);
if (empty($outerAttributes['style']))
{
$outerAttributes['style'] = '';
}
else
{
$outerAttributes['style'] .= '; ';
}
$outerAttributes['style'] .= "background-color: $styling[bgColor]; color: $styling[color]";
if (empty($outerAttributes['class']))
{
$outerAttributes['class'] = '';
}
else
{
$outerAttributes['class'] .= ' ';
}
$outerAttributes['class'] .= 'avatar--default avatar--default--dynamic';
return '<span class="' . $innerClassHtml . '">' . $styling['innerContent'] . '</span>';
}
protected function getDefaultAvatarStyling($username)
{
if (!isset($this->avatarDefaultStylingCache[$username]))
{
$bytes = md5($username, true);
$r = dechex(round(5 * ord($bytes[0]) / 255) * 0x33);
$g = dechex(round(5 * ord($bytes[1]) / 255) * 0x33);
$b = dechex(round(5 * ord($bytes[2]) / 255) * 0x33);
$hexBgColor = sprintf('%02s%02s%02s', $r, $g, $b);
$hslBgColor = \XF\Util\Color::hexToHsl($hexBgColor);
$bgChanged = false;
if ($hslBgColor[1] > 60)
{
$hslBgColor[1] = 60;
$bgChanged = true;
}
else if ($hslBgColor[1] < 15)
{
$hslBgColor[1] = 15;
$bgChanged = true;
}
if ($hslBgColor[2] > 85)
{
$hslBgColor[2] = 85;
$bgChanged = true;
}
else if ($hslBgColor[2] < 15)
{
$hslBgColor[2] = 15;
$bgChanged = true;
}
if ($bgChanged)
{
$hexBgColor = \XF\Util\Color::hslToHex($hslBgColor);
}
$hslColor = \XF\Util\Color::darkenOrLightenHsl($hslBgColor, 35);
$hexColor = \XF\Util\Color::hslToHex($hslColor);
$bgColor = '#' . $hexBgColor;
$color = '#' . $hexColor;
if (preg_match($this->avatarLetterRegex, $username, $match))
{
$innerContent = htmlspecialchars(utf8_strtoupper($match[0]));
}
else
{
$innerContent = '?';
}
$this->avatarDefaultStylingCache[$username] = [
'bgColor' => $bgColor,
'color' => $color,
'innerContent' => $innerContent
];
}
return $this->avatarDefaultStylingCache[$username];
}
protected function getFallbackAvatarHtml($innerClassHtml, array &$outerAttributes)
{
if (empty($outerAttributes['class']))
{
$outerAttributes['class'] = '';
}
else
{
$outerAttributes['class'] .= ' ';
}
$fallbackType = $this->style->getProperty('avatarDefaultType', 'text');
$outerAttributes['class'] .= 'avatar--default avatar--default--' . $fallbackType;
return '<span class="' . $innerClassHtml . '"></span>';
}
public function fnBaseUrl($templater, &$escape, $url = null, $full = false)
{
$pather = $this->pather;
return $pather($url ?: '', $full ? 'full' : 'base');
}
public function fnBbCode($templater, &$escape, $bbCode, $context, $content, array $options = [], $type = 'html')
{
$escape = false;
return $this->app->bbCode()->render($bbCode, $type, $context, $content, $options);
}
public function fnBbCodeType($templater, &$escape, $type, $bbCode, $context, $content, array $options = [])
{
return $this->fnBbCode($templater, $escape, $bbCode, $context, $content, $options, $type);
}
public function fnButtonIcon($templater, &$escape, $icon)
{
$icon = preg_replace('#[^a-zA-Z0-9_-]#', '', strval($icon));
if (!$icon)
{
return '';
}
$escape = false;
return " button--icon button--icon--" . $icon;
}
public function fnCallable($templater, &$escape, $var, $fn)
{
$escape = false;
if (!\XF\Util\Php::validateCallback($var, $fn))
{
return false;
}
if (!\XF\Util\Php::nameIndicatesReadOnly($fn))
{
return false;
}
return true;
}
public function fnCaptcha($templater, &$escape, $force = false)
{
if (!$force && !\XF::visitor()->isShownCaptcha())
{
return '';
}
$captcha = $this->app->captcha();
if ($captcha)
{
$escape = false;
return $captcha->render($templater);
}
return '';
}
public function fnCopyright($templater, &$escape)
{
$escape = false;
return \XF::getCopyrightHtml();
}
public function fnCoreJs($templater, &$escape)
{
$jqVersion = $this->jQueryVersion;
$jqMin = '.min';
$jqLocal = $this->getJsUrl("vendor/jquery/jquery-{$jqVersion}{$jqMin}.js");
$jqRemote = '';
if ($this->app['app.defaultType'] == 'public')
{
switch ($this->jQuerySource)
{
case 'jquery':
$jqRemote = "https://code.jquery.com/jquery-{$jqVersion}{$jqMin}.js";
break;
case 'google':
$jqRemote = "https://ajax.googleapis.com/ajax/libs/jquery/{$jqVersion}/jquery{$jqMin}.js";
break;
case 'microsoft':
$jqRemote = "https://ajax.microsoft.com/ajax/jquery/jquery-{$jqVersion}{$jqMin}.js";
break;
}
}
if ($jqRemote)
{
$output = '<script src="' . htmlspecialchars($jqRemote) . '"></script>'
. '<script>window.jQuery || document.write(\'<script src="'
. \XF::escapeString($jqLocal, 'htmljs') . '"><\\/script>\')</script>';
}
else
{
$output = '<script src="' . htmlspecialchars($jqLocal) . '"></script>';
}
$files = [
'vendor/vendor-compiled.js'
];
if ($this->app['config']['development']['fullJs'])
{
$files[] = 'xf/core.js';
foreach (glob(\XF::getRootDirectory() . '/js/xf/core/*.js') AS $file)
{
if (substr($file, -7) == '.min.js')
{
continue;
}
$files[] = 'xf/core/' . basename($file);
}
}
else
{
$files[] = 'xf/core-compiled.js';
}
foreach ($files AS $file)
{
$output .= "\n\t<script src=\"" . htmlspecialchars($this->getJsUrl($file)) . '"></script>';
}
$escape = false;
return $output;
}
public function fnCount($templater, &$escape, $value)
{
if (is_array($value) || $value instanceof \Countable)
{
return count($value);
}
return null;
}
public function fnCsrfInput($templater, &$escape)
{
$escape = false;
return '<input type="hidden" name="_xfToken" value="' . htmlspecialchars($this->app['csrf.token']) . '" />';
}
public function fnCsrfToken($templater, &$escape)
{
return $this->app['csrf.token'];
}
public function fnCssUrl($templater, &$escape, array $templates, $includeValidation = true)
{
return $this->getCssLoadUrl($templates, $includeValidation);
}
public function fnDate($templater, &$escape, $date, $format = null)
{
return $this->language->date($date, $format);
}
public function fnDateFromFormat($templater, &$escape, $format, $dateString, $timeZone = null)
{
return \DateTime::createFromFormat($format, $dateString, $timeZone === null
? $this->language->getTimezone()
: new \DateTimeZone($timeZone));
}
public function fnDateDynamic($templater, &$escape, $dateTime, array $attributes = [])
{
if (!($dateTime instanceof \DateTime))
{
$ts = intval($dateTime);
$dateTime = new \DateTime();
$dateTime->setTimestamp($ts);
$dateTime->setTimezone($this->language->getTimeZone());
}
else
{
$ts = $dateTime->getTimestamp();
}
list($date, $time) = $this->language->getDateTimeParts($ts);
$full = $this->language->getDateTimeOutput($date, $time);
$relative = $this->language->getRelativeDateTimeOutput($ts, $date, $time, !empty($attributes['data-full-date']));
$class = $this->processAttributeToHtmlAttribute($attributes, 'class', 'u-dt', true);
unset($attributes['title']);
$unhandledAttrs = $this->processUnhandledAttributes($attributes);
$escape = false;
return '<time ' . $class . ' dir="auto" datetime="' . $dateTime->format(\DateTime::ISO8601)
. '" data-time="' . $ts
. '" data-date-string="' . htmlspecialchars($date)
. '" data-time-string="' . htmlspecialchars($time)
. '" title="' . htmlspecialchars($full)
. '"' . $unhandledAttrs . '>' . htmlspecialchars($relative) . '</time>';
}
public function fnDateTime($templater, &$escape, $date)
{
return $this->language->dateTime($date);
}
public function fnDebugUrl($templater, &$escape, $url = null)
{
if (!$url)
{
$url = $this->app->request()->getRequestUri();
}
if (strpos($url, '?') === false)
{
$url .= '?';
}
else
{
$url .= '&';
}
return $url . '_debug=1';
}
public function fnDump($templater, &$escape, $value)
{
$escape = false;
ob_start();
\XF::dump($value);
$dump = ob_get_clean();
return $dump;
}
public function fnDumpSimple($templater, &$escape, $value)
{
$escape = false;
return \XF::dumpSimple($value, false);
}
public function fnEmpty($templater, &$escape, $value)
{
return empty($value);
}
public function fnDisplayTotals($templater, &$escape, $count, $total = null)
{
if (is_array($count) || $count instanceof \Countable)
{
$count = count($count);
}
if ($total === null)
{
$total = $count;
}
else if (is_array($total) || $total instanceof \Countable)
{
$total = count($total);
}
$params = [
'count' => $this->language->numberFormat($count),
'total' => $this->language->numberFormat($total)
];
if ($count < 1)
{
$phrase = 'no_items_to_display';
}
else if ($count == $total)
{
$phrase = 'showing_all_items';
}
else
{
$phrase = 'showing_x_of_y_items';
}
$escape = false;
return '<span class="js-displayTotals" data-count="' . $count . '" data-total="' . $total . '"'
. ' data-xf-init="tooltip" title="' . \XF::phrase('there_are_x_items_in_total', ['total' => $params['total']]) . '">'
. \XF::phrase($phrase, $params) . '</span>';
}
public function fnFileSize($templater, &$escape, $number)
{
return $this->language->fileSizeFormat($number);
}
public function fnCeil($templater, &$escape, $value)
{
return ceil($value);
}
public function fnFloor($templater, &$escape, $value)
{
return floor($value);
}
public function fnGravatarUrl($templater, &$escape, $user, $size)
{
if ($user instanceof \XF\Entity\User)
{
return $user->getGravatarUrl($size);
}
else
{
return '';
}
}
public function fnHighlight($templater, &$escape, $string, $term, $class = 'textHighlight')
{
$escape = false;
return $this->app->stringFormatter()->highlightTermForHtml($string, $term, $class);
}
public function fnInArray($templater, &$escape, $needle, $haystack, $strict = false)
{
$escape = false;
if ($haystack instanceof \Traversable)
{
$haystack = iterator_to_array($haystack);
}
if (!is_array($haystack))
{
return false;
}
return in_array($needle, $haystack, $strict);
}
public function fnIsArray($templater, &$escape, $array)
{
$escape = false;
return is_array($array);
}
public function fnIsAddonActive($templater, &$escape, $addOnId, $versionId = null, $operator = '>=')
{
$addOns = $this->app->registry()['addOns'];
if (!isset($addOns[$addOnId]))
{
return false;
}
if ($versionId === null)
{
return $addOns[$addOnId];
}
else
{
$activeVersionId = $addOns[$addOnId];
switch ($operator)
{
case '>':
return ($activeVersionId > $versionId);
case '>=':
return ($activeVersionId >= $versionId);
case '<':
return ($activeVersionId < $versionId);
case '<=':
return ($activeVersionId <= $versionId);
default:
return $addOns[$addOnId];
}
}
}
public function fnIsEditorCapable($templater, &$escape)
{
$ua = $this->app->request()->getUserAgent();
if (!$ua)
{
return true;
}
if (preg_match('#blackberry|opera mini|opera mobi#i', $ua))
{
// older/limited mobile browsers
return false;
}
if (preg_match('#msie (\d+)#i', $ua, $match) && intval($match[1]) < 10)
{
// only supported in IE10+
return false;
}
if (preg_match('#android (\d+)\.#i', $ua, $match) && intval($match[1]) < 5)
{
// Froala only officially supports Android 6 and above.
// However, it seems Froala actually still works on Android 5.1.1 (at least) so we'll go with that.
// So far we've only had issues reported with Android 4.x.
// Older Android versions do support Chrome and Firefox so if those are installed
// They will likely be up to date and work fine with the RTE.
if (preg_match('#(Firefox/|Chrome/)#i', $ua))
{
return true;
}
else
{
return false;
}
}
if (preg_match('#(iphone|ipod|ipad).+OS (\d+)_#i', $ua, $match) && intval($match[2]) < 8)
{
// only supported in iOS 8+
return false;
}
return true;
}
public function fnIsToggled($templater, &$escape, $storageKey, $storageContainer = 'toggle')
{
$cookie = $this->app->request()->getCookie($storageContainer);
if (!$cookie)
{
return false;
}
$cookieDecoded = @json_decode($cookie, true);
if (!$cookieDecoded)
{
return false;
}
if (!isset($cookieDecoded[$storageKey]))
{
return false;
}
return empty($cookieDecoded[$storageKey][2]);
}
public function fnJsUrl($templater, &$escape, $file)
{
return $this->getJsUrl($file);
}
public function fnLastPages($templater, &$escape, $total, $perPage, $max = 2)
{
$escape = false;
$perPage = intval($perPage);
if ($perPage <= 0)
{
return [];
}
$total = intval($total);
if ($total <= $perPage)
{
return [];
}
$max = max(1, intval($max));
$totalPages = ceil($total / $perPage);
if ($totalPages == 2)
{
return [2];
}
// + 1 represents that range covers including the start, whereas we want only the last X, which is start + 1
$start = max($totalPages - $max + 1, 2);
return range($start, $totalPages);
}
public function fnLikes($templater, &$escape, $count, $users, $liked, $url, array $attributes = [])
{
$escape = false;
$count = intval($count);
if ($count <= 0)
{
return '';
}
if (!$users || !is_array($users))
{
$phrase = ($count > 1 ? 'likes.x_people' : 'likes.1_person');
return $this->renderTemplate('public:like_list_row', [
'url' => $url,
'likes' => \XF::phrase($phrase, ['likes' => $this->language->numberFormat($count)])
]);
}
$userCount = count($users);
if ($userCount < 5 && $count > $userCount) // indicates some users are deleted
{
for ($i = 0; $i < $count; $i++)
{
if (empty($users[$i]))
{
$users[$i] = [
'user_id' => 0,
'username' => \XF::phrase('likes.deleted_user')
];
}
}
}
if ($liked)
{
$visitorId = \XF::visitor()->user_id;
foreach ($users AS $key => $user)
{
if ($user['user_id'] == $visitorId)
{
unset($users[$key]);
break;
}
}
$users = array_values($users);
if (count($users) == 3)
{
unset($users[2]);
}
}
$user1 = $user2 = $user3 = '';
if (isset($users[0]))
{
$user1 = $this->preEscaped('<bdi>' . \XF::escapeString($users[0]['username']) . '</bdi>', 'html');
if (isset($users[1]))
{
$user2 = $this->preEscaped('<bdi>' . \XF::escapeString($users[1]['username']) . '</bdi>', 'html');
if (isset($users[2]))
{
$user3 = $this->preEscaped('<bdi>' . \XF::escapeString($users[2]['username']) . '</bdi>', 'html');
}
}
}
switch ($count)
{
case 1: $phrase = ($liked ? 'likes.you' : 'likes.user1'); break;
case 2: $phrase = ($liked ? 'likes.you_and_user1' : 'likes.user1_and_user2'); break;
case 3: $phrase = ($liked ? 'likes.you_user1_and_user2' : 'likes.user1_user2_and_user3'); break;
case 4: $phrase = ($liked ? 'likes.you_user1_user2_and_1_other' : 'likes.user1_user2_user3_and_1_other'); break;
default: $phrase = ($liked ? 'likes.you_user1_user2_and_x_others' : 'likes.user1_user2_user3_and_x_others'); break;
}
$params = [
'user1' => $user1,
'user2' => $user2,
'user3' => $user3,
'others' => $this->language->numberFormat($count - 3)
];
return $this->renderTemplate('public:like_list_row', [
'url' => $url,
'likes' => \XF::phrase($phrase, $params)
]);
}
public function fnLikesContent($templater, &$escape, $content, $url, array $attributes = [])
{
$escape = false;
if (!($content instanceof \XF\Mvc\Entity\Entity))
{
trigger_error("Content must be an entity link likes_content (given " . gettype($content) . ")", E_USER_WARNING);
return '';
}
$count = $content->likes;
$users = $content->like_users;
$userId = \XF::visitor()->user_id;
$liked = $userId ? isset($content->Likes[$userId]) : false;
return $this->fn('likes', [$count, $users, $liked, $url, $attributes], false);
}
public function fnLink($templater, &$escape, $link, $data = null, array $params = [])
{
return $this->getRouter()->buildLink($link, $data, $params);
}
public function fnLinkType($templater, &$escape, $type, $link, $data = null, array $params = [])
{
$container = $this->app->container();
/** @var \XF\Mvc\Router|null $router */
$router = isset($container['router.' . $type]) ? $container['router.' . $type] : null;
if ($router)
{
return $router->buildLink($link, $data, $params);
}
else
{
return '';
}
}
public function fnMaxLength($templater, &$escape, $entity, $column)
{
static $entityCache = [];
// if $entity is not an entity, expect an entity id string like XF:Thread
if (is_string($entity) && preg_match('/^\w+(?:\\\w+)?:\w+$/i', $entity))
{
if (!isset($entityCache[$entity]))
{
$entityCache[$entity] = $this->app->em()->create($entity);
}
$entity = $entityCache[$entity];
}
if ($entity instanceof \XF\Mvc\Entity\Entity)
{
$maxlength = $entity->getMaxLength($column);
return $maxlength > 0 ? $maxlength : null;
}
else
{
return null;
}
}
public function fnMediaSites($templater, &$escape)
{
$output = [];
foreach ($this->mediaSites AS $site)
{
if (!$site['supported'])
{
continue;
}
if ($site['site_url'])
{
$output[] = '<a href="' . htmlspecialchars($site['site_url']) . '" target="_blank" rel="nofollow" dir="auto">' . htmlspecialchars($site['site_title']) . '</a>';
}
else
{
$output[] = htmlspecialchars($site['site_title']);
}
}
$escape = false;
return implode(', ', $output);
}
public function fnMustache($templater, &$escape, $name, $inner = null)
{
$escape = false;
$var = '{{' . $name . '}}';
if ($inner === null)
{
return $var;
}
else
{
$close = '{{/' . substr($name, 1) . '}}';
return "{$var}{$inner}{$close}";
}
}
public function fnNumber($templater, &$escape, $number, $precision = 0)
{
return $this->language->numberFormat($number, $precision);
}
public function fnNamedColors($templater, &$escape)
{
return \XF\Util\Color::getNamedColors();
}
public function fnPageDescription($templater, &$escape)
{
if (isset($this->pageParams['pageDescription']))
{
return $this->pageParams['pageDescription'];
}
else
{
return '';
}
}
public function fnPageH1($templater, &$escape, $fallback = '')
{
if (isset($this->pageParams['pageH1']))
{
return $this->pageParams['pageH1'];
}
else if (isset($this->pageParams['pageTitle']))
{
return $this->pageParams['pageTitle'];
}
else
{
return $fallback;
}
}
public function fnPageNav($templater, &$escape, array $config)
{
$escape = false;
$config = array_merge([
'pageParam' => 'page',
'page' => 0,
'perPage' => 0,
'total' => 0,
'range' => 2,
'template' => $this->applyDefaultTemplateType('page_nav'),
'variantClass' => '',
'link' => '',
'data' => null,
'params' => [],
'wrapper' => '',
'wrapperclass' => '',
], $config);
if (!is_array($config['params']))
{
$config['params'] = [];
}
$perPage = intval($config['perPage']);
if ($perPage <= 0)
{
return '';
}
$total = intval($config['total']);
if ($total <= $perPage)
{
return '';
}
$totalPages = ceil($total / $perPage);
$current = intval($config['page']);
$current = max(1, min($current, $totalPages));
// number of pages either side of the current page
$range = intval($config['range']);
$startInner = max(2, $current - $range);
$endInner = min($current + $range, $totalPages - 1);
if ($startInner <= $endInner)
{
$innerPages = range($startInner, $endInner);
}
else
{
$innerPages = [];
}
$wrapperClass = $this->processAttributeToRaw($config, 'wrapperclass', '', true);
$wrapper = $this->processAttributeToRaw($config, 'wrapper');
if ($wrapperClass && !$wrapper)
{
$wrapper = 'div';
}
$router = $this->router;
$prev = false;
if ($current > 1)
{
$prevPageParam = $current - 1;
if ($prevPageParam <= 1)
{
$prevPageParam = null;
}
$prev = $router->buildLink($config['link'], $config['data'], $config['params'] + [$config['pageParam'] => $prevPageParam]);
if (!isset($this->pageParams['head']['prev']))
{
$this->pageParams['head']['prev'] = $this->preEscaped('<link rel="prev" href="' . \XF::escapeString($prev) . '" />');
}
}
$next = false;
if ($current < $totalPages)
{
$next = $router->buildLink($config['link'], $config['data'], $config['params'] + [$config['pageParam'] => $current + 1]);
if (!isset($this->pageParams['head']['next']))
{
$this->pageParams['head']['next'] = $this->preEscaped('<link rel="next" href="' . \XF::escapeString($next) . '" />');
}
}
$html = $this->renderTemplate($config['template'], [
'prev' => $prev,
'current' => $current,
'next' => $next,
'perPage' => $perPage,
'total' => $total,
'totalPages' => $totalPages,
'innerPages' => $innerPages,
'startInner' => $startInner,
'endInner' => $endInner,
'pageParam' => $config['pageParam'],
'link' => $config['link'],
'data' => $config['data'],
'params' => $config['params'],
'variantClass' => $config['variantClass']
]);
if ($wrapper)
{
$wrapperOpen = $wrapper . ($wrapperClass ? " class=\"$wrapperClass\"" : '');
$html = "<{$wrapperOpen}>{$html}</{$wrapper}>";
}
return $html;
}
public function fnPageTitle($templater, &$escape, $formatter = null, $fallback = '', $page = null)
{
if (isset($this->pageParams['pageTitle']) && strlen($this->pageParams['pageTitle']))
{
$pageTitle = $this->pageParams['pageTitle'];
$page = intval($page);
if ($page > 1)
{
$pageAppend = $this->language->phrase('title_page_x', ['page' => $page]);
if ($pageTitle instanceof \XF\PreEscaped)
{
$pageTitle = clone $pageTitle;
$pageTitle->value .= $pageAppend;
}
else
{
$pageTitle .= $pageAppend;
}
}
if ($formatter)
{
$value = sprintf($formatter,
$this->escape($pageTitle, $escape),
$this->escape($fallback, $escape)
);
$escape = false;
return $value;
}
else
{
return $pageTitle;
}
}
else
{
return $fallback;
}
}
protected function getPrefixPhraseName($contentType, $id, $group = false)
{
return $contentType . '_prefix' . ($group ? '_group.' : '.') . $id;
}
public function fnParens($templater, &$escape, $value)
{
return $this->filterParens($templater, $value, $escape);
}
public function fnParseLessColor($templater, &$escape, $value)
{
/** @var \XF\CssRenderer $renderer */
$rendererClass = $this->app->extendClass('XF\CssRenderer');
$renderer = new $rendererClass($this->app, $this);
$renderer->setStyle($this->style);
return $renderer->parseLessColorValue($value);
}
public function fnPrefix($templater, &$escape, $contentType, $prefixId, $format = 'html', $append = null)
{
if (!is_int($prefixId))
{
$prefixId = $prefixId->prefix_id;
}
if (!$prefixId)
{
return '';
}
$prefixCache = $this->app->container('prefixes.' . $contentType);
$prefixClass = isset($prefixCache[$prefixId]) ? $prefixCache[$prefixId] : null;
if (!$prefixClass)
{
return '';
}
$output = $this->fn('prefix_title', [$contentType, $prefixId], false);
switch ($format)
{
case 'html':
$output = '<span class="' . htmlspecialchars($prefixClass) . '" dir="auto">'
. \XF::escapeString($output, 'html') . '</span>';
if ($append === null)
{
$append = '<span class="label-append"> </span>';
}
break;
case 'plain':
if ($output instanceof \XF\Phrase)
{
$output = $output->render('raw');
}
break; // ok as is
default:
$output = \XF::escapeString($output, 'html'); // just be safe and escape everything else
}
if ($append === null)
{
$append = ' - ';
}
$escape = false;
return $output . $append;
}
public function fnPrefixGroup($templater, &$escape, $contentType, $groupId)
{
if ($groupId == 0)
{
return '(' . \XF::phrase('ungrouped') . ')';
}
return \XF::phrase($this->getPrefixPhraseName($contentType, $groupId, true), [], false);
}
public function fnPrefixTitle($templater, &$escape, $contentType, $prefixId)
{
return \XF::phrase($this->getPrefixPhraseName($contentType, $prefixId), [], false);
}
public function fnProperty($templater, &$escape, $name, $fallback = null)
{
$escape = false;
if (!$this->style)
{
return $fallback;
}
return $this->style->getProperty($name, $fallback);
}
public function fnRand($templater, &$escape, $min = 0, $max = 999)
{
return mt_rand($min, $max);
}
public function fnRange($templater, &$escape, $start, $end, $step = 1)
{
return range($start, $end, $step);
}
public function fnRedirectInput($templater, &$escape, $url = null, $fallbackUrl = null, $useReferrer = true)
{
$escape = false;
if ($url)
{
$redirect = $this->app->request()->convertToAbsoluteUri($url);
}
else
{
$redirect = $this->app->getDynamicRedirect($fallbackUrl ?: null, (bool)$useReferrer);
}
return '<input type="hidden" name="_xfRedirect" value="' . htmlspecialchars($redirect) . '" />';
}
public function fnRepeat($templater, &$escape, $string, $count)
{
return str_repeat($string, $count);
}
public function fnRepeatRaw($templater, &$escape, $string, $count)
{
$escape = false;
return str_repeat($string, $count);
}
public function fnShowIgnored($templater, &$escape, array $attributes = [])
{
$escape = false;
if (!\XF::visitor()->user_id)
{
return '';
}
$wrapperClass = $this->processAttributeToRaw($attributes, 'wrapperclass', '', true);
$wrapper = $this->processAttributeToRaw($attributes, 'wrapper');
if ($wrapperClass && !$wrapper)
{
$wrapper = 'div';
}
$class = $this->processAttributeToRaw($attributes, 'class', ' %s', true);
$unhandledAttrs = $this->processUnhandledAttributes($attributes);
$html = '<a href="javascript:"'
. ' class="showIgnoredLink is-hidden js-showIgnored' . $class . '" data-xf-init="tooltip"'
. ' title="' . \XF::phrase('show_hidden_content_by_x', ['names' => '{{names}}']) . '"'
. ' ' . $unhandledAttrs . '>' .
\XF::phrase('show_ignored_content')
. '</a>';
if ($wrapper)
{
$wrapperOpen = $wrapper . ($wrapperClass ? " class=\"$wrapperClass\"" : '');
$html = "<{$wrapperOpen}>{$html}</{$wrapper}>";
}
return $html;
}
public function fnSmilie($templater, &$escape, $smilieString)
{
$escape = false;
$formatter = $this->app->stringFormatter();
return $formatter->replaceSmiliesHtml($smilieString);
}
public function fnSnippet($templater, &$escape, $string, $maxLength = 0, array $options = [])
{
// if we aren't escaping here
$needsEscaping = ($escape ? true : false);
$escape = false;
$formatter = $this->app->stringFormatter();
$string = $formatter->snippetString($string, $maxLength, $options);
if (!empty($options['term']))
{
return $formatter->highlightTermForHtml(
$string, $options['term'], isset($options['highlightClass']) ? $options['highlightClass'] : 'textHighlight'
);
}
else
{
return $needsEscaping ? \XF::escapeString($string) : $string;
}
}
public function fnStrlen($templater, &$escape, $string)
{
return utf8_strlen($string);
}
public function fnContains($templater, &$escape, $haystack, $needle)
{
return utf8_strpos(utf8_strtolower($haystack), utf8_strtolower($needle)) !== false;
}
public function fnStructuredText($templater, &$escape, $string, $nl2br = true)
{
$stringFormatter = $this->app->stringFormatter();
$string = $stringFormatter->censorText($string);
$string = \XF::escapeString($string);
$string = $stringFormatter->autoLinkStructuredText($string);
$string = $stringFormatter->linkStructuredTextMentions($string);
if ($nl2br)
{
$string = nl2br($string);
}
$escape = false;
return $string;
}
public function fnTemplater($templater, &$escape)
{
$escape = false;
return $templater;
}
public function fnTime($templater, &$escape, $time, $format = null)
{
return $this->language->time($time, $format);
}
public function fnTransparentImg($templater, &$escape)
{
return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
}
public function fnTrim($templater, &$escape, $str, $charlist = " \t\n\r\0\x0B")
{
return trim($str, $charlist);
}
public function fnUniqueId($templater, &$escape, $baseValue = null)
{
if ($baseValue === null)
{
$this->uniqueIdCounter++;
$baseValue = $this->uniqueIdCounter;
}
return sprintf($this->uniqueIdFormat, $baseValue);
}
public function fnUserActivity($templater, &$escape, $user)
{
if (!$user instanceof \XF\Entity\User || !$user->user_id)
{
return '';
}
if (!$user->canViewOnlineStatus())
{
return '';
}
$output = '';
$hasActivity = false;
if ($user->canViewCurrentActivity() && $user->Activity)
{
if ($user->Activity->description)
{
$output .= \XF::escapeString($user->Activity->description);
if ($user->Activity->item_title)
{
$title = \XF::escapeString($user->Activity->item_title);
$url = \XF::escapeString($user->Activity->item_url);
$output .= " <em><a href=\"{$url}\" dir=\"auto\">{$title}</a></em>";
}
$output .= ' <span role="presentation" aria-hidden="true">·</span> ';
$hasActivity = true;
}
}
$output .= $this->fnDateDynamic($this, $escape, $user->last_activity);
if ($hasActivity && $user->Activity->view_state == 'error' && \XF::visitor()->canBypassUserPrivacy())
{
$output .= ' <span role="presentation" aria-hidden="true">·</span> ';
$output .= '<i class="fa fa-warning u-muted" title="' . $this->filterForAttr($this,\XF::phrase('viewing_an_error'), $null) . '" aria-hidden="true"></i>';
$output .= ' <span class="u-srOnly">' . \XF::phrase('viewing_an_error') . '</span>';
}
$escape = false;
return $output;
}
public function fnUserBanners($templater, &$escape, $user, $attributes = [])
{
/** @var \XF\Entity\User $user */
$escape = false;
if (!$user || !($user instanceof \XF\Entity\User) || !$user->user_id)
{
/** @var \XF\Repository\User $userRepo */
$userRepo = $this->app->repository('XF:User');
$user = $userRepo->getGuestUser();
}
$class = $this->processAttributeToRaw($attributes, 'class', ' %s', true);
if (!empty($attributes['tag']))
{
$tag = htmlspecialchars($attributes['tag']);
}
else
{
$tag = 'em';
}
unset($attributes['tag']);
$unhandledAttrs = $this->processUnhandledAttributes($attributes);
$banners = [];
$config = $this->userBannerConfig;
if (!empty($config['showStaff']) && $user->is_staff)
{
$p = \XF::phrase('staff_member');
$banners['staff'] = "<{$tag} class=\"userBanner userBanner--staff{$class}\" dir=\"auto\"{$unhandledAttrs}>"
. "<span class=\"userBanner-before\"></span><strong>{$p}</strong><span class=\"userBanner-after\"></span></{$tag}>";
}
$memberGroupIds = $user->secondary_group_ids;
$memberGroupIds[] = $user->user_group_id;
foreach ($this->userBanners AS $groupId => $banner)
{
if (!in_array($groupId, $memberGroupIds))
{
continue;
}
$banners[$groupId] = "<{$tag} class=\"userBanner {$banner['class']}{$class}\"{$unhandledAttrs}>"
. "<span class=\"userBanner-before\"></span><strong>{$banner['text']}</strong><span class=\"userBanner-after\"></span></{$tag}>";
}
if (!$banners)
{
return '';
}
if (!empty($config['displayMultiple']))
{
return implode("\n", $banners);
}
else if (!empty($config['showStaffAndOther']) && isset($banners['staff']) && count($banners) >= 2)
{
$staffBanner = $banners['staff'];
unset($banners['staff']);
return $staffBanner . "\n" . reset($banners);
}
else
{
return reset($banners);
}
}
public function fnUserBlurb($templater, &$escape, $user, $attributes = [])
{
if (!$user instanceof \XF\Entity\User)
{
return '';
}
$blurbParts = [];
$userTitle = $this->fnUserTitle($this, $escape, $user);
if ($userTitle)
{
$blurbParts[] = $userTitle;
}
if ($user->Profile->age)
{
$blurbParts[] = $user->Profile->age;
}
if ($user->Profile->location)
{
$location = \XF::escapeString($user->Profile->location);
if (\XF::options()->geoLocationUrl)
{
$location = '<a href="' . $this->app->router('public')->buildLink('misc/location-info', null, ['location' => $location]) . '" class="u-concealed" target="_blank" rel="nofollow noreferrer">' . $location. '</a>';
}
$blurbParts[] = \XF::phrase('from_x_location', ['location' => new \XF\PreEscaped($location)])->render();
}
$tag = $this->processAttributeToRaw($attributes, 'tag');
if (!$tag)
{
$tag = 'div';
}
$class = $this->processAttributeToRaw($attributes, 'class', '%s', true);
$unhandledAttrs = $this->processUnhandledAttributes($attributes);
return "<{$tag} class=\"{$class}\" dir=\"auto\" {$unhandledAttrs}>"
. implode(' <span role="presentation" aria-hidden="true">·</span> ', $blurbParts)
. "</{$tag}>";
}
public function fnUserTitle($templater, &$escape, $user, $withBanner = false, $attributes = [])
{
/** @var \XF\Entity\User $user */
$escape = false;
$userIsValid = ($user instanceof \XF\Entity\User);
$userTitle = null;
if ($userIsValid)
{
$customTitle = $user->custom_title;
if ($customTitle)
{
$userTitle = htmlspecialchars($customTitle);
}
}
if ($userTitle === null)
{
if ($withBanner && !empty($this->userBannerConfig['hideUserTitle']))
{
if (!$userIsValid)
{
return '';
}
if (!empty($this->userBannerConfig['showStaff']) && $user->is_staff)
{
return '';
}
if ($user->isMemberOf(array_keys($this->userBanners)))
{
return '';
}
}
if ($userIsValid)
{
$groupId = $user->display_style_group_id;
if (!empty($this->groupStyles[$groupId]['user_title']))
{
$userTitle = $this->groupStyles[$groupId]['user_title'];
}
else
{
foreach ($this->userTitleLadder AS $points => $title)
{
if ($user[$this->userTitleLadderField] >= $points)
{
$userTitle = $title;
break;
}
}
}
}
else
{
$guestGroupId = 1;
if (empty($this->groupStyles[$guestGroupId]['user_title']))
{
return '';
}
$userTitle = $this->groupStyles[$guestGroupId]['user_title'];
}
}
if ($userTitle === null || !strlen($userTitle))
{
return '';
}
$class = $this->processAttributeToRaw($attributes, 'class', ' %s', true);
if (!empty($attributes['tag']))
{
$tag = htmlspecialchars($attributes['tag']);
}
else
{
$tag = 'span';
}
unset($attributes['tag']);
$unhandledAttrs = $this->processUnhandledAttributes($attributes);
return "<{$tag} class=\"userTitle{$class}\" dir=\"auto\"{$unhandledAttrs}>{$userTitle}</{$tag}>";
}
public function fnUsernameLink($templater, &$escape, $user, $rich = false, $attributes = [])
{
$escape = false;
if (isset($attributes['username']))
{
$username = $attributes['username'];
}
else if (isset($user['username']) && $user['username'] !== '')
{
$username = $user['username'];
}
else if (isset($attributes['defaultname']))
{
$username = $attributes['defaultname'];
}
else
{
return '';
}
$noTooltip = !empty($attributes['notooltip']);
if (isset($attributes['href']))
{
$href = $attributes['href'];
$noTooltip = true; // custom URL so tooltip won't work and might be misleading
}
else
{
$linkPath = $this->currentTemplateType == 'admin' ? 'users/edit' : 'members';
$href = !empty($user['user_id']) ? $this->getRouter()->buildLink($linkPath, $user) : null;
if (!$href || $this->currentTemplateType == 'admin')
{
$noTooltip = true;
}
}
$hrefAttr = $href ? ' href="' . htmlspecialchars($href) . '"' : '';
$class = $this->processAttributeToRaw($attributes, 'class', ' %s', true);
$usernameStylingClasses = $this->fnUsernameClasses($this, $null, $user, $rich);
$xfInit = $this->processAttributeToRaw($attributes, 'data-xf-init', '', true);
if (!$noTooltip)
{
$xfInit = ltrim("$xfInit member-tooltip");
}
$xfInitAttr = $xfInit ? " data-xf-init=\"$xfInit\"" : '';
unset($attributes['username'], $attributes['defaultname'], $attributes['href'], $attributes['notooltip']);
$unhandledAttrs = $this->processUnhandledAttributes($attributes);
$userId = !empty($user['user_id']) ? intval($user['user_id']) : 0;
$username = htmlspecialchars($username);
if ($usernameStylingClasses)
{
$username = "<span class=\"{$usernameStylingClasses}\">{$username}</span>";
}
if ($hrefAttr)
{
$tag = 'a';
}
else
{
$tag = 'span';
}
return "<{$tag}{$hrefAttr} class=\"username {$class}\" dir=\"auto\" data-user-id=\"{$userId}\"{$xfInitAttr}{$unhandledAttrs}>{$username}</{$tag}>";
}
public function fnUsernameLinkEmail($templater, &$escape, $user, $defaultName = '', array $attributes = [])
{
$escape = false;
if (isset($attributes['username']))
{
$username = $attributes['username'];
}
else if (isset($user['username']) && $user['username'] !== '')
{
$username = $user['username'];
}
else if ($defaultName !== '')
{
$username = $defaultName;
}
else
{
return '';
}
unset($attributes['username']);
if (isset($attributes['href']))
{
$href = $attributes['href'];
}
else
{
$href = !empty($user['user_id']) ? $this->getRouter()->buildLink('canonical:members', $user) : null;
}
$hrefAttr = $href ? ' href="' . htmlspecialchars($href) . '"' : '';
$tag = $href ? 'a' : 'span';
unset($attributes['username'], $attributes['href']);
$unhandledAttrs = $this->processUnhandledAttributes($attributes);
$username = htmlspecialchars($username);
return "<{$tag} dir=\"auto\"{$hrefAttr}{$unhandledAttrs}>{$username}</{$tag}>";
}
public function fnUsernameClasses($templater, &$escape, $user, $includeGroupStyling = true)
{
$classes = [];
if ($includeGroupStyling)
{
if (!$user || empty($user['user_id']))
{
$displayGroupId = 1;
}
else
{
if (!empty($user['display_style_group_id']))
{
$displayGroupId = $user['display_style_group_id'];
}
else
{
$displayGroupId = 0;
}
}
if ($displayGroupId && !empty($this->groupStyles[$displayGroupId]['username_css']))
{
$classes[] = 'username--style' . $displayGroupId;
}
}
if (!empty($user['is_banned']) && \XF::visitor()->canBypassUserPrivacy())
{
$classes[] = 'username--banned';
}
foreach (['staff', 'moderator', 'admin'] AS $userType)
{
if (!empty($user["is_{$userType}"]))
{
$classes[] = "username--{$userType}";
}
}
$escape = false; // note: not doing this explicitly, shouldn't be needed for the output format
return implode(' ', $classes);
}
public function fnWidgetData($templater, &$escape, $widgetData, $asArray = false)
{
$output = [];
$escape = false;
if (isset($widgetData['id']))
{
if ($asArray)
{
$output['data-widget-id'] = $widgetData['id'];
}
else
{
$output[] = 'data-widget-id="' . $widgetData['id'] . '"';
}
}
if (isset($widgetData['key']))
{
if ($asArray)
{
$output['data-widget-key'] = $widgetData['key'];
}
else
{
$output[] = 'data-widget-key="' . $widgetData['key'] . '"';
}
}
if (isset($widgetData['definition']))
{
if ($asArray)
{
$output['data-widget-definition'] = $widgetData['definition'];
}
else
{
$output[] = 'data-widget-definition="' . $widgetData['definition'] . '"';
}
}
if ($asArray)
{
return $output ? $output : [];
}
else
{
return $output ? ' ' . implode(' ', $output) : '';
}
}
////////////////////// FILTERS //////////////////////////
public function filterDefault($templater, $value, &$escape, $defaultValue)
{
if ($value === null)
{
$value = $defaultValue;
}
return $value;
}
public function filterCensor($templater, $value, &$escape, $censorChar = null)
{
return $this->app->stringFormatter()->censorText($value, $censorChar);
}
public function filterCurrency($templater, $value, &$escape, $code = '', $format = null)
{
$currency = $this->app->data('XF:Currency');
return $currency->languageFormat($value, $code, $this->language, $format);
}
public function filterEscape($templater, $value, &$escape, $type = true)
{
$escape = $type;
return $value;
}
public function filterForAttr($templater, $value, &$escape)
{
// this is a sanity check to make sure even pre-escaped values are escaped and can't break out of
// an HTML attribute
$escape = false;
return htmlspecialchars(strval($value), ENT_QUOTES, 'UTF-8', false);
}
public function filterFileSize($templater, $value, &$escape)
{
return $this->language->fileSizeFormat($value);
}
public function filterFirst($templater, $value, &$escape)
{
if (is_array($value))
{
return reset($value);
}
else if ($value instanceof AbstractCollection)
{
return $value->first();
}
else
{
return $value;
}
}
public function filterHex($templater, $value, &$escape)
{
return bin2hex($value);
}
public function filterHost($templater, $value, &$escape)
{
return \XF\Util\Ip::getHost($value);
}
public function filterIp($templater, $value, &$escape)
{
return \XF\Util\Ip::convertIpBinaryToString($value);
}
public function filterJoin($templater, $value, &$escape, $join = ',')
{
if (!$this->isTraversable($value))
{
return '';
}
$parts = [];
foreach ($value AS $child)
{
$parts[] = $escape ? $this->escape($child, $escape) : $child;
}
$escape = false;
return implode($join, $parts);
}
public function filterJson($templater, $value, &$escape, $prettyPrint = false)
{
if ($prettyPrint)
{
$output = \XF\Util\Json::jsonEncodePretty($value, false);
// do limited slash escaping to improve readability
$output = str_replace('</', '<\\/', $output);
}
else
{
$output = json_encode($value);
}
$output = str_replace('<!', '\u003C!', $output);
return $output;
}
public function filterLast($templater, $value, &$escape)
{
if (is_array($value))
{
return end($value);
}
else if ($value instanceof AbstractCollection)
{
return $value->last();
}
else
{
return $value;
}
}
public function filterNl2Br($templater, $value, &$escape)
{
if ($escape)
{
$value = $this->escape($value, $escape);
}
$escape = false;
return nl2br($value);
}
public function filterNl2Nl($templater, $value, &$escape)
{
if ($escape)
{
$value = $this->escape($value, $escape);
}
$escape = false;
return str_replace('\n', "\n", $value);
}
public function filterNumber($templater, $value, &$escape, $precision = 0)
{
return $this->language->numberFormat($value, $precision);
}
public function filterNumberShort($templater, $value, &$escape)
{
return $this->language->shortNumberFormat($value);
}
public function filterZeroFill($templater, $value, &$escape, $length = 3)
{
if (is_int($value))
{
$length = intval($length);
return sprintf("%0{$length}d", $value);
}
return $value;
}
public function filterPad($templater, $value, &$escape, $padChar, $length, $postPad = false)
{
$length = intval($length);
$padChar = substr($padChar, 0, 1);
$postPad = $postPad ? '-' : '';
return sprintf("%{$postPad}'{$padChar}{$length}s", $value);
}
public function filterParens($templater, $value, &$escape)
{
$value = (string)$value;
if (strlen($value))
{
$value = $this->language['parenthesis_open'] . $value . $this->language['parenthesis_close'];
}
return $value;
}
public function filterPluck($templater, $value, &$escape, $valueField, $keyField = null)
{
if (!$this->isTraversable($value))
{
return [];
}
$parts = [];
foreach ($value AS $key => $child)
{
if ($keyField !== null && isset($child[$keyField]))
{
$key = $child[$keyField];
}
$parts[$key] = isset($child[$valueField]) ? $child[$valueField] : null;
}
return $parts;
}
public function filterPreEscaped($templater, $value, &$escape, $type = 'html')
{
$escape = false;
return $this->preEscaped($value, $type);
}
public function filterRaw($templater, $value, &$escape)
{
$escape = false;
return $value;
}
public function filterReplace($templater, $value, &$escape, $from, $to = null)
{
if ($value instanceof \XF\Mvc\Entity\AbstractCollection)
{
$value = $value->toArray();
}
if (!is_array($from))
{
$from = [$from => $to];
}
if (!is_array($from))
{
return $value;
}
if (is_array($value))
{
return array_replace($value, $from);
}
else if (is_string($value))
{
return str_replace(array_keys($from), $from, $value);
}
else
{
return $value;
}
}
public function filterSplit($templater, $value, &$escape, $delimiter = ',', $limit = PHP_INT_MAX)
{
switch ($delimiter)
{
case ',':
$split = @preg_split('#\s*,\s*#', $value, $limit, PREG_SPLIT_NO_EMPTY);
break;
case 'nl':
$split = @preg_split('/\r?\n/', $value, $limit, PREG_SPLIT_NO_EMPTY);
break;
default:
$split = @explode($delimiter, $value, $limit);
break;
}
if (!is_array($split))
{
$split = [];
}
return $split;
}
public function filterStripTags($templater, $value, &$escape, $allowableTags = null)
{
return strip_tags($value, $allowableTags);
}
public function filterToLower($templater, $value, &$escape, $type = 'strtolower')
{
switch ($type)
{
case 'lcfirst': return lcfirst($value);
case 'strtolower': return utf8_strtolower($value);
default:
trigger_error("Invalid to lower type '{$type}' provided.", E_USER_WARNING);
return '';
}
}
public function filterToUpper($templater, $value, &$escape, $type = 'strtoupper')
{
switch ($type)
{
case 'ucfirst':
case 'ucwords':
case 'strtoupper':
$f = 'utf8_' . $type;
return $f($value);
default:
trigger_error("Invalid to upper type '{$type}' provided.", E_USER_WARNING);
return '';
}
}
public function filterUrl($templater, $value, &$escape, $component = null, $fallback = '')
{
$result = @parse_url($value);
if (!$result)
{
return $fallback;
}
if (!$component)
{
return $value;
}
if (isset($result[$component]))
{
return $result[$component];
}
else
{
return $fallback;
}
}
public function filterUrlencode($templater, $value, &$escape)
{
return urlencode($value);
}
////////////////////// TESTS ////////////////////////
public function testEmpty($templater, $value)
{
if (is_object($value) && is_callable([$value, '__toString']))
{
return strval($value) === '';
}
if ($value instanceof \Countable)
{
return count($value) == 0;
}
return ($value === '' || $value === false || $value === null || $value === []);
}
////////////////////// FORM ELEMENTS ////////////////////////
public function mergeChoiceOptions($original, $additional)
{
if ($original instanceof \Traversable)
{
$original = iterator_to_array($original, false);
}
else if (!is_array($original))
{
$original = [];
}
if ($this->isTraversable($additional))
{
foreach ($additional AS $key => $option)
{
if (is_string($option)
|| is_numeric($option)
|| (is_object($option) && method_exists($option, '__toString'))
)
{
$original[] = [
'value' => $key,
'label' => \XF::escapeString($option),
'_type' => 'option'
];
}
}
}
return $original;
}
public function processAttributeToHtmlAttribute(array &$attributes, $name, $fallbackValue = '', $appendFallback = false)
{
return $this->processAttributeToNamedHtmlAttribute($attributes, $name, $name, $fallbackValue, $appendFallback);
}
public function processAttributeToNamedHtmlAttribute(array &$attributes, $sourceName, $targetName, $fallbackValue = '', $appendFallback = false)
{
if (isset($attributes[$sourceName]))
{
$value = $attributes[$sourceName];
if ($appendFallback && $fallbackValue)
{
$value .= " $fallbackValue";
}
}
else
{
$value = $fallbackValue;
}
unset($attributes[$sourceName]);
if (is_array($value))
{
return '';
}
$value = strval($value);
if ($value === '')
{
return '';
}
else
{
return " $targetName=\"" . \XF::escapeString($value) . "\"";
}
}
public function processCodeAttribute(array &$attributes)
{
if (isset($attributes['code']))
{
if ($attributes['code'] === 'true' || $attributes['code'] === 1)
{
$attributes['dir'] = 'ltr';
$attributes['class'] = (empty($attributes['class']) ? 'input--code' : $attributes['class'] . ' input--code');
}
unset($attributes['code']);
}
}
public function processBooleanAttributeHtml(array &$attributes, $name, $outputAttribute)
{
if (!isset($attributes[$name]))
{
return '';
}
$value = $attributes[$name];
unset($attributes[$name]);
if ($value)
{
return " $outputAttribute";
}
else
{
return '';
}
}
public function processAttributeToRaw(array &$attributes, $name, $formatter = '', $escapeValue = false)
{
if (isset($attributes[$name]))
{
$value = strval($attributes[$name]);
if ($value !== '')
{
if ($escapeValue)
{
$value = \XF::escapeString($value);
}
if ($formatter)
{
if ($formatter instanceof \Closure)
{
$value = $formatter($value);
}
else
{
$value = sprintf($formatter, $value);
}
}
}
}
else
{
$value = '';
}
unset($attributes[$name]);
return $value;
}
protected function processUnhandledAttributes(array $attributes)
{
$output = '';
foreach ($attributes AS $name => $value)
{
if (is_array($value))
{
continue;
}
if ($value instanceof \XF\Phrase)
{
// strval will do escaping of the values or the whole phrase, so get the raw value and escape that here
$value = $value->render('raw');
}
else
{
$value = strval($value);
}
if ($value !== '')
{
$output .= " $name=\"" . \XF::escapeString($value) . "\"";
}
}
return $output;
}
protected function processDynamicAttributes(array &$attributes, array $skip = [])
{
if (!isset($attributes['attributes']))
{
return;
}
foreach ($attributes['attributes'] AS $key => $attribute)
{
if ($key == 'attributes' || isset($attributes[$key]) || isset($skip[$key]))
{
continue;
}
$attributes[$key] = $attribute;
}
unset($attributes['attributes']);
}
protected function handleChoices(array $choices, \Closure $choiceFormatter, \Closure $groupFormatter)
{
$html = '';
foreach ($choices AS $choice)
{
if (isset($choice['_type']))
{
$type = $choice['_type'];
}
else
{
$type = 'option';
}
unset($choice['_type']);
if ($type == 'optgroup')
{
$childHtml = $this->handleChoices($choice['options'], $choiceFormatter, $groupFormatter);
unset($choice['options']);
$html .= $groupFormatter($choice, $childHtml);
}
else
{
$dependent = !empty($choice['_dependent']) ? $choice['_dependent'] : [];
foreach ($dependent AS $key => &$val)
{
$val = trim($val);
if (!strlen($val))
{
unset($dependent[$key]);
}
}
unset($choice['_dependent']);
$html .= $choiceFormatter($choice, $dependent);
}
}
return $html;
}
public function isChoiceSelected(array $choice, $inputValue, $allowMultiple = false)
{
if (isset($choice['selected']))
{
return $choice['selected'];
}
if ($inputValue !== null)
{
$choiceValue = isset($choice['value']) ? strval($choice['value']) : '';
if (is_array($inputValue) && $allowMultiple)
{
return in_array($choiceValue, $inputValue);
}
else if (!is_array($inputValue))
{
return (
($inputValue === true && $choiceValue === '1')
|| ($inputValue === false && $choiceValue === '0')
|| (strval($inputValue) === $choiceValue)
);
}
}
return false;
}
public function formHiddenVal($name, $value, array $extraAttributes = [])
{
$this->processDynamicAttributes($extraAttributes);
$nameHtml = \XF::escapeString($name);
$valueHtml = \XF::escapeString($value);
$extraAttrs = $this->processUnhandledAttributes($extraAttributes);
return "<input type=\"hidden\" name=\"{$nameHtml}\" value=\"{$valueHtml}\"{$extraAttrs} />";
}
public function formCheckBox(array $controlOptions, array $choices)
{
$this->processDynamicAttributes($controlOptions);
$name = \XF::escapeString($this->processAttributeToRaw($controlOptions, 'name'));
if ($name && substr($name, -2) != '[]')
{
$name .= '[]';
}
$readOnly = $this->processAttributeToRaw($controlOptions, 'readonly');
$value = isset($controlOptions['value']) ? $controlOptions['value'] : null;
$standalone = ($this->processAttributeToRaw($controlOptions, 'standalone') && count($choices) == 1);
$choiceFormatter = function(array $choice, array $dependent) use ($name, $readOnly, $value, $standalone)
{
$selected = $this->isChoiceSelected($choice, $value, true);
if (!empty($choice['name']))
{
$localName = \XF::escapeString($choice['name']);
}
else
{
$localName = $name;
}
if ($localName)
{
$nameAttr = ' name="' . $localName . '"';
}
else
{
$nameAttr = '';
}
unset($choice['selected'], $choice['name'], $choice['type']);
$dependentHtml = '';
if ($dependent && !$standalone)
{
$dependentHtmlInner = '';
foreach ($dependent AS $child)
{
$dependentHtmlInner .= "\n\t\t\t\t<li class=\"inputChoices-option\">$child</li>";
}
$dependentHtml = "\n\t\t\t<ul class=\"inputChoices-dependencies\">{$dependentHtmlInner}\n\t\t\t</ul>\n\t\t";
}
if ($dependentHtml)
{
$this->addElementHandler($choice, 'disabler');
}
$labelClass = 'iconic iconic--checkbox';
$label = trim($this->processAttributeToRaw($choice, 'label'));
if ($label !== '')
{
$labelClass .= ' iconic--labelled';
}
$labelClas***tra = $this->processAttributeToRaw($choice, 'labelclass', '', true);
if ($labelClas***tra !== '')
{
$labelClass .= " {$labelClas***tra}";
}
$hiddenLabel = $this->processAttributeToRaw($choice, 'hiddenlabel');
if ($label && $hiddenLabel != '')
{
$hiddenLabel = true;
}
else
{
$hiddenLabel = false;
}
if ($label && $hiddenLabel)
{
$label = '<span class="u-srOnly">' . $label . '</span>';
}
$titleAttr = $this->processAttributeToHtmlAttribute($choice, 'title');
$tooltipAttr = '';
if ($choice['data-xf-init'] == 'tooltip')
{
$tooltipAttr = $this->processAttributeToHtmlAttribute($choice, 'data-xf-init');
}
$checkAll = $this->processAttributeToRaw($choice, 'check-all');
if ($checkAll != '')
{
$choice['data-xf-init'] .= (empty($choice['data-xf-init']) ? '' : ' ') . 'check-all';
$choice['data-container'] = $checkAll;
}
$hint = $this->processAttributeToRaw($choice, 'hint', "\n\t\t\t\t\t<dfn class=\"inputChoices-explain\">%s</dfn>");
$extraHtml = $this->processAttributeToRaw($choice, 'html', "\n\t\t\t\t\t%s");
$afterHint = $this->processAttributeToRaw($choice, 'afterhint', "\n\t\t\t<dfn class=\"inputChoices-explain inputChoices-explain--after\">%s</dfn>");
$afterHtml = $this->processAttributeToRaw($choice, 'afterhtml', "\n\t\t\t%s");
$valueAttr = $this->processAttributeToHtmlAttribute($choice, 'value');
if (!$valueAttr)
{
$valueAttr = ' value="1"';
}
$selectedAttr = $selected ? ' checked="checked"' : '';
$readOnlyAttr = $readOnly ? ' readonly="readonly" onclick="return false"' : '';
if ($readOnly)
{
$labelClass .= ' is-readonly';
}
if (isset($choice['defaultvalue']) && $localName && substr($localName, -2) != '[]')
{
// $localName is escaped
$defaultValueInput = '<input type="hidden" name="' . $localName
. '" value="' . \XF::escapeString($choice['defaultvalue']) . '" />';
unset($choice['defaultvalue']);
}
else
{
$defaultValueInput = '';
}
$attributes = $this->processUnhandledAttributes($choice);
$checkboxHtml = $defaultValueInput . "<label class=\"{$labelClass}\"{$titleAttr}{$tooltipAttr}>"
. "<input type=\"checkbox\" {$nameAttr}{$valueAttr}{$selectedAttr}{$readOnlyAttr}{$attributes} />"
. "<i aria-hidden=\"true\"></i>{$label}</label>{$hint}{$extraHtml}{$dependentHtml}{$afterHint}{$afterHtml}";
if ($standalone)
{
return $checkboxHtml . "\n";
}
else
{
return "<li class=\"inputChoices-choice\">{$checkboxHtml}</li>\n";
}
};
$groupFormatter = function(array $group, $html)
{
$label = $this->processAttributeToRaw($group, 'label');
if ($label)
{
$checkAll = $this->processAttributeToRaw($group, 'check-all');
if ($checkAll)
{
$label = '<label class="iconic iconic--checkbox iconic--labelled inputChoices-heading-checkAll">
<input type="checkbox" data-xf-init="check-all" data-container="< .inputChoices-group" /><i aria-hidden="true"></i>'
. $label . '</label>';
}
$class = $this->processAttributeToRaw($group, 'class', '', true);
$listClass = $this->processAttributeToRaw($group, 'listclass', '', true);
$html = "<li class=\"inputChoices-group {$class}\">
<div class=\"inputChoices-heading\">{$label}</div>
<ul class=\"inputChoices {$listClass}\">{$html}</ul>
</li>";
}
return $html;
};
$choiceHtml = $this->handleChoices($choices, $choiceFormatter, $groupFormatter);
$hideEmpty = $this->processAttributeToRaw($controlOptions, 'hideempty');
if ($hideEmpty && !$choiceHtml)
{
return '';
}
if ($standalone)
{
return $choiceHtml;
}
$listClassAttr = $this->processAttributeToNamedHtmlAttribute($controlOptions, 'listclass', 'class', 'inputChoices', true);
$unhandledAttrs = $this->processUnhandledAttributes($controlOptions);
return "
<ul{$listClassAttr}{$unhandledAttrs}>
$choiceHtml
</ul>
";
}
public function formCheckBoxRow(array $controlOptions, array $choices, array $rowOptions)
{
$controlHtml = $this->formCheckBox($controlOptions, $choices);
return $controlHtml ? $this->formRow($controlHtml, $rowOptions) : '';
}
public function formRadio(array $controlOptions, array $choices)
{
$this->processDynamicAttributes($controlOptions);
$name = \XF::escapeString($this->processAttributeToRaw($controlOptions, 'name'));
$readOnly = $this->processAttributeToRaw($controlOptions, 'readonly');
$value = isset($controlOptions['value']) ? $controlOptions['value'] : null;
unset($controlOptions['value']);
$standalone = ($this->processAttributeToRaw($controlOptions, 'standalone') && count($choices) == 1);
$choiceFormatter = function(array $choice, array $dependent) use ($name, $readOnly, $value, $standalone)
{
$selected = $this->isChoiceSelected($choice, $value, false);
unset($choice['selected'], $choice['type']);
$dependentHtml = '';
if ($dependent)
{
$dependentHtmlInner = '';
foreach ($dependent AS $child)
{
$dependentHtmlInner .= "\n\t\t\t\t<li class=\"inputChoices-choice\">$child</li>";
}
$dependentHtml = "\n\t\t\t<ul class=\"inputChoices-dependencies\">{$dependentHtmlInner}\n\t\t\t</ul>\n\t\t";
}
if ($dependentHtml)
{
$this->addElementHandler($choice, 'disabler');
}
$labelClass = 'iconic iconic--radio';
$label = trim($this->processAttributeToRaw($choice, 'label'));
if ($label !== '')
{
$labelClass .= ' iconic--labelled';
}
$labelClas***tra = $this->processAttributeToRaw($choice, 'labelclass', '', true);
if ($labelClas***tra !== '')
{
$labelClass .= " {$labelClas***tra}";
}
$titleAttr = $this->processAttributeToHtmlAttribute($choice, 'title');
$tooltipAttr = '';
if ($choice['data-xf-init'] == 'tooltip')
{
$tooltipAttr = $this->processAttributeToHtmlAttribute($choice, 'data-xf-init');
}
$hint = $this->processAttributeToRaw($choice, 'hint', "\n\t\t\t\t\t<dfn class=\"inputChoices-explain\">%s</dfn>");
$extraHtml = $this->processAttributeToRaw($choice, 'html', "\n\t\t\t\t\t%s");
$valueAttr = $this->processAttributeToHtmlAttribute($choice, 'value');
if (!$valueAttr)
{
$valueAttr = ' value=""';
}
$selectedAttr = $selected ? ' checked="checked"' : '';
$readOnlyAttr = $readOnly ? ' readonly="readonly" onclick="return false"' : '';
if ($readOnly)
{
$labelClass .= ' is-readonly';
}
$listItemClass = $this->processAttributeToNamedHtmlAttribute($choice, 'listitemclass', 'class', 'inputChoices-choice', true);
$attributes = $this->processUnhandledAttributes($choice);
$radioHtml = "<label class=\"{$labelClass}\"{$titleAttr}{$tooltipAttr}>"
. "<input type=\"radio\" name=\"$name\"{$valueAttr}{$selectedAttr}{$readOnlyAttr}{$attributes} />"
. "<i aria-hidden=\"true\"></i>{$label}</label>{$hint}{$dependentHtml}{$extraHtml}";
if ($standalone)
{
return $radioHtml . "\n";
}
else
{
return "<li{$listItemClass}>{$radioHtml}</li>\n";
}
};
$groupFormatter = function(array $group, $html)
{
$label = $this->processAttributeToRaw($group, 'label');
if ($label)
{
$class = $this->processAttributeToRaw($group, 'class', '', true);
$listClass = $this->processAttributeToRaw($group, 'listclass', '', true);
$html = "<li class=\"inputChoices-group {$class}\">
<div class=\"inputChoices-heading\">{$label}</div>
<ul class=\"inputChoices {$listClass}\">{$html}</ul>
</li>";
}
};
$choiceHtml = $this->handleChoices($choices, $choiceFormatter, $groupFormatter);
$hideEmpty = $this->processAttributeToRaw($controlOptions, 'hideempty');
if ($hideEmpty && !$choiceHtml)
{
return '';
}
if ($standalone)
{
return $choiceHtml;
}
$listClassAttr = $this->processAttributeToNamedHtmlAttribute($controlOptions, 'listclass', 'class', 'inputChoices', true);
$unhandledAttrs = $this->processUnhandledAttributes($controlOptions);
return "
<ul{$listClassAttr}{$unhandledAttrs}>
$choiceHtml
</ul>
";
}
public function formRadioRow(array $controlOptions, array $choices, array $rowOptions)
{
$controlHtml = $this->formRadio($controlOptions, $choices);
return $controlHtml ? $this->formRow($controlHtml, $rowOptions) : '';
}
public function formSelect(array $controlOptions, array $choices)
{
$this->processDynamicAttributes($controlOptions);
$name = \XF::escapeString($this->processAttributeToRaw($controlOptions, 'name'));
$value = isset($controlOptions['value']) ? $controlOptions['value'] : null;
unset($controlOptions['value']);
$multiple = !empty($controlOptions['multiple']);
if ($multiple)
{
$multipleAttr = ' multiple="multiple"';
if ($name && substr($name, -2) != '[]')
{
$name .= '[]';
}
}
else
{
$multipleAttr = '';
}
unset($controlOptions['multiple']);
$choiceFormatter = function(array $choice) use ($name, $value, $multiple)
{
$selected = $this->isChoiceSelected($choice, $value, $multiple);
unset($choice['selected'], $choice['explain']);
$label = trim($this->processAttributeToRaw($choice, 'label'));
if ($label === '')
{
$label = ' ';
}
$valueAttr = $this->processAttributeToHtmlAttribute($choice, 'value');
if (!$valueAttr)
{
$valueAttr = ' value=""';
}
$selectedAttr = $selected ? ' selected="selected"' : '';
$disabled = $this->processAttributeToRaw($choice, 'disabled');
$disabledAttr = $disabled ? ' disabled="disabled"': '';
$attributes = $this->processUnhandledAttributes($choice);
return "<option{$valueAttr}{$selectedAttr}{$disabledAttr}{$attributes}>{$label}</option>\n";
};
$groupFormatter = function(array $group, $html)
{
if (!$html)
{
return '';
}
$attributes = $this->processUnhandledAttributes($group);
return "<optgroup{$attributes}>\n$html</optgroup>";
};
$choiceHtml = $this->handleChoices($choices, $choiceFormatter, $groupFormatter);
$hideEmpty = $this->processAttributeToRaw($controlOptions, 'hideempty');
if ($hideEmpty && !$choiceHtml)
{
return '';
}
$readOnly = $this->processAttributeToRaw($controlOptions, 'readonly');
$disabled = $this->processAttributeToRaw($controlOptions, 'disabled');
if ($readOnly)
{
$this->addToClassAttribute($controlOptions, 'is-readonly');
$disabled = true;
}
$disabledAttr = $disabled ? ' disabled="disabled"' : '';
$classAttr = $this->processAttributeToHtmlAttribute($controlOptions, 'class', 'input', true);
$unhandledAttrs = $this->processUnhandledAttributes($controlOptions);
$select = "
<select name=\"{$name}\"{$multipleAttr}{$classAttr}{$disabledAttr}{$unhandledAttrs}>
$choiceHtml
</select>
";
if ($readOnly && $value !== null)
{
if ($multiple)
{
if (is_array($value))
{
foreach ($value AS $subValue)
{
$select .= '<input type="hidden" name="' . $name . '" value="' . \XF::escapeString($subValue) . '" />';
}
}
}
else
{
$select .= '<input type="hidden" name="' . $name . '" value="' . \XF::escapeString($value) . '" />';
}
}
return $select;
}
public function formSelectRow(array $controlOptions, array $choices, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formSelect($controlOptions, $choices);
return $controlHtml ? $this->formRow($controlHtml, $rowOptions, $controlId) : '';
}
public function formSubmitRow(array $controlOptions, array $rowOptions)
{
$this->processDynamicAttributes($controlOptions);
$sticky = $this->processAttributeToRaw($controlOptions, 'sticky');
if ($sticky && $sticky != 'false')
{
$this->addElementHandler($rowOptions, 'form-submit-row', 'rowclass');
if ($sticky != 'true' && !is_numeric($sticky)) // indicates a container selector
{
$rowOptions['data-container'] = $sticky;
}
}
$buttonClasses = 'button button--primary';
$icon = $this->processAttributeToRaw($controlOptions, 'icon');
if ($icon)
{
$buttonClasses .= ' button--icon button--icon--' . preg_replace('#[^a-zA-Z0-9_-]#', '', $icon);
}
$classAttr = $this->processAttributeToHtmlAttribute($controlOptions, 'class', $buttonClasses, true);
$submit = strval($this->processAttributeToRaw($controlOptions, 'submit'));
if (!$submit && $icon)
{
$submit = $this->getButtonPhraseFromIcon($icon, 'button.submit');
}
$unhandledControlAttrs = $this->processUnhandledAttributes($controlOptions);
if (strlen($submit))
{
$controlHtml = "<button {$classAttr}{$unhandledControlAttrs}><span class=\"button-text\">{$submit}</span></button>";
}
else
{
$controlHtml = '';
}
$extraHtml = $this->processAttributeToRaw($rowOptions, 'html', "\n\t\t\t\t%s");
$class = $this->processAttributeToRaw($rowOptions, 'rowclass', ' %s', true);
if ($sticky)
{
$class .= ' formSubmitRow--sticky';
}
$rowType = $this->processAttributeToRaw($rowOptions, 'rowtype');
if ($rowType)
{
$class = $this->appendClassList($class, $rowType, 'formSubmitRow--%s');
}
$unhandledRowAttrs = $this->processUnhandledAttributes($rowOptions);
return "
<dl class=\"formRow formSubmitRow{$class}\"{$unhandledRowAttrs}>
<dt></dt>
<dd>
<div class=\"formSubmitRow-main\">
<div class=\"formSubmitRow-bar\"></div>
<div class=\"formSubmitRow-controls\">{$controlHtml}{$extraHtml}</div>
</div>
</dd>
</dl>
";
}
public function formTextArea(array $controlOptions)
{
$this->processDynamicAttributes($controlOptions);
$this->processCodeAttribute($controlOptions);
$autosize = $this->processAttributeToRaw($controlOptions, 'autosize');
if ($autosize)
{
$this->addElementHandler($controlOptions, 'textarea-handler');
$classAppend = ' input--fitHeight';
}
else
{
$classAppend = '';
}
$maxLength = $this->processAttributeToRaw($controlOptions, 'maxlength');
if ($maxLength)
{
$maxlengthAttr = " maxlength=\"{$maxLength}\"";
}
else
{
$maxlengthAttr = '';
}
$value = \XF::escapeString($this->processAttributeToRaw($controlOptions, 'value'));
$readOnlyAttr = $this->processAttributeToRaw($controlOptions, 'readonly') ? ' readonly="readonly"' : '';
$classAttr = $this->processAttributeToHtmlAttribute($controlOptions, 'class', 'input' . $classAppend, true);
$unhandledAttrs = $this->processUnhandledAttributes($controlOptions);
return "<textarea{$classAttr}{$readOnlyAttr}{$maxlengthAttr}{$unhandledAttrs}>{$value}</textarea>";
}
public function formTextAreaRow(array $controlOptions, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formTextArea($controlOptions);
return $this->formRow($controlHtml, $rowOptions, $controlId);
}
public function formDateInput(array $controlOptions)
{
$this->processDynamicAttributes($controlOptions);
$class = $this->processAttributeToRaw($controlOptions, 'class', ' %s', true);
$xfInit = $this->processAttributeToRaw($controlOptions, 'data-xf-init', ' %s', true);
$xfInitAttr = " data-xf-init=\"date-input$xfInit\"";
$weekStart = $this->processAttributeToRaw($controlOptions, 'week-start', '', true);
if (!$weekStart)
{
$weekStart = $this->language['week_start'];
}
$weekStartAttr = " data-week-start=\"$weekStart\"";
$readOnly = $this->processAttributeToRaw($controlOptions, 'readonly');
$readOnlyAttr = $readOnly ? ' readonly="readonly"' : '';
$unhandledAttrs = $this->processUnhandledAttributes($controlOptions);
return "<div class=\"inputGroup inputGroup--date inputGroup--joined inputDate\"><input type=\"text\" class=\"input input--date {$class}\"{$xfInitAttr}{$weekStartAttr}{$readOnlyAttr}{$unhandledAttrs} /><span class=\"inputGroup-text inputDate-icon\"></span></div>";
}
public function formDateInputRow(array $controlOptions, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formDateInput($controlOptions);
return $this->formRow($controlHtml, $rowOptions, $controlId);
}
public function formCodeEditor(array $controlOptions)
{
$this->processDynamicAttributes($controlOptions);
$name = $this->processAttributeToRaw($controlOptions, 'name');
$value = $this->processAttributeToRaw($controlOptions, 'value');
$extraClasses = $this->processAttributeToRaw($controlOptions, 'class');
/** @var \XF\Data\CodeLanguage $codeLanguageData */
$codeLanguageData = $this->app->data('XF:CodeLanguage');
$supportedLanguages = $codeLanguageData->getSupportedLanguages();
$mode = $this->processAttributeToRaw($controlOptions, 'mode');
if (isset($supportedLanguages[$mode]))
{
$modeConfig = $supportedLanguages[$mode];
}
else
{
$modeConfig = [];
}
$readOnly = $this->processAttributeToRaw($controlOptions, 'readonly');
if ($readOnly)
{
$extraClasses .= ' is-readonly';
}
$attrsHtml = $this->processUnhandledAttributes($controlOptions);
return $this->renderTemplate('public:code_editor', [
'name' => $name,
'value' => $value,
'lang' => $mode,
'modeConfig' => $modeConfig,
'extraClasses' => $extraClasses,
'readOnly' => $readOnly,
'attrsHtml' => $attrsHtml
]);
}
public function formCodeEditorRow(array $controlOptions, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formCodeEditor($controlOptions);
return $this->formRow($controlHtml, $rowOptions, $controlId);
}
public function formEditor(array $controlOptions)
{
$this->processDynamicAttributes($controlOptions);
$name = $this->processAttributeToRaw($controlOptions, 'name');
$value = $this->processAttributeToRaw($controlOptions, 'value');
$styleAttr = $this->processAttributeToRaw($controlOptions, 'style');
if (!isset($controlOptions['previewable']))
{
$previewable = true;
}
else
{
$previewable = (bool)$this->processAttributeToRaw($controlOptions, 'previewable');
}
if (!isset($controlOptions['rows']))
{
$controlOptions['rows'] = '10';
}
$attachments = isset($controlOptions['attachments']) ? $controlOptions['attachments'] :[];
if (!$this->isTraversable($attachments))
{
$attachments = [];
}
unset($controlOptions['attachments']);
$bbCodeContainer = $this->app->bbCode();
$customIcons = [];
foreach ($bbCodeContainer['custom'] AS $k => $custom)
{
if ($custom['editor_icon_type'])
{
$customIcons[$k] = [
'title' => \XF::phrase('custom_bb_code_title.' . $k),
'type' => $custom['editor_icon_type'],
'value' => $custom['editor_icon_value'],
'option' => $custom['has_option']
];
}
}
if (substr($name, -1) == ']')
{
$htmlName = substr($name, 0, -1) . '_html]';
}
else
{
$htmlName = $name . '_html';
}
if ($value !== '')
{
$htmlValue = $this->app->bbCode()->render($value, 'editorHtml', 'editor', null, [
'attachments' => $attachments
]);
}
else
{
$htmlValue = '';
}
if (!isset($controlOptions['data-min-height']))
{
$controlOptions['data-min-height'] = 250;
}
$height = intval($controlOptions['data-min-height']);
$removeButtons = [];
$hasSmilies = $this->app->smilies;
if (isset($controlOptions['removebuttons']))
{
$removeButtons = $controlOptions['removebuttons'];
}
if (!$hasSmilies)
{
$removeButtons[] = '_smilies';
}
if (isset($controlOptions['maxlength']) && empty($controlOptions['maxlength']))
{
unset($controlOptions['maxlength']);
}
$attrsHtml = $this->processUnhandledAttributes($controlOptions);
$config = $this->app->config();
return $this->renderTemplate('public:editor', [
'name' => $name,
'htmlName' => $htmlName,
'value' => $value,
'attachments' => $attachments,
'htmlValue' => $htmlValue,
'styleAttr' => $styleAttr,
'attrsHtml' => $attrsHtml,
'customIcons' => $customIcons,
'previewable' => $previewable,
'height' => $height,
'removeButtons' => array_unique($removeButtons),
'fullEditorJs' => ($config['development']['fullJs'] && $config['development']['fullEditorJs'])
]);
}
public function formEditorRow(array $controlOptions, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formEditor($controlOptions);
return $this->formRow($controlHtml, $rowOptions, $controlId);
}
public function formPrefixInput($prefixes, array $controlOptions)
{
$this->processDynamicAttributes($controlOptions);
$prefixType = $this->processAttributeToRaw($controlOptions, 'type');
$prefixName = $this->processAttributeToRaw($controlOptions, 'prefix-name');
$textboxName = $this->processAttributeToRaw($controlOptions, 'textbox-name');
$prefixClass = $this->processAttributeToRaw($controlOptions, 'prefix-class', ' %s');
$textboxClass = $this->processAttributeToRaw($controlOptions, 'textbox-class', ' %s');
$prefixValue = $this->processAttributeToRaw($controlOptions, 'prefix-value');
$textboxValue = $this->processAttributeToRaw($controlOptions, 'textbox-value');
$href = $this->processAttributeToRaw($controlOptions, 'href');
$listenTo = $this->processAttributeToRaw($controlOptions, 'listen-to');
$rows = $this->processAttributeToRaw($controlOptions, 'rows');
$attrsHtml = $this->processUnhandledAttributes($controlOptions);
return $this->renderTemplate('public:prefix_input', [
'prefixes' => $prefixes ?: [],
'prefixType' => $prefixType,
'prefixName' => $prefixName ?: 'prefix_id',
'prefixClass' => $prefixClass,
'textboxClass' => $textboxClass,
'textboxName' => $textboxName ?: 'title',
'prefixValue' => $prefixValue ?: 0,
'textboxValue' => $textboxValue ?: $this->zeroValueValid($textboxValue),
'href' => $href,
'listenTo' => $listenTo,
'rows' => $rows,
'attrsHtml' => $attrsHtml
]);
}
protected function zeroValueValid($var)
{
if ($var === 0 || $var === '0')
{
return $var;
}
return '';
}
public function formPrefixInputRow($prefixes, array $controlOptions, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formPrefixInput($prefixes, $controlOptions);
return $this->formRow($controlHtml, $rowOptions, $controlId);
}
public function formTextBox(array $controlOptions)
{
$this->processDynamicAttributes($controlOptions);
$units = ($controlOptions['type'] == 'number' && !empty($controlOptions['units'])
? $controlOptions['units']
: '');
unset($controlOptions['units']);
$this->processCodeAttribute($controlOptions);
$typeAttr = $this->processAttributeToHtmlAttribute($controlOptions, 'type', 'text');
$class = $this->processAttributeToRaw($controlOptions, 'class', '', true);
$xfInit = $this->processAttributeToRaw($controlOptions, 'data-xf-init', '', true);
$acSingle = '';
$autoComplete = $this->processAttributeToRaw($controlOptions, 'ac');
if ($autoComplete)
{
if ($autoComplete == 'single')
{
$acSingle = " data-single=\"true\"";
}
$xfInit = ltrim("$xfInit auto-complete");
}
$xfInitAttr = $xfInit ? " data-xf-init=\"$xfInit\"" : '';
$readOnlyAttr = $this->processAttributeToRaw($controlOptions, 'readonly') ? ' readonly="readonly"' : '';
$unhandledAttrs = $this->processUnhandledAttributes($controlOptions);
$input = "<input{$typeAttr} class=\"" . trim("input {$class}") . "\"{$xfInitAttr}{$acSingle}{$readOnlyAttr}{$unhandledAttrs} />";
if ($units)
{
return "<div class=\"inputGroup inputGroup--numbers\">$input<span class=\"inputGroup-text\">$units</span></div>";
}
else
{
return $input;
}
}
public function formTextBoxRow(array $controlOptions, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formTextBox($controlOptions);
return $this->formRow($controlHtml, $rowOptions, $controlId);
}
public function formNumberBox(array $controlOptions)
{
$this->processDynamicAttributes($controlOptions);
$min = isset($controlOptions['min']) ? $controlOptions['min'] : null;
$max = isset($controlOptions['max']) ? $controlOptions['max'] : null;
$step = isset($controlOptions['step']) ? $controlOptions['step'] : 1;
$minAttr = '';
$maxAttr = '';
$stepAttr = '';
if ($min !== null)
{
$minAttr = ' min="' . htmlspecialchars($min) . '"';
}
if ($max !== null)
{
$maxAttr = ' max="' . htmlspecialchars($max) . '"';
}
if ($step)
{
$stepAttr = ' step="' . htmlspecialchars($step) . '"';
}
$type = 'number';
if ($typeAttr = $this->processAttributeToRaw($controlOptions, 'type', '', true))
{
$type = $typeAttr;
}
// This is mostly targeting iOS which presents a symbol + number keyboard by default for the number input.
// If step contains a decimal point or could support negative values then don't force a pattern, otherwise
// assume it's \d* which will force the numeric only keypad on iOS.
if ($step == 'any' || strpos($step, '.') !== false || ($min === null || $min < 0))
{
$pattern = '';
}
else
{
$pattern = '\d*';
}
if (isset($controlOptions['value']))
{
$controlOptions['value'] = trim($controlOptions['value']);
if (preg_match('/[^0-9.-]/', $controlOptions['value']))
{
if (preg_match('/^{{(?:\s+)?(?:.*)(?:\s+)?}}$/', $controlOptions['value']))
{
// not a valid number but looks like a mustache/field adder template
$value = $controlOptions['value'];
}
else
{
// value isn't a valid number
$value = '';
}
}
else
{
$value = $controlOptions['value'];
}
}
else if (isset($controlOptions['min']))
{
$value = $controlOptions['min'];
}
else
{
$value = '';
}
$hasRequired = isset($controlOptions['required']);
$required = $this->processAttributeToRaw($controlOptions, 'required');
if (isset($controlOptions['min']) && !$hasRequired)
{
$required = true;
}
$requiredAttr = $required ? ' required="required"' : '';
$units = !empty($controlOptions['units']) ? $controlOptions['units'] : '';
unset(
$controlOptions['min'],
$controlOptions['max'],
$controlOptions['step'],
$controlOptions['value'],
$controlOptions['units']
);
$class = $this->processAttributeToRaw($controlOptions, 'class', ' %s', true);
$xfInit = $this->processAttributeToRaw($controlOptions, 'data-xf-init', '', true);
$xfInitAttr = $xfInit ? " data-xf-init=\"$xfInit\"" : '';
$readOnlyAttr = $this->processAttributeToRaw($controlOptions, 'readonly') ? ' readonly="readonly"' : '';
$unhandledAttrs = $this->processUnhandledAttributes($controlOptions);
$input = "<div class=\"inputGroup inputGroup--numbers inputNumber\" data-xf-init=\"number-box\">"
. "<input type=\"{$type}\" pattern=\"{$pattern}\" class=\"input input--number js-numberBoxTextInput{$class}\" value=\"{$value}\" {$minAttr}{$maxAttr}{$stepAttr}{$requiredAttr}{$readOnlyAttr}{$xfInitAttr}{$unhandledAttrs} />"
. "</div>";
if ($units)
{
return "<div class=\"inputGroup\">$input<div class=\"inputGroup\"><span class='inputGroup--splitter'></span><span class=\"inputGroup-text\">$units</span></div></div>";
}
else
{
return $input;
}
}
public function formNumberBoxRow(array $controlOptions, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formNumberBox($controlOptions);
return $this->formRow($controlHtml, $rowOptions, $controlId);
}
public function formTokenInput(array $controlOptions)
{
$this->processDynamicAttributes($controlOptions);
$name = $this->processAttributeToRaw($controlOptions, 'name');
$value = $this->processAttributeToRaw($controlOptions, 'value');
$hrefAttr = $this->processAttributeToRaw($controlOptions, 'href');
$styleAttr = $this->processAttributeToRaw($controlOptions, 'style');
$minLength = $this->processAttributeToRaw($controlOptions, 'min-length');
$maxLength = $this->processAttributeToRaw($controlOptions, 'max-length');
$maxTokens = $this->processAttributeToRaw($controlOptions, 'max-tokens');
$attrsHtml = $this->processUnhandledAttributes($controlOptions);
return $this->renderTemplate('public:token_input', [
'name' => $name,
'value' => $value,
'hrefAttr' => $hrefAttr,
'styleAttr' => $styleAttr,
'minLength' => $minLength,
'maxLength' => $maxLength,
'maxTokens' => $maxTokens,
'attrsHtml' => $attrsHtml
]);
}
public function formTokenInputRow(array $controlOptions, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formTokenInput($controlOptions);
return $this->formRow($controlHtml, $rowOptions, $controlId);
}
public function formUpload(array $controlOptions)
{
$this->processDynamicAttributes($controlOptions);
$class = $this->processAttributeToRaw($controlOptions, 'class', '', true);
$unhandledAttrs = $this->processUnhandledAttributes($controlOptions);
return "<input type=\"file\" class=\"input {$class}\"{$unhandledAttrs} />";
}
public function formUploadRow(array $controlOptions, array $rowOptions)
{
$this->addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');
$controlId = $this->assignFormControlId($controlOptions);
$controlHtml = $this->formUpload($controlOptions);
return $this->formRow($controlHtml, $rowOptions, $controlId);
}
protected function assignFormControlId(array &$controlOptions)
{
if (!empty($controlOptions['id']))
{
return $controlOptions['id'];
}
$controlOptions['id'] = $this->fn('unique_id');
return $controlOptions['id'];
}
public function formRow($contentHtml, array $rowOptions, $controlId = null)
{
$class = $this->processAttributeToRaw($rowOptions, 'rowclass', ' %s', true);
$rowType = $this->processAttributeToRaw($rowOptions, 'rowtype');
if ($rowType)
{
$class = $this->appendClassList($class, $rowType, 'formRow--%s');
}
$id = $this->processAttributeToRaw($rowOptions, 'rowid');
$idAttr = $id ? ' id="' . htmlspecialchars($id) . '"' : '';
if (isset($rowOptions['controlid']))
{
$controlId = $rowOptions['controlid'];
unset($rowOptions['controlid']);
}
$labelFor = $controlId ? ' for="' . htmlspecialchars($controlId) . '"' : '';
$label = $this->processAttributeToRaw($rowOptions, 'label', "\n\t\t\t\t\t<label class=\"formRow-label\"{$labelFor}>%s</label>");
$hint = $this->processAttributeToRaw($rowOptions, 'hint', "\n\t\t\t\t\t<dfn class=\"formRow-hint\">%s</dfn>");
$initialHtml = $this->processAttributeToRaw($rowOptions, 'initialhtml', "\n\t\t\t\t\t%s");
$html = $this->processAttributeToRaw($rowOptions, 'html', "\n\t\t\t\t\t%s");
$explain = $this->processAttributeToRaw($rowOptions, 'explain', "\n\t\t\t\t\t<div class=\"formRow-explain\">%s</div>");
$finalHtml = $this->processAttributeToRaw($rowOptions, 'finalhtml', "\n\t\t\t\t\t%s");
$unhandledAttrs = $this->processUnhandledAttributes($rowOptions);
return '
<dl class="formRow' . $class . '"' . $idAttr . $unhandledAttrs . '>
<dt>
<div class="formRow-labelWrapper">' . $label . $hint . '</div>
</dt>
<dd>
' . $initialHtml // stuff to go before the control (rarely)
. $contentHtml // controls etc.
. $html // extra HTML, dependent controls etc.
. $explain // final <p.explain> that describes all the above
. $finalHtml // used for <input hidden> etc.
. '
</dd>
</dl>
';
}
public function formRowIfContent($contentHtml, array $rowOptions, $controlId = null)
{
$contentHtml = trim($contentHtml);
if (!strlen($contentHtml))
{
return '';
}
else
{
return $this->formRow($contentHtml, $rowOptions, $controlId);
}
}
public function formInfoRow($contentHtml, array $rowOptions)
{
$class = $this->processAttributeToRaw($rowOptions, 'rowclass', ' %s', true);
$rowType = $this->processAttributeToRaw($rowOptions, 'rowtype');
if ($rowType)
{
$class = $this->appendClassList($class, $rowType, 'formInfoRow--%s');
}
$unhandledRowAttrs = $this->processUnhandledAttributes($rowOptions);
return "
<div class=\"formInfoRow{$class}\"{$unhandledRowAttrs}>
{$contentHtml}
</div>
";
}
public function form($contentHtml, array $options)
{
$this->processDynamicAttributes($options);
$method = $this->processAttributeToRaw($options, 'method', '', true);
if (!$method)
{
$method = 'post';
}
$getFormParams = '';
$action = $this->processAttributeToRaw($options, 'action', '', true);
if ($action && strtolower($method) == 'get')
{
$qStart = strpos($action, '?');
if ($qStart !== false)
{
$qString = htmlspecialchars_decode(substr($action, $qStart + 1));
$action = substr($action, 0, $qStart);
if (preg_match('/^([^=&]*)(&|$)/', $qString, $qStringUrl))
{
$route = $qStringUrl[1];
$qString = substr($qString, strlen($qStringUrl[0]));
}
else
{
$route = '';
}
if ($route !== '')
{
$getFormParams .= $this->formHiddenVal('_xfRoute', $route);
}
if ($qString)
{
$params = \XF\Util\Arr::parseQueryString($qString);
foreach ($params AS $name => $value)
{
$getFormParams .= "\n\t" . $this->formHiddenVal($name, $value);
}
}
}
}
$ajax = $this->processAttributeToRaw($options, 'ajax');
$class = $this->processAttributeToRaw($options, 'class', '', true);
$upload = $this->processAttributeToRaw($options, 'upload', '', true);
$encType = $this->processAttributeToRaw($options, 'enctype', '', true);
$preview = $this->processAttributeToRaw($options, 'preview', '', true);
$xfInit = $this->processAttributeToRaw($options, 'data-xf-init', '', true);
if ($ajax)
{
$xfInit = ltrim("$xfInit ajax-submit");
}
$encTypeAttr = '';
if ($encType)
{
$encTypeAttr = " enctype=\"$encType\"";
}
else if ($upload)
{
$encTypeAttr = " enctype=\"multipart/form-data\"";
}
$previewUrlAttr = '';
if ($preview)
{
$xfInit = ltrim("$xfInit preview");
$previewUrlAttr = " data-preview-url=\"$preview\"";
}
$draftAttrs = $this->handleDraftAttribute($options, $class, $xfInit);
$xfInitAttr = $xfInit ? " data-xf-init=\"$xfInit\"" : '';
$unhandledAttrs = $this->processUnhandledAttributes($options);
if (strtolower($method) == 'post')
{
$csrfInput = $this->fn('csrf_input');
}
else
{
$csrfInput = '';
}
return "
<form action=\"{$action}\" method=\"{$method}\" class=\"{$class}\"
{$xfInitAttr}{$encTypeAttr}{$previewUrlAttr}{$draftAttrs}{$unhandledAttrs}
>
{$contentHtml}
{$csrfInput}
{$getFormParams}
</form>
";
}
protected function handleDraftAttribute(array &$options, &$class, &$xfInit)
{
$draftOptions = $this->app->options()->saveDrafts;
if (!empty($draftOptions['enabled']))
{
$draft = $this->processAttributeToRaw($options, 'draft', '', true);
if ($draft)
{
$xfInit = ltrim("$xfInit draft");
return " data-draft-url=\"$draft\" data-draft-autosave=\"$draftOptions[saveFrequency]\"";
}
}
unset($options['draft']);
return '';
}
public function dataList($contentHtml, array $options)
{
$this->processDynamicAttributes($options);
$class = $this->processAttributeToRaw($options, 'class', '', true);
$unhandledAttrs = $this->processUnhandledAttributes($options);
return "
<div class=\"dataList {$class}\"{$unhandledAttrs}>
<table class=\"dataList-table\">
{$contentHtml}
</table>
</div>
";
}
public function dataRow(array $options, array $cells = [])
{
if (!empty($options['rowtype']))
{
$rowType = $options['rowtype'];
}
else
{
$rowType = 'row';
}
if ($rowType == 'header')
{
if (!isset($options['rowclass']))
{
$options['rowclass'] = '';
}
$options['rowclass'] = trim($options['rowclass'] . ' dataList-row--header dataList-row--noHover');
}
else if ($rowType == 'subsection' || $rowType == 'subSection')
{
$rowType = 'subSection';
if (!isset($options['rowclass']))
{
$options['rowclass'] = '';
}
$options['rowclass'] = trim($options['rowclass'] . ' dataList-row--subSection');
}
$label = (isset($options['label']) && strlen($options['label'])) ? $options['label'] : null;
if ($label !== null)
{
$cell = [
'_type' => 'main',
'href' => !empty($options['href']) ? $options['href'] : null,
'target' => !empty($options['target']) ? $options['target'] : null,
'label' => $label,
'hint' => (isset($options['hint']) && strlen(trim($options['hint']))) ? $options['hint'] : null,
'explain' => (isset($options['explain']) && strlen(trim($options['explain']))) ? $options['explain'] : null,
'hash' => (isset($options['hash']) && strlen(trim($options['hash']))) ? $options['hash'] : null,
'colspan' => !empty($options['colspan']) ? $options['colspan'] : null,
'html' => ''
];
if (!empty($options['dir']))
{
$cell['dir'] = $options['dir'];
}
if (!empty($options['href']) && !empty($options['overlay']))
{
$cell['overlay'] = $options['overlay'];
foreach ($this->overlayClickOptions AS $attributeName)
{
if (isset($options[$attributeName]))
{
$cell[$attributeName] = $options[$attributeName];
}
}
}
array_unshift($cells, $cell);
}
$delete = (isset($options['delete']) && $options['delete']) ? $options['delete'] : null;
if ($delete)
{
$cells[] = [
'_type' => 'delete',
'href' => $delete,
'html' => ''
];
}
$rowClass = $this->processAttributeToRaw($options, 'rowclass', ' %s', true);
$cellsHtml = [];
foreach ($cells AS $cell)
{
$cellHtml = $this->getDataRowCell($rowType, $cell, $rowClass);
if ($cellHtml)
{
$cellsHtml[] = $cellHtml;
}
}
$html = implode("\n", $cellsHtml);
$unhandledAttrs = $this->processUnhandledAttributes($options);
return "
<tr class=\"dataList-row{$rowClass}\"{$unhandledAttrs}>
{$html}
</tr>
";
}
/**
* @param string $rowType Type of row; currently header or row
* @param array $cell Array of attributes for the cell itself
* @param string $rowClass Allows cells to affect the appearance of the parent row
*
* @return string
*/
protected function getDataRowCell($rowType, array $cell, &$rowClass = '')
{
$type = isset($cell['_type']) ? $cell['_type'] : 'cell';
unset($cell['_type']);
$html = isset($cell['html']) ? $cell['html'] : '';
unset($cell['html']);
$selected = !empty($cell['selected']);
unset($cell['selected']);
$class = $this->processAttributeToRaw($cell, 'class', ' %s', true);
if ($type == 'delete')
{
$html = ''; // ignored
}
else if ($type == 'toggle')
{
$name = $this->processAttributeToRaw($cell, 'name', '', true);
$inputType = $this->processAttributeToRaw($cell, 'type', '', true);
$class .= ' dataList-cell--iconic';
if (!$inputType)
{
$inputType = 'checkbox';
}
$hiddenHtml = '';
if (isset($cell['value']))
{
$value = $this->processAttributeToRaw($cell, 'value', '', true);
}
else
{
$value = '1';
if ($inputType == 'checkbox')
{
$hiddenHtml = "<input type=\"hidden\" name=\"{$name}\" value=\"0\" />";
}
}
$checkedHtml = $selected ? ' checked="checked"' : '';
$disabled = !empty($cell['disabled']);
unset($cell['disabled']);
$disabledHtml = $disabled ? ' disabled="disabled"' : '';
$tooltip = $this->processAttributeToRaw($cell, 'tooltip', '', true);
if ($tooltip)
{
$tooltipHtml = " data-xf-init=\"tooltip\" title=\"{$tooltip}\"";
}
else
{
$tooltipHtml = '';
}
$submit = $this->processAttributeToRaw($cell, 'submit', '', true);
if ($submit)
{
$labelClass = 'iconic iconic--toggle';
$submitHtml = ' data-xf-click="submit"';
if ($submit != 'true')
{
$submitHtml .= ' data-target="' . $submit . '"';
}
if ($inputType == 'checkbox' && !$selected)
{
$rowClass = $rowClass . ' dataList-row--disabled';
}
}
else
{
$labelClass = 'iconic';
$submitHtml = '';
}
$html = $hiddenHtml
. "<label class=\"{$labelClass}\"{$tooltipHtml}{$submitHtml}>"
. "<input type=\"{$inputType}\" name=\"{$name}\" value=\"{$value}\"{$checkedHtml}{$disabledHtml} /><i aria-hidden=\"true\"></i>"
. "</label>";
}
else if ($type == 'popup')
{
$label = (isset($cell['label']) && strlen(trim($cell['label']))) ? $cell['label'] : \XF::phrase('actions');
$outerHtml = '<a data-xf-click="menu" class="menuTrigger" role="button" tabindex="0" aria-expanded="false" aria-haspopup="true">' . $label . '</a>'
. $html;
$html = $outerHtml;
}
else if ($type == 'main')
{
$label = (isset($cell['label']) && strlen(trim($cell['label']))) ? $cell['label'] : null;
if ($label !== null)
{
$hint = (isset($cell['hint']) && strlen(trim($cell['hint']))) ? $cell['hint'] : null;
$explain = (isset($cell['explain']) && strlen(trim($cell['explain']))) ? $cell['explain'] : null;
if (!empty($cell['dir']))
{
$label = '<span dir="' . htmlspecialchars($cell['dir']) . '">' . $label . '</span>';
$explainDirAttr = ' dir="' . htmlspecialchars($cell['dir']) . '"';
}
else
{
$explainDirAttr = '';
}
$html = '<div class="dataList-mainRow">'
. $label
. ($hint !== null ? " <span class=\"dataList-hint\" dir=\"auto\">{$hint}</span>" : '') . '</div>'
. ($explain !== null ? "\n<div class=\"dataList-subRow\"{$explainDirAttr}>{$explain}</div>" : '');
}
unset($cell['dir']);
}
if (isset($cell['hash']) && strlen(trim($cell['hash'])))
{
$html = '<span class="u-anchorTarget" id="'
. htmlspecialchars($this->app->getRedirectHash($cell['hash']))
. '"></span>'
. $html;
}
unset($cell['hash']);
if (!strlen($html))
{
$html = ' ';
}
$isAction = ($type == 'action' || $type == 'delete');
$href = isset($cell['href']) ? htmlspecialchars($cell['href']) : '';
if ($href)
{
if (!$isAction)
{
$class .= ' dataList-cell--link';
}
$target = $this->processAttributeToRaw($cell, 'target', '', true);
if ($target)
{
$target = " target=\"{$target}\"";
}
if ($type == 'delete')
{
$class .= ' dataList-cell--iconic';
$tooltip = $this->processAttributeToRaw($cell, 'tooltip', '', true);
if (!$tooltip)
{
$tooltip = \XF::phrase('delete');
}
$html = "<a href=\"{$href}\" class=\"dataList-delete\" data-xf-init=\"tooltip\" title=\"{$tooltip}\" data-xf-click=\"overlay\"{$target}></a>";
}
else
{
$overlay = $this->processAttributeToRaw($cell, 'overlay', '', true);
if ($overlay)
{
$overlay = " data-xf-click=\"overlay\"";
foreach ($this->overlayClickOptions AS $attributeName)
{
if (isset($cell[$attributeName]))
{
$attributeValue = $this->processAttributeToRaw($cell, $attributeName, '', true);
$overlay .= " $attributeName=\"{$attributeValue}\"";
}
}
if (isset($cell['overlaycache']))
{
$overlayCache = $this->processAttributeToRaw($cell, 'overlaycache', '', true);
$overlay .= " data-cache=\"{$overlayCache}\"";
}
}
$html = "<a href=\"{$href}\" {$overlay}{$target}>{$html}</a>";
}
}
if ($isAction)
{
$class .= ' dataList-cell--action';
}
if ($type == 'toggle')
{
$class .= ' dataList-cell--link dataList-cell--min dataList-cell--toggle';
}
else if ($type == 'popup')
{
$class .= ' dataList-cell--alt dataList-cell--link dataList-cell--min';
}
else if ($type == 'main')
{
$class .= ' dataList-cell--main';
}
unset($cell['href'], $cell['label'], $cell['explain'], $cell['hint']);
$unhandledAttrs = $this->processUnhandledAttributes($cell);
$tag = ($rowType == 'header' ? 'th' : 'td');
return "<{$tag} class=\"dataList-cell{$class}\"{$unhandledAttrs}>{$html}</{$tag}>";
}
protected function addToClassAttribute(array &$options, $class, $key = 'class')
{
if (!isset($options[$key]))
{
$options[$key] = '';
}
if (strlen($options[$key]))
{
$options[$key] .= " $class";
}
else
{
$options[$key] = $class;
}
}
protected function appendClassList($existingClasses, $classList, $formatter = '')
{
if (!$classList)
{
return $existingClasses;
}
$classList = preg_replace('#[^a-z0-9_ -]#i', '', $classList);
foreach (preg_split('#\s+#', $classList, -1, PREG_SPLIT_NO_EMPTY) AS $class)
{
if ($formatter)
{
$class = sprintf($formatter, $class);
}
$existingClasses .= ' ' . $class;
}
return $existingClasses;
}
protected function addElementHandler(array &$attributes, $handler, $classAttr = 'class')
{
if (!isset($attributes['data-xf-init']))
{
$attributes['data-xf-init'] = '';
}
if (!preg_match('/(^|\s)' . $handler . '($|\s)/', $attributes['data-xf-init']))
{
if (strlen($attributes['data-xf-init']))
{
$attributes['data-xf-init'] .= ' ' . $handler;
}
else
{
$attributes['data-xf-init'] = $handler;
}
}
}
protected function getButtonPhraseFromIcon($icon, $fallback = '')
{
switch ($icon)
{
case 'attach':
case 'cancel':
case 'confirm':
case 'copy':
case 'delete':
case 'edit':
case 'export':
case 'import':
case 'login':
case 'merge':
case 'move':
case 'preview':
case 'purchase':
case 'save':
case 'search':
case 'sort':
case 'submit':
case 'translate':
$phrase = 'button.' . $icon;
break;
default:
$phrase = $fallback;
}
return $phrase ? \XF::phrase($phrase) : '';
}
public function renderNavigationClosure(\Closure $navHandler, $selectedNav = '', array $params = [], $addDefaultParams = true)
{
if ($addDefaultParams)
{
$params = array_merge($this->defaultParams, $params);
}
set_error_handler([$this, 'handleTemplateError']);
try
{
$output = $navHandler($this, $selectedNav, $params);
}
catch (\Exception $e)
{
if (\XF::$debugMode)
{
throw $e;
}
$this->app->logException($e, false, 'Error rendering navigation: ');
$output = null;
}
restore_error_handler();
return $output;
}
}
Mọi thắc mắc vui lòng phản hồi bên dưới
Chúc các bạn thành công!