*/ private $assets = [ self::CONTEXT_APP => [], ]; private ?string $version = null; /** * @var string */ private $siteUrl; private ?PathsHelper $pathsHelper = null; private BuilderIntegrationsHelper $builderIntegrationsHelper; private InstallService $installService; public function __construct( private Packages $packages, private CoreParametersHelper $coreParametersHelper ) { } /** * Gets asset prefix. * * @param bool $includeEndingSlash * * @return string */ public function getAssetPrefix($includeEndingSlash = false) { $prefix = $this->pathsHelper->getSystemPath('asset_prefix'); if (!empty($prefix)) { if ($includeEndingSlash && !str_ends_with($prefix, '/')) { $prefix .= '/'; } elseif (!$includeEndingSlash && str_ends_with($prefix, '/')) { $prefix = substr($prefix, 0, -1); } } return $prefix; } public function getImagesPath(bool $absolute = false): string { return $this->pathsHelper->getSystemPath('images', $absolute); } /** * Returns the path to an asset that may be overridden in the media folder. * * This function is meant for assets that may be overridden in the media folder. * This could be logo's, country flags, ... * So to be able to override an asset, it has to exist in the assets folder. * * @param string $path * @param bool|false $absolute * * @return string|bool */ public function getOverridableUrl($path, $absolute = false) { $mediaPath = $this->pathsHelper->getSystemPath('media', false); $assetsPath = $this->pathsHelper->getSystemPath('assets', false); if (!file_exists($this->pathsHelper->getAssetsPath().DIRECTORY_SEPARATOR.$path)) { return false; } if (file_exists($this->pathsHelper->getMediaPath().DIRECTORY_SEPARATOR.$path)) { $path = $mediaPath.DIRECTORY_SEPARATOR.$path; } else { $path = $assetsPath.DIRECTORY_SEPARATOR.$path; } return $this->getUrl($path, null, null, $absolute); } /** * Set asset url path. * * @param string $path * @param string|null $packageName * @param string|null $version * @param bool|false $absolute * @param bool|false $ignorePrefix * * @return string */ public function getUrl($path, $packageName = null, $version = null, $absolute = false, $ignorePrefix = false) { // if we have http in the url it is absolute and we can just return it if (str_starts_with($path, 'http')) { return $path; } // otherwise build the complete path if (!$ignorePrefix) { $assetPrefix = $this->getAssetPrefix(!str_starts_with($path, '/')); $path = $assetPrefix.$path; } $path = $this->appendVersion($path, $version); $url = $this->packages->getUrl($path, $packageName); if ($absolute) { $url = $this->getBaseUrl().'/'.$path; } return $url; } /** * Get base URL. * * @return string */ public function getBaseUrl() { return $this->siteUrl; } /** * Define the context for which the assets will be injected and/or retrieved. * * If changing the context from app, it's important to reset the context back to app after * injecting/fetching assets for a different context. * * @param string $context * * @return $this */ public function setContext($context = self::CONTEXT_APP) { $this->context = $context; if (!isset($this->assets[$context])) { $this->assets[$context] = []; } return $this; } /** * Adds a JS script to the template. * * @param string|array $script * @param string $location * @param bool $async * @param string $name * * @return $this */ public function addScript($script, $location = 'head', $async = false, $name = null) { $assets = &$this->assets[$this->context]; $addScripts = function ($s) use ($location, &$assets, $async, $name): void { $name = $name ?: 'script_'.hash('sha1', uniqid((string) mt_rand())); if ('head' == $location) { // special place for these so that declarations and scripts can be mingled $assets['headDeclarations'][$name] = ['script' => [$s, $async]]; } else { if (!isset($assets['scripts'][$location])) { $assets['scripts'][$location] = []; } if (!in_array($s, $assets['scripts'][$location])) { $assets['scripts'][$location][$name] = [$s, $async]; } } }; if (is_array($script)) { foreach ($script as $s) { $addScripts($s); } } else { $addScripts($script); } return $this; } /** * Adds JS script declarations to the template. * * @param string $script * @param string $location * * @return $this */ public function addScriptDeclaration($script, $location = 'head') { if ('head' == $location) { // special place for these so that declarations and scripts can be mingled $this->assets[$this->context]['headDeclarations'][] = ['declaration' => $script]; } else { if (!isset($this->assets[$this->context]['scriptDeclarations'][$location])) { $this->assets[$this->context]['scriptDeclarations'][$location] = []; } if (!in_array($script, $this->assets[$this->context]['scriptDeclarations'][$location])) { $this->assets[$this->context]['scriptDeclarations'][$location][] = $script; } } return $this; } /** * Adds a stylesheet to be loaded in the template header. * * @param string|array $stylesheet * * @return $this */ public function addStylesheet($stylesheet) { $addSheet = function ($s): void { if (!isset($this->assets[$this->context]['stylesheets'])) { $this->assets[$this->context]['stylesheets'] = []; } if (!in_array($s, $this->assets[$this->context]['stylesheets'])) { $this->assets[$this->context]['stylesheets'][] = $s; } }; if (is_array($stylesheet)) { foreach ($stylesheet as $s) { $addSheet($s); } } else { $addSheet($stylesheet); } return $this; } /** * Add style tag to the header. * * @param string $styles * * @return $this */ public function addStyleDeclaration($styles) { if (!isset($this->assets[$this->context]['styleDeclarations'])) { $this->assets[$this->context]['styleDeclarations'] = []; } if (!in_array($styles, $this->assets[$this->context]['styleDeclarations'])) { $this->assets[$this->context]['styleDeclarations'][] = $styles; } return $this; } /** * Adds a custom declaration to . * * @param string $declaration * @param string $location * * @return $this */ public function addCustomDeclaration($declaration, $location = 'head') { if ('head' == $location) { $this->assets[$this->context]['headDeclarations'][] = ['custom' => $declaration]; } else { if (!isset($this->assets[$this->context]['customDeclarations'][$location])) { $this->assets[$this->context]['customDeclarations'][$location] = []; } if (!in_array($declaration, $this->assets[$this->context]['customDeclarations'][$location])) { $this->assets[$this->context]['customDeclarations'][$location][] = $declaration; } } return $this; } /** * Outputs the stylesheets and style declarations. */ public function outputStyles(): void { echo $this->getStyles(); } /** * Outputs the stylesheets and style declarations. */ public function getStyles(): string { $styles = ''; if (isset($this->assets[$this->context]['stylesheets'])) { foreach (array_reverse($this->assets[$this->context]['stylesheets']) as $s) { $styles .= ''."\n"; } } if (isset($this->assets[$this->context]['styleDeclarations'])) { $styles .= "\n"; } return $styles; } /** * Outputs the script files and declarations. * * @param string $location */ public function outputScripts($location): void { if (isset($this->assets[$this->context]['scripts'][$location])) { foreach (array_reverse($this->assets[$this->context]['scripts'][$location]) as $s) { [$script, $async] = $s; echo ''."\n"; } } if (isset($this->assets[$this->context]['scriptDeclarations'][$location])) { echo "\n"; } if (isset($this->assets[$this->context]['customDeclarations'][$location])) { foreach (array_reverse($this->assets[$this->context]['customDeclarations'][$location]) as $d) { echo "$d\n"; } } } /** * Output head scripts, stylesheets, and custom declarations. */ public function outputHeadDeclarations(): void { echo $this->getHeadDeclarations(); } /** * Returns head scripts, stylesheets, and custom declarations. */ public function getHeadDeclarations(): string { $headOutput = $this->getStyles(); if (!empty($this->assets[$this->context]['headDeclarations'])) { $scriptOpen = false; foreach ($this->assets[$this->context]['headDeclarations'] as $declaration) { $type = key($declaration); $output = $declaration[$type]; switch ($type) { case 'script': if ($scriptOpen) { $headOutput .= "\n"; $scriptOpen = false; } [$script, $async] = $output; $headOutput .= "\n".''; break; case 'custom': case 'declaration': if ('custom' == $type && $scriptOpen) { $headOutput .= "\n"; $scriptOpen = false; } elseif ('declaration' == $type && !$scriptOpen) { $headOutput .= "\n\n\n"; } } return $headOutput; } /** * Output system stylesheets. */ public function outputSystemStylesheets(): void { $assets = $this->assetHelper->getAssets(); if (isset($assets['css'])) { foreach ($assets['css'] as $url) { echo ''."\n"; } } } /** * Output system scripts. * * @param bool|false $includeEditor */ public function outputSystemScripts($includeEditor = false): void { $assets = $this->assetHelper->getAssets(); if ($includeEditor) { $assets['js'] = array_merge($assets['js'], $this->getFroalaScripts(), $this->getCKEditorScripts()); } if (isset($assets['js'])) { foreach ($assets['js'] as $url) { echo ''."\n"; } } if ($this->installService->checkIfInstalled()) { /** * We want to enable JS consumers to simply query Mautic.getActiveBuilderName() so they can add logic based on the active builder. * The $builderName variable is passed to the template so we can get that info on the JS-side. */ try { $builder = $this->builderIntegrationsHelper->getBuilder('email'); $builderName = $builder->getName(); } catch (IntegrationNotFoundException) { // Assume legacy builder $builderName = 'legacy'; } echo ''."\n"; } } /** * Fetch system scripts. * * @param bool $render If true, a string will be returned of rendered script for header * @param bool $includeEditor * * @return array|string */ public function getSystemScripts($render = false, $includeEditor = false) { $assets = $this->assetHelper->getAssets(); if ($includeEditor) { $assets['js'] = array_merge($assets['js'], $this->getFroalaScripts(), $this->getCKEditorScripts()); } if ($render) { $js = ''; if (isset($assets['js'])) { foreach ($assets['js'] as $url) { $js .= ''."\n"; } } return $js; } return $assets['js']; } /** * Load CKEditor JS source files. * * @return array */ private function getCKEditorScripts(): array { $base = 'media/libraries/ckeditor/'; return [ $base.'ckeditor.js?v'.$this->version, ]; } /** * Load Froala JS source files. * * @return string[] */ public function getFroalaScripts(): array { if (!$this->coreParametersHelper->get('load_froala_assets')) { return []; } $base = 'app/bundles/CoreBundle/Assets/js/libraries/froala/'; $plugins = $base.'plugins/'; return [ $base.'froala_editor.js?v'.$this->version, $plugins.'align.js?v'.$this->version, $plugins.'code_beautifier.js?v'.$this->version, $plugins.'code_view.js?v'.$this->version, $plugins.'colors.js?v'.$this->version, // $plugins . 'file.js?v' . $this->version, // @todo $plugins.'font_family.js?v'.$this->version, $plugins.'font_size.js?v'.$this->version, $plugins.'fullscreen.js?v'.$this->version, $plugins.'image.js?v'.$this->version, // $plugins . 'image_manager.js?v' . $this->version, $plugins.'filemanager.js?v'.$this->version, $plugins.'inline_style.js?v'.$this->version, $plugins.'line_breaker.js?v'.$this->version, $plugins.'link.js?v'.$this->version, $plugins.'lists.js?v'.$this->version, $plugins.'paragraph_format.js?v'.$this->version, $plugins.'paragraph_style.js?v'.$this->version, $plugins.'quick_insert.js?v'.$this->version, $plugins.'quote.js?v'.$this->version, $plugins.'table.js?v'.$this->version, $plugins.'url.js?v'.$this->version, // $plugins . 'video.js?v' . $this->version, $plugins.'gatedvideo.js?v'.$this->version, $plugins.'token.js?v'.$this->version, $plugins.'dynamic_content.js?v'.$this->version, ]; } /** * Loads an addon script. * * @param string $assetFilePath The path to the file location. Can use full path or relative to mautic web root * @param string $onLoadCallback Mautic namespaced function to call for the script onload * @param string $alreadyLoadedCallback Mautic namespaced function to call if the script has already been loaded */ public function includeScript($assetFilePath, $onLoadCallback = '', $alreadyLoadedCallback = ''): string { return '"; } /** * Include stylesheet. * * @param string $assetFilePath the path to the file location. Can use full path or relative to mautic web root */ public function includeStylesheet($assetFilePath): string { return ''; } /** * Turn all URLs in clickable links. * * @param string $text * @param array $protocols http/https, ftp, mail, twitter * @param array $attributes * * @return string|string[]|null */ public function makeLinks($text, $protocols = ['http', 'mail'], array $attributes = []): string|array|null { // clear tags in text $text = InputHelper::url($text, false, $protocols); // Link attributes $attr = ''; foreach ($attributes as $key => $val) { $attr = ' '.$key.'="'.htmlentities($val).'"'; } $links = []; // Extract existing links and tags $text = preg_replace_callback('~(.*?|<.*?>)~i', function ($match) use (&$links): string { return '<'.array_push($links, $match[1]).'>'; }, $text); // Extract text links for each protocol foreach ((array) $protocols as $protocol) { $text = match ($protocol) { 'http', 'https' => preg_replace_callback('~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?escape($match[2] ?: $match[3]); return '<'.array_push($links, "$link").'>'; }, $text), 'mail' => preg_replace_callback('~([^\s<]+?@[^\s<]+?\.[^\s<]+)(?escape($match[1]); return '<'.array_push($links, "{$match[1]}").'>'; }, $text), 'twitter' => preg_replace_callback('~(?escape($match[0]); $match[1] = $this->escape($match[1]); return '<'.array_push($links, "{$match[0]}").'>'; }, $text), default => preg_replace_callback('~'.preg_quote($protocol, '~').'://([^\s<]+?)(?escape($match[1]); return '<'.array_push($links, "{$match[1]}").'>'; }, $text), }; } // Insert all link return preg_replace_callback('/<(\d+)>/', function ($match) use (&$links): string { return $links[(int) $match[1] - 1]; }, $text); } /** * Returns only first $charCount chars of the $text and adds "..." if it is shortened. * * @param string $text * @param int $charCount * * @return string */ public function shortenText($text, $charCount = null) { if ($charCount && strlen($text) > $charCount) { return mb_substr($text, 0, $charCount, 'utf-8').'...'; } return $text; } /** * @param string $country * @param bool|true $urlOnly * @param string $class * * @return string */ public function getCountryFlag($country, $urlOnly = true, $class = '') { $country = ucwords(str_replace(' ', '-', $country)); $flagImg = (string) $this->getOverridableUrl('images/flags/'.$country.'.png'); if ($urlOnly) { return $flagImg; } else { return ''; } } /** * Clear all the assets. */ public function clear(): void { $this->assets = []; } public function getName(): string { return 'assets'; } public function setAssetHelper(AssetGenerationHelper $helper): void { $this->assetHelper = $helper; } /** * @param ?string $siteUrl can be null on installation */ public function setSiteUrl($siteUrl): void { if ($siteUrl && str_ends_with($siteUrl, '/')) { $siteUrl = substr($siteUrl, 0, -1); } $this->siteUrl = $siteUrl; } public function setPathsHelper(PathsHelper $pathsHelper): void { $this->pathsHelper = $pathsHelper; } /** * @param string $secretKey * @param string|int $version */ public function setVersion($secretKey, $version): void { $this->version = substr(hash('sha1', $secretKey.$version), 0, 8); } public function setBuilderIntegrationsHelper(BuilderIntegrationsHelper $builderIntegrationsHelper): void { $this->builderIntegrationsHelper = $builderIntegrationsHelper; } public function setInstallService(InstallService $installService): void { $this->installService = $installService; } private function escape(string $string): string { return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false); } /** * Appends the version to the path if is not present. */ private function appendVersion(string $path, string $version = null): string { $version = $version ?: $this->version; if (!$version) { // no version is set return $path; } $versionArgument = 'v'.$version; $querySeparator = '?'; $argumentSeparator = '&'; $query = explode($querySeparator, $path)[1] ?? ''; parse_str(str_replace($argumentSeparator, '&', $query), $arguments); if (isset($arguments[$versionArgument])) { // path already contains the version return $path; } return rtrim($path, $querySeparator).($query ? $argumentSeparator : $querySeparator).$versionArgument; } }