<?php
namespace App\Twig;
use App\Admin\Entity\StdDomainsValues;
use App\Admin\Entity\StdPagesPages;
use App\Service\PageInfoCacheService;
use Twig\Extension\AbstractExtension;
use Twig\Environment;
use Twig\TwigFunction;
use Twig\TwigFilter;
use Twig\TwigTest;
use App\Utils\Functions;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\Translation\TranslatorInterface;
class AppExtension extends AbstractExtension
{
protected $entityManager;
protected $parameterBag;
private $twig;
private Security $security;
private RequestStack $requestStack;
private TranslatorInterface $translator;
private PageInfoCacheService $pageInfoCache;
public function __construct(
EntityManagerInterface $entityManager,
ParameterBagInterface $parameterBag,
Environment $twig,
Security $security,
RequestStack $requestStack,
TranslatorInterface $translator,
PageInfoCacheService $pageInfoCache
) {
$this->entityManager = $entityManager;
$this->parameterBag = $parameterBag;
$this->security = $security;
$this->twig = $twig;
$this->requestStack = $requestStack;
$this->translator = $translator;
$this->pageInfoCache = $pageInfoCache;
}
public function getFunctions(): array
{
return [
new TwigFunction('url_content', [$this, 'getUrlContent']),
new TwigFunction('file_get_contents',[$this, 'fileGetContents']),
new TwigFunction('clean_string', [$this, 'cleanString']),
new TwigFunction('get_config', [$this, 'getConfig']),
new TwigFunction('get_env', [$this, 'getVariables']),
new TwigFunction('generate_barcode', [$this, 'generateBarcode']),
new TwigFunction('array_search', 'array_search'),
new TwigFunction('is_mobile', [$this, 'isMobile']),
new TwigFunction('trim_text', [$this, 'trimText']),
new TwigFunction('get_page_info', [$this, 'getPageInfo']),
new TwigFunction('get_image_size', [$this, 'getImageSize']),
new TwigFunction('get_domain_value_description', [$this, 'getDomainValueDescription']),
new TwigFunction('get_domain_value', [$this, 'getDomainValue']),
new TwigFunction('get_relation_children', [$this, 'getRelationChildren']),
new TwigFunction('check_form_permissions', [$this, 'checkFormPermissions']),
new TwigFunction('get_content_by_machine_name', [$this, 'getContentByMachineName']),
new TwigFunction('twig_file_exists', [$this, 'twigFileExists']),
new TwigFunction('eval', [$this, 'evalFunction']),
new TwigFunction('is_authenticated_in_studio', [$this, 'isAuthenticatedInStudio']),
new TwigFunction('get_studio_user', [$this, 'getStudioUser']),
new TwigFunction('time_elapsed', [$this, 'timeElapsed']),
new TwigFunction('format_event_range', [$this, 'formatEventRange']),
new TwigFunction('format_date_locale', [$this, 'formatDateLocale']),
];
}
public function getFilters(): array
{
return [
new TwigFilter('urldecode', 'urldecode'),
new TwigFilter('format_date', [$this,'formatDate']),
new TwigFilter('array_filter', 'array_filter'),
new TwigFilter('array_unique', 'array_unique'),
new TwigFilter('json_decode', 'json_decode'),
new TwigFilter('extension', [$this, 'extension']),
new TwigFilter('get_variables', [$this, 'getVariables']),
new TwigFilter('get_pdf_cover', [$this, 'getPDFCover']),
new TwigFilter('html_entity_decode', 'html_entity_decode'),
new TwigFilter('eval', [$this, 'evalFilter']),
new TwigFilter('to_lower', [$this, 'toLower']),
new TwigFilter('pad', [$this, 'pad']),
new TwigFilter('fix_iframes', [$this, 'fixIframes'], ['is_safe' => ['html']]),
new TwigFilter('encode_url_path', [$this, 'encodeUrlPath']),
];
}
public function getTests(): array
{
return [
new TwigTest('ondisk',[$this,'onDisk']),
new TwigTest('instanceof', array($this, 'isInstanceOf'))
];
}
public function onDisk($file){
return file_exists($this->parameterBag->get('kernel.project_dir')."/".getenv("PUBLIC_DIR").$file);
}
public function isInstanceOf($object, $class)
{
$reflectionClass = new \ReflectionClass($class);
return $reflectionClass->isInstance($object);
}
public function getUrlContent($url)
{
return Functions::getUrlContent($url);
}
public function isMobile()
{
$useragent=$_SERVER['HTTP_USER_AGENT'];
return preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i',$useragent)||preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i',substr($useragent,0,4));
}
public function trimText($text,$trimLength,$textToAdd=""){
$return = html_entity_decode($text, ENT_COMPAT, 'utf-8');
$return = substr($return,0,$trimLength).$textToAdd;
return $return;
}
public function cleanString($input, $replace = '-', $remove_words = false, $words_array = array())
{
$return = htmlentities($input, ENT_COMPAT, 'utf-8');
$return = preg_replace("`&([a-z])(acute|uml|circ|grave|ring|cedil|slash|tilde|caron|lig|quot|rsquo);`i", "\\1", $return);
$return = trim(preg_replace('/(&)/', '', strtolower($return)));
$return = trim(preg_replace('/[^a-zA-Z0-9\s\-]/', '', strtolower($return)));
$return = trim(preg_replace('/ +/', ' ', $return));
if ($remove_words) {
$return = remove_words($return, $replace, $words_array);
}
$return = str_replace(' ', $replace, $return);
$return = trim(preg_replace('/' . $replace . '+/', $replace, $return));
return $return;
}
public function getConfig($machineName, $lang = '')
{
return Functions::getConfig($machineName,$lang,$this->entityManager);
}
public function extension($file)
{
return pathinfo($file, PATHINFO_EXTENSION);
}
public function formatDate($str){
return date("d/m/Y h:i A",time($str));
}
public function generateBarcode($type,$code,$width,$height,$color='black',$bgcolor='white',$responsetype='html')
{
$barcode = new \Com\Tecnick\Barcode\Barcode();
if($type == "EAN13"){
//Calcula o CheckDigit
$digits =(string)$code;
// Deprecated $even_sum = $digits{1} + $digits{3} + $digits{5} + $digits{7} + $digits{9} + $digits{11};
$even_sum = substr($digits, 1, 1) + substr($digits, 3, 1) + substr($digits, 5, 1) + substr($digits, 7, 1) + substr($digits, 9, 1) + substr($digits, 11, 1);
$even_sum_three = $even_sum * 3;
// Deprecated $odd_sum = $digits{0} + $digits{2} + $digits{4} + $digits{6} + $digits{8} + $digits{10};
$odd_sum = substr($digits, 0, 1) + substr($digits, 2, 1) + substr($digits, 4, 1) + substr($digits, 6, 1) + substr($digits, 8, 1) + substr($digits, 10, 1);
$total_sum = $even_sum_three + $odd_sum;
$next_ten = (ceil($total_sum/10))*10;
$check_digit = $next_ten - $total_sum;
$code = substr($code,0,12) . $check_digit;
}
$bobj = $barcode->getBarcodeObj($type, $code, $width, $height, $color,array(0,0,0,0))->setBackgroundColor($bgcolor);
if($responsetype == "svg")
return $bobj->getSvgCode();
else
return $bobj->getHtmlDiv();
}
public function getVariables($variable) {
return getenv($variable);
}
public function getPDFCover($pdffile){
$pdffile = urldecode($pdffile);
if($this->onDisk($pdffile)){
$filesignature=md5($pdffile).".jpg";
$filepath=$this->parameterBag->get('kernel.project_dir')."/public".$pdffile;
$cachepath=$this->parameterBag->get('kernel.project_dir')."/public/media/".$filesignature;
if(!$this->onDisk("/media/".$filesignature) || filemtime($filepath)>filemtime($cachepath)){
try {
$image = new \Imagick();
$image->setResolution(200,200);
$image->SetColorspace(\Imagick::COLORSPACE_SRGB);
$image->readImage($filepath."[0]");
$image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE );
$image->setImageFormat('jpg');
$image->writeImage($cachepath);
$image->clear();
$image->destroy();
return "/media/".$filesignature;
} catch (\Throwable $th) {
}
}
else
{
return "/media/".$filesignature;
}
}
$default="/media/default.jpg";
if(!$this->onDisk($default)){
$image = imagecreate(1, 1);
$background_color = imagecolorallocate($image, 255, 255, 255);
imagefill($image, 0, 0, $background_color);
imagejpeg($image, $this->parameterBag->get('kernel.project_dir')."/public".$default);
}
return $default;
}
public function evalFilter($value){
//create twig string from value
$template=$this->twig->createTemplate($value);
return $template->render(array());
}
public function evalFunction($value,$params){
//create twig string from value
$template=$this->twig->createTemplate($value);
return $template->render($params);
}
public function getPageInfo($locale, $id, $renderCat = true, $rootCategory = null)
{
// Use Redis-cached version for better performance
return $this->pageInfoCache->getPageInfo($locale, $id, $renderCat, $rootCategory);
}
public function getDomainValueDescription($domainMachineName,$machineName,string $locale):?string
{
return Functions::getDomainValueDescription($locale,$domainMachineName,$machineName, $this->entityManager);
}
/**
* Encodes URL path segments while preserving slashes.
*/
public function encodeUrlPath(?string $url): string
{
if (empty($url)) {
return '';
}
// Parse the URL to get its components
$parsed = parse_url($url);
if (isset($parsed['path'])) {
// Split path by slash, encode each segment, then rejoin
$segments = explode('/', $parsed['path']);
$encodedSegments = array_map(function($segment) {
return rawurlencode(rawurldecode($segment));
}, $segments);
$parsed['path'] = implode('/', $encodedSegments);
}
// Rebuild the URL
$result = '';
if (isset($parsed['scheme'])) {
$result .= $parsed['scheme'] . '://';
}
if (isset($parsed['host'])) {
$result .= $parsed['host'];
}
if (isset($parsed['port'])) {
$result .= ':' . $parsed['port'];
}
if (isset($parsed['path'])) {
$result .= $parsed['path'];
}
if (isset($parsed['query'])) {
$result .= '?' . $parsed['query'];
}
if (isset($parsed['fragment'])) {
$result .= '#' . $parsed['fragment'];
}
return $result;
}
public function getImageSize(string $filename, array &$image_info = null)
{
$filename = substr(parse_url($filename, PHP_URL_PATH), 1);
if(file_exists($filename)){
return getimagesize($filename, $image_info);
}
return [0,0];
}
public function getDomainValue($domainMachineName,$machineName):?StdDomainsValues
{
return $this->entityManager->getRepository(StdDomainsValues::class)->findOneByDomainAndValue($domainMachineName,$machineName);
}
public function getRelationChildren($relationId,$languagecode, $params)
{
return $this->entityManager->getRepository(StdPagesPages::class)->getRelationChildren($relationId, $languagecode,$params);
}
public function checkFormPermissions($form_content)
{
return Functions::checkFormPermissions( $this->security, $this->entityManager, $form_content);
}
public function toLower($str)
{
if (is_array($str)){
array_walk($str, function(&$item){$item = strtolower($item);});
}
else{
$str = strtolower($str);
}
return $str;
}
public function fileGetContents($filepath){
if($this->onDisk($filepath)){
return file_get_contents($this->parameterBag->get('kernel.project_dir')."/".getenv("PUBLIC_DIR").$filepath);
}else{
return "";
}
}
public function getContentByMachineName($locale, $machineName)
{
return Functions::getContentByMachineName($locale, $machineName, $this->entityManager);
}
public function twigFileExists($path)
{
$loader = $this->twig->getLoader();
return $loader->exists($path);
}
public function pad($number, $length, $padString = '0', $side = 'left')
{
$padType = $side === 'left' ? STR_PAD_LEFT : STR_PAD_RIGHT;
return str_pad($number, $length, $padString, $padType);
}
public function isAuthenticatedInStudio(): bool
{
$studioToken = $this->requestStack->getSession()->get('_security_studio_context');
if (!$studioToken) {
return false;
}
// If token exists in session, check if it's valid
try {
$token = unserialize($studioToken);
return $token && $token->isAuthenticated();
} catch (\Exception $e) {
return false;
}
}
public function getStudioUser()
{
$studioToken = $this->requestStack->getSession()->get('_security_studio_context');
if (!$studioToken) {
return null;
}
// If token exists in session, check if it's valid
try {
$token = unserialize($studioToken);
return $token->getUser();
} catch (\Exception $e) {
return null;
}
}
/**
* Calculate time elapsed since a given date using translations
*
* @param mixed $publishedDate The published date (string, DateTime, or null)
* @return string The formatted time elapsed string
*/
public function timeElapsed($publishedDate): string
{
if (!$publishedDate) {
return $this->translator->trans('time_recently', [], 'custom');
}
try {
// Convert to DateTime if it's a string
if (is_string($publishedDate)) {
$published = new \DateTime($publishedDate);
} elseif ($publishedDate instanceof \DateTimeInterface) {
$published = $publishedDate;
} else {
return $this->translator->trans('time_recently', [], 'custom');
}
$now = new \DateTime();
$diff = $now->getTimestamp() - $published->getTimestamp();
// Calculate time units
$minutes = round($diff / 60);
$hours = round($diff / 3600);
$days = round($diff / 86400);
$weeks = round($diff / 604800);
$months = round($diff / 2592000); // Approximate month (30 days)
$years = round($diff / 31536000); // Approximate year (365 days)
// Return appropriate format using translations
if ($diff < 60) {
return $this->translator->trans('time_recently', [], 'custom');
} elseif ($minutes < 60) {
return $this->translator->trans('time_minutes_ago', ['%count%' => $minutes], 'custom');
} elseif ($hours < 24) {
return $this->translator->trans('time_hours_ago', ['%count%' => $hours], 'custom');
} elseif ($days < 7) {
return $this->translator->trans('time_days_ago', ['%count%' => $days], 'custom');
} elseif ($weeks < 4) {
return $this->translator->trans('time_weeks_ago', ['%count%' => $weeks], 'custom');
} elseif ($months < 12) {
return $this->translator->trans('time_months_ago', ['%count%' => $months], 'custom');
} else {
return $this->translator->trans('time_years_ago', ['%count%' => $years], 'custom');
}
} catch (\Exception $e) {
return $this->translator->trans('time_recently', [], 'custom');
}
}
// 2) Mapa de meses por idioma
private function monthsByLocale(string $locale): array
{
$map = [
'pt' => [1=>'janeiro',2=>'fevereiro',3=>'março',4=>'abril',5=>'maio',6=>'junho',7=>'julho',8=>'agosto',9=>'setembro',10=>'outubro',11=>'novembro',12=>'dezembro'],
'en' => [1=>'January',2=>'February',3=>'March',4=>'April',5=>'May',6=>'June',7=>'July',8=>'August',9=>'September',10=>'October',11=>'November',12=>'December'],
'es' => [1=>'enero',2=>'febrero',3=>'marzo',4=>'abril',5=>'mayo',6=>'junio',7=>'julio',8=>'agosto',9=>'septiembre',10=>'octubre',11=>'noviembre',12=>'diciembre'],
];
// normaliza ('pt-PT' -> 'pt')
$short = strtolower(substr($locale, 0, 2));
return $map[$short] ?? $map['pt'];
}
/**
* Converte para DateTime de forma segura
*/
private function toDateTime($value): ?\DateTimeInterface
{
if ($value instanceof \DateTimeInterface) return $value;
if (is_string($value) && trim($value) !== '') {
try { return new \DateTime($value); } catch (\Throwable $e) {}
}
return null;
}
/**
* Formata uma data simples no idioma atual.
* Ex.: "14 de agosto de 2025" (pt) | "August 14, 2025" (en)
*/
public function formatDateLocale($date, ?string $locale = null): string
{
$dt = $this->toDateTime($date);
if (!$dt) return '';
$loc = $locale ?: ($this->requestStack->getCurrentRequest()->getLocale() ?? 'pt');
$months = $this->monthsByLocale($loc);
$d = $dt->format('d');
$m = (int)$dt->format('n');
$y = $dt->format('Y');
$short = strtolower(substr($loc, 0, 2));
if ($short === 'en') {
// English style
return sprintf('%s %s, %s', $months[$m], $d, $y);
} elseif ($short === 'es') {
return sprintf('%s de %s de %s', $d, $months[$m], $y);
}
// default pt
return sprintf('%s de %s %s', $d, $months[$m], $y);
}
/**
* Formata o intervalo start/end no idioma atual.
* Regras:
* - anos diferentes: "dd de Mês AAAA a dd de Mês AAAA"
* - mesmo ano e mês: "dd a dd de Mês AAAA"
* - mesmo ano, meses diferentes: "dd de Mês a dd de Mês AAAA"
* - sem end: data única completa
*/
public function formatEventRange($start, $end = null, ?string $locale = null): string
{
$s = $this->toDateTime($start);
if (!$s) return '';
$loc = $locale ?: ($this->requestStack->getCurrentRequest()->getLocale() ?? 'pt');
$months = $this->monthsByLocale($loc);
$y1 = (int)$s->format('Y');
$m1 = (int)$s->format('n');
$d1 = $s->format('d');
$e = $this->toDateTime($end);
if (!$e) {
return $this->formatDateLocale($s, $loc);
}
$y2 = (int)$e->format('Y');
$m2 = (int)$e->format('n');
$d2 = $e->format('d');
$short = strtolower(substr($loc, 0, 2));
// anos diferentes
if ($y1 !== $y2) {
if ($short === 'en') {
return sprintf('%s %s, %s to %s %s, %s', $months[$m1], $d1, $y1, $months[$m2], $d2, $y2);
} elseif ($short === 'es') {
return sprintf('%s de %s de %s a %s de %s de %s', $d1, $months[$m1], $y1, $d2, $months[$m2], $y2);
}
// pt
return sprintf('%s de %s %s a %s de %s %s', $d1, $months[$m1], $y1, $d2, $months[$m2], $y2);
}
// mesmo ano
if ($m1 === $m2) {
if ($short === 'en') {
return sprintf('%s–%s %s %s', $d1, $d2, $months[$m2], $y2);
} elseif ($short === 'es') {
return sprintf('%s a %s de %s de %s', $d1, $d2, $months[$m2], $y2);
}
// pt
return sprintf('%s a %s de %s %s', $d1, $d2, $months[$m2], $y2);
}
// mesmo ano, meses diferentes
if ($short === 'en') {
return sprintf('%s %s to %s %s, %s', $months[$m1].' '.$d1, $y1, $months[$m2].' '.$d2, $months[$m2], $y2); // forma enxuta
} elseif ($short === 'es') {
return sprintf('%s de %s a %s de %s de %s', $d1, $months[$m1], $d2, $months[$m2], $y2);
}
// pt
return sprintf('%s de %s a %s de %s %s', $d1, $months[$m1], $d2, $months[$m2], $y2);
}
}