Spaces:
No application file
No application file
| namespace Mautic\CoreBundle\Twig\Helper; | |
| use Mautic\CoreBundle\Helper\AssetGenerationHelper; | |
| use Mautic\CoreBundle\Helper\CoreParametersHelper; | |
| use Mautic\CoreBundle\Helper\InputHelper; | |
| use Mautic\CoreBundle\Helper\PathsHelper; | |
| use Mautic\InstallBundle\Install\InstallService; | |
| use Mautic\IntegrationsBundle\Exception\IntegrationNotFoundException; | |
| use Mautic\IntegrationsBundle\Helper\BuilderIntegrationsHelper; | |
| use Symfony\Component\Asset\Packages; | |
| final class AssetsHelper | |
| { | |
| /** | |
| * Used for Mautic app. | |
| */ | |
| public const CONTEXT_APP = 'app'; | |
| /** | |
| * Used within the content iframe when building content with a theme. | |
| */ | |
| public const CONTEXT_BUILDER = 'builder'; | |
| private ?AssetGenerationHelper $assetHelper = null; | |
| private string $context = self::CONTEXT_APP; | |
| /** | |
| * @var array<mixed, mixed> | |
| */ | |
| 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<string, string> $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<string, string> $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 <head />. | |
| * | |
| * @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 .= '<link rel="stylesheet" href="'.$this->getUrl($s).'" data-source="mautic" />'."\n"; | |
| } | |
| } | |
| if (isset($this->assets[$this->context]['styleDeclarations'])) { | |
| $styles .= "<style data-source=\"mautic\">\n"; | |
| foreach (array_reverse($this->assets[$this->context]['styleDeclarations']) as $d) { | |
| $styles .= "$d\n"; | |
| } | |
| $styles .= "</style>\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 '<script src="'.$this->getUrl($script).'"'.($async ? ' async' : '').' data-source="mautic"></script>'."\n"; | |
| } | |
| } | |
| if (isset($this->assets[$this->context]['scriptDeclarations'][$location])) { | |
| echo "<script data-source=\"mautic\">\n"; | |
| foreach (array_reverse($this->assets[$this->context]['scriptDeclarations'][$location]) as $d) { | |
| echo "$d\n"; | |
| } | |
| echo "</script>\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</script>"; | |
| $scriptOpen = false; | |
| } | |
| [$script, $async] = $output; | |
| $headOutput .= "\n".'<script src="'.$this->getUrl($script).'"'.($async ? ' async' : '').' data-source="mautic"></script>'; | |
| break; | |
| case 'custom': | |
| case 'declaration': | |
| if ('custom' == $type && $scriptOpen) { | |
| $headOutput .= "\n</script>"; | |
| $scriptOpen = false; | |
| } elseif ('declaration' == $type && !$scriptOpen) { | |
| $headOutput .= "\n<script data-source=\"mautic\">"; | |
| $scriptOpen = true; | |
| } | |
| $headOutput .= "\n$output"; | |
| break; | |
| } | |
| } | |
| if ($scriptOpen) { | |
| $headOutput .= "\n</script>\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 '<link rel="stylesheet" href="'.$this->getUrl($url).'" data-source="mautic" />'."\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 '<script src="'.$this->getUrl($url).'" data-source="mautic"></script>'."\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 '<script>Mautic.getActiveBuilderName = function() { return \''.$builderName.'\'; }</script>'."\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, string>|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 .= '<script src="'.$this->getUrl($url).'" data-source="mautic"></script>'."\n"; | |
| } | |
| } | |
| return $js; | |
| } | |
| return $assets['js']; | |
| } | |
| /** | |
| * Load CKEditor JS source files. | |
| * | |
| * @return array<string> | |
| */ | |
| 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 '<script async="async" type="text/javascript" data-source="mautic">Mautic.loadScript(\''.$this->getUrl($assetFilePath)."', '$onLoadCallback', '$alreadyLoadedCallback');</script>"; | |
| } | |
| /** | |
| * 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 '<script async="async" type="text/javascript" data-source="mautic">Mautic.loadStylesheet(\''.$this->getUrl($assetFilePath).'\');</script>'; | |
| } | |
| /** | |
| * Turn all URLs in clickable links. | |
| * | |
| * @param string $text | |
| * @param array<string> $protocols http/https, ftp, mail, twitter | |
| * @param array<string, string> $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('~(<a .*?>.*?</a>|<.*?>)~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<]+))(?<![\.,:])~i', function ($match) use ($protocol, &$links, $attr): string { | |
| if ($match[1]) { | |
| $protocol = $match[1]; | |
| } | |
| $link = $this->escape($match[2] ?: $match[3]); | |
| return '<'.array_push($links, "<a $attr href=\"$protocol://$link\">$link</a>").'>'; | |
| }, $text), | |
| 'mail' => preg_replace_callback('~([^\s<]+?@[^\s<]+?\.[^\s<]+)(?<![\.,:])~', function ($match) use (&$links, $attr): string { | |
| $match[1] = $this->escape($match[1]); | |
| return '<'.array_push($links, "<a $attr href=\"mailto:{$match[1]}\">{$match[1]}</a>").'>'; | |
| }, $text), | |
| 'twitter' => preg_replace_callback('~(?<!\w)[@#](\w++)~', function ($match) use (&$links, $attr): string { | |
| $match[0] = $this->escape($match[0]); | |
| $match[1] = $this->escape($match[1]); | |
| return '<'.array_push($links, "<a $attr href=\"https://twitter.com/".('@' == $match[0][0] ? '' : 'search/%23').$match[1]."\">{$match[0]}</a>").'>'; | |
| }, $text), | |
| default => preg_replace_callback('~'.preg_quote($protocol, '~').'://([^\s<]+?)(?<![\.,:])~i', function ($match) use ($protocol, &$links, $attr): string { | |
| $match[1] = $this->escape($match[1]); | |
| return '<'.array_push($links, "<a $attr href=\"$protocol://{$match[1]}\">{$match[1]}</a>").'>'; | |
| }, $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 '<img src="'.$flagImg.'" class="'.$class.'" />'; | |
| } | |
| } | |
| /** | |
| * 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; | |
| } | |
| } | |