<?php
namespace MMO\Hide\BbCode\Tag;
use MMO\CoreLib\Util\Plural;
use MMO\CoreLib\Util\Str;
use MMO\Hide\BbCode\AbstractTag;
use MMO\Hide\BbCode\Provider\CustomFunctionProvider;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\SyntaxError;
use XF\Repository\UserGroupRepository;
class Hide extends AbstractTag
{
/**
* @inheritDoc
*/
public function render($content, array $tagChildren, $tagOption, array $tag, array $options)
{
$visitor = $this->visitor();
$option = $this->options()->mhHideTagComprassion;
if($option != 'default')
{
return call_user_func_array([\XF::stringToClass($option, '%s\BbCode\Tag\%s'), 'renderHide'], [
$tagChildren,
$tagOption,
$tag,
$options,
$this->renderer
]);
}
if (preg_match ('/^\(.*\)$/', (string) $tag['option']))
{
$expression = $this->stripParentheses($tag['option']);
if(Str::contains($expression, $this->getAllowedTags()))
{
return $this->renderExpressionTag($expression, $visitor, $content, $options, $tag);
}
}
$title = [
'visible' => $this->getTitleVisible('mh_tag_hide'),
'hidden' => $this->getTitleHidden('mh_tag_hide')
];
return $this->renderTemplate($content, $title, [
'canView' => $visitor->user_id,
'options' => $options,
'tag' => $tag['tag']
]);
}
private function stripParentheses(string $expression): string
{
return Str::of($expression)->replaceFirst('(', '')->replaceLast(')', '')->__toString();
}
private function renderExpressionTag($expression, $visitor, $content, $options, $tag)
{
static $expressionLanguage = null;
if (!$expressionLanguage)
{
$expressionLanguage = new ExpressionLanguage();
$expressionLanguage->registerProvider(new CustomFunctionProvider());
}
$allowTag = [
'posts' => $visitor->message_count,
'likes' => $visitor->reaction_score,
'reactions' => $visitor->reaction_score,
'trophies' => $visitor->trophy_points,
'days' => \intval((\XF::$time - $visitor->register_date) / 86400),
'username' => $visitor->username,
'user_id' => $visitor->user_id,
'user_state' => $visitor->user_state,
'is_staff' => $visitor->is_staff,
'is_admin' => $visitor->is_admin,
'is_moderator' => $visitor->is_moderator
];
$string = $expression;
$string = $this->replaceIsMemberOf($string);
$string = $this->replaceComparison($string, $visitor);
$string = $this->replaceReplacements($string);
try
{
$canView = (bool)$expressionLanguage->evaluate($expression, $allowTag);
}
catch (SyntaxError $err)
{
return $this->handleSyntaxError($err);
}
return $this->renderTemplate($content, $string, [
'canView' => $canView,
'options' => $options,
'tag' => $tag['tag']
]);
}
private function replaceIsMemberOf($string)
{
return \preg_replace_callback('/isMemberOf\((.*?)\)/', function ($matches)
{
$groupIds = \array_map('intval', explode(',', $matches[1]));
/** @var UserGroupRepository $groupRepository */
$groupRepository = $this->repository(UserGroupRepository::class);
$groupTitles = $groupRepository->getUserGroupTitlePairs();
$groupNames = [];
foreach ($groupIds as $groupId)
{
if (isset($groupTitles[$groupId]))
{
$groupNames[] = $groupTitles[$groupId];
}
}
return \XF::phrase('mh_hide_tag_hide_is_member_of', [
'groups' => \implode(', ', $groupNames)
]);
}, $string);
}
private function replaceComparison($string, $visitor)
{
return preg_replace_callback('/(\w+)\s*([><=]{1,2})\s*(\d+|"[^"]*")/', function ($matches) use ($visitor)
{
$param = $matches[1];
$operator = $matches[2];
$value = $matches[3];
$paramMap = [
'posts' => \XF::phrase('mh_hide_tag_hide_cond_posts', [
'plural' => Plural::choice('mh_hide_tag_hide_cond_posts_plural', \intval($value)),
]),
'likes' => \XF::phrase('mh_hide_tag_hide_cond_likes', [
'plural' => Plural::choice('mh_hide_tag_hide_cond_likes_plural', \intval($value))
]),
'reactions' => \XF::phrase('mh_hide_tag_hide_cond_likes', [
'plural' => Plural::choice('mh_hide_tag_hide_cond_reactions_plural', \intval($value))
]),
'trophies' => \XF::phrase('mh_hide_tag_hide_cond_trophies', [
'plural' => Plural::choice('mh_hide_tag_hide_cond_trophies_plural', \intval($value))
]),
'days' => \XF::phrase('mh_hide_tag_hide_cond_days', [
'plural' => Plural::choice('mh_hide_tag_hide_cond_days_plural', \intval($value))
]),
'user_state' => \XF::phrase('user_state'),
'username' => \XF::phrase('username_is'),
'user_id' => \XF::phrase('user_id'),
];
$param_str = $paramMap[$param] ?? $param;
$propertyMap = [
'posts' => 'message_count',
'likes' => 'reaction_score',
'reactions' => 'reaction_score',
'trophies' => 'trophy_points',
'days' => 'register_date',
];
$property = $propertyMap[$param] ?? $param;
$operatorMap = [
'>' => function ($a, $b) { return $a > $b; },
'>=' => function ($a, $b) { return $a >= $b; },
'<' => function ($a, $b) { return $a < $b; },
'<=' => function ($a, $b) { return $a <= $b; },
'==' => function ($a, $b) { return $a == $b; },
'===' => function ($a, $b) { return $a === $b; },
'!=' => function ($a, $b) { return $a != $b; },
'!==' => function ($a, $b) { return $a !== $b; },
];
$propertyValue = $property === 'register_date' ? \intval((\XF::$time - $visitor->$property) / 86400) : $visitor->$property;
if (isset($operatorMap[$operator]))
{
$comparison = $operatorMap[$operator];
$result = $comparison($propertyValue, is_numeric($value) ? $value : str_replace('"', '', $value));
$color = $result ? 'now' : 'require';
}
else
{
$color = 'now'; // default value
}
return sprintf('%s %s %s <span class="bbCodeBlock-count--%s">(%s)</span>', $param_str, $operator, $value, $color, $propertyValue);
}, $string);
}
private function replaceReplacements($string)
{
$replacements = [
'and' => \XF::phrase('mh_hide_and'),
'&&' => \XF::phrase('mh_hide_and'),
'or' => \XF::phrase('mh_hide_or'),
'||' => \XF::phrase('mh_hide_or'),
'==' => \XF::phrase('mh_hide_is'),
'is_staff' => \XF::phrase('staff_member'),
'is_admin' => \XF::phrase('user_is_administrator'),
'is_moderator' => \XF::phrase('user_is_moderator'),
];
return Str::replace(array_keys($replacements), array_values($replacements), $string);
}
private function handleSyntaxError($error)
{
$phrase = $error->getMessage();
$params = [];
preg_replace_callback_array(
[
'/variable "(?<variable>[a-z_]+)" is not valid around position (?<position>[0-9]+) for expression (?<expression>.+?)\./i' => function ($match) use (&$phrase, &$params)
{
$phrase = 'mh_tag_hide_variable_x_is_not_valid_around_position_y_for_expression_z';
$params = array_filter($match, 'is_string', ARRAY_FILTER_USE_KEY);
},
'/Unexpected character "(?<variable>[!-\/:-@[-`{-~])" around position (?<position>[0-9]+) for expression (?<expression>.+?)\./i' => function ($match) use (&$phrase, &$params)
{
$phrase = 'mh_tag_hide_unexpected_character_variable_x_is_not_valid_around_position_y_for_expression_z';
$params = array_filter($match, 'is_string', ARRAY_FILTER_USE_KEY);
},
'/other exception with (?<param>[a-z]+)\./i' => function ($match) use (&$phrase, &$params)
{
$phrase = 'other_exception_with_x';
$params = array_filter($match, 'is_string', ARRAY_FILTER_USE_KEY);
}
],
$error->getMessage()
);
if ($phrase != $error->getMessage())
{
$error = \XF::phrase($phrase, $params);
}
else
{
$error = $error->getMessage();
}
return $this->templater->renderTemplate('public:mh_tag_hide_error_conditions', [
'error' => $error
]);
}
public function getAllowedTags(): array
{
return [
'posts',
'likes',
'trophies',
'days',
'username',
'user_id',
'user_state',
'isMemberOf',
'is_staff'
];
}
}