summaryrefslogtreecommitdiff
path: root/app/frontend/web/ClientScriptManager.php
diff options
context:
space:
mode:
Diffstat (limited to 'app/frontend/web/ClientScriptManager.php')
-rw-r--r--app/frontend/web/ClientScriptManager.php572
1 files changed, 572 insertions, 0 deletions
diff --git a/app/frontend/web/ClientScriptManager.php b/app/frontend/web/ClientScriptManager.php
new file mode 100644
index 0000000..223c6e2
--- /dev/null
+++ b/app/frontend/web/ClientScriptManager.php
@@ -0,0 +1,572 @@
+<?php
+
+Prado::using('Application.layouts.Layout');
+
+class ClientScriptManager extends TClientScriptManager {
+
+ private $_page;
+ public function __construct(TPage $page) {
+ $this->_page = $page;
+ parent::__construct($page);
+ }
+
+ // Base path of the entire application
+ private function _getBasePath() {
+ return Prado::getPathOfNamespace('Web');
+ }
+
+ // Translate URLs to filesystem paths, index return array by URLs
+ private function _getBasePaths(array $urls) {
+ $basePath = $this->_getBasePath();
+ return array_combine(
+ $urls,
+ array_map(
+ function($path) use($basePath) {
+ return $basePath . DIRECTORY_SEPARATOR . $path;
+ },
+ $urls
+ )
+ );
+ }
+
+ // Base cache path, suffixed with subdirectory, create on demand
+ private function _getCachePath(string $subdir = 'assets') {
+ $cachePath = $this->Application->RuntimePath
+ . DIRECTORY_SEPARATOR
+ . $subdir;
+ if (!is_dir($cachePath)) {
+ if (file_exists($cachePath)) {
+ throw new TIOException(
+ sprintf(
+ 'Client script manager cache path "%s" exists and is not a directory',
+ $cachePath
+ )
+ );
+ } else {
+ mkdir($cachePath);
+ }
+ }
+ return $cachePath;
+ }
+
+ // Cache path for a file of specified type
+ private function _getCacheFilePath($path, $type) {
+ return $this->_getCachePath($type)
+ . DIRECTORY_SEPARATOR
+ . $path;
+ }
+
+ // Cache key for specific file set, including current theme
+ private function _getFileCollectionCacheKey(array $files) {
+ sort($files);
+ if ($this->_page->Theme) {
+ $files[] = $this->_page->Theme->Name;
+ }
+ return md5(implode(PHP_EOL, $files));
+ }
+
+ // Last modification time of a file set
+ private function _getFileCollectionMTime(array $files) {
+ return max(array_map('filemtime', $files));
+ }
+
+ // Storage (application cache) key for list of rendered assets of specified type
+ // Rendered[ASSET_TYPE].[VIEW_ID] (VIEW_ID as rendered in Layout hidden field)
+ private function _getRenderedAssetsStoreKey($type) {
+ $template = $this->_page->Master;
+ if (!$template instanceof Layout) {
+ throw new TNotSupportedException(
+ 'Compiled assets may only be used within Layout master class controls'
+ );
+ }
+ return sprintf('Rendered%s.%s', $type, $template->generateViewID());
+ }
+
+ // Shorthand for JS assets cache key
+ private function _getRenderedScriptsStoreKey() {
+ return $this->_getRenderedAssetsStoreKey('Scripts');
+ }
+
+ // Shorthand for CSS assets cache key
+ private function _getRenderedSheetsStoreKey() {
+ return $this->_getRenderedAssetsStoreKey('Sheets');
+ }
+
+ // Application (primary) cache module, required to keep track of assets rendered on current page
+ private function _getCache() {
+ $cache = $this->Application->Cache;
+ if (!$cache) {
+ throw new TNotSupportedException(
+ 'Compiled assets require cache to be configured'
+ );
+ }
+ return $cache;
+ }
+
+ // Check cache file validity, comparing to source file set
+ private function _isCacheValid($cacheFile, array $paths) {
+ return file_exists($cacheFile)
+ && (filemtime($cacheFile) >= $this->_getFileCollectionMTime($paths));
+ }
+
+ // Determine whether specific URL points to a local asset (i.e. existing on the filesystem)
+ private function _isFileLocal($file) {
+ $basePath = $this->_getBasePath();
+ return file_exists($basePath . DIRECTORY_SEPARATOR . $file);
+ }
+
+ // Filter URL set to leave only local assets
+ private function _determineLocalFiles($files) {
+ return array_filter(
+ $files, [$this, '_isFileLocal']
+ );
+ }
+
+ // Scripts - internal methods
+
+ private $_renderedScriptsInitialized = FALSE;
+
+ // Retrieve scripts already rendered on current page from application cache,
+ // maintaining the state over callbacks
+ private function _getRenderedScripts() {
+ $sessionKey = $this->_getRenderedScriptsStoreKey();
+ if ($this->_page->IsCallBack || $this->_renderedScriptsInitialized) {
+ return $this->_getCache()->get($sessionKey) ?: [];
+ } else {
+ $this->_getCache()->delete($sessionKey);
+ $this->_renderedScriptsInitialized = TRUE;
+ return [];
+ }
+ }
+
+ // Store information on rendered scripts in application cache
+ private function _appendRenderedScripts(array $newScripts, $compiledFileKey) {
+ $scripts = $this->_getRenderedScripts();
+ if (!isset($scripts[$compiledFileKey])) {
+ $scripts[$compiledFileKey] = [];
+ }
+ $scripts[$compiledFileKey] = array_unique(
+ array_merge(
+ $scripts[$compiledFileKey],
+ $newScripts
+ )
+ );
+ $this->_getCache()->set(
+ $this->_getRenderedScriptsStoreKey(),
+ $scripts
+ );
+ }
+
+ // Compress JS file and return its content
+ private function _getCompressedScript($path) {
+ return trim(TJavaScript::JSMin(
+ file_get_contents($path)
+ ));
+ }
+
+ // Join multiple script files into single asset, mark all of them as rendered in parent
+ private function _compileScriptFiles(array $files) {
+ foreach ($files as $file) {
+ $this->markScriptFileAsRendered($file);
+ }
+ $paths = $this->_getBasePaths($files);
+ $cacheFile = $this->_getCacheFilePath(
+ $this->_getFileCollectionCacheKey($paths) . '.js',
+ 'scripts'
+ );
+ $this->_appendRenderedScripts($files, $cacheFile);
+ if (!$this->_isCacheValid($cacheFile, $paths)) {
+ $scriptContent = implode(
+ PHP_EOL,
+ array_map(
+ [$this, '_getCompressedScript'],
+ $paths
+ )
+ );
+ file_put_contents($cacheFile, $scriptContent);
+ }
+ return $this->Application->AssetManager->publishFilePath($cacheFile);
+ }
+
+ // Write output tag for a single, compiled JS asset, consisting of multiple local assets
+ private function _renderLocalScriptFiles(THtmlWriter $writer, array $localFiles) {
+ if ($localFiles) {
+ $assetPath = $this->_compileScriptFiles($localFiles);
+ $writer->write(TJavascript::renderScriptFile($assetPath));
+ }
+ }
+
+ // Keep track of external JS scripts rendered on the page
+ private function _renderExternalScriptFiles(THtmlWriter $writer, array $externalFiles) {
+ if ($externalFiles) {
+ foreach ($externalFiles as $file) {
+ $this->markScriptFileAsRendered($file);
+ $this->_appendRenderedScripts([$file], $file);
+ }
+ parent::renderScriptFiles($writer, $externalFiles);
+ }
+ }
+
+ // Determine actual asset URL that a source JS file was rendered as (after compilation/compression)
+ // FALSE means script wasn't rendered at all (i.e. was just registered in current callback)
+ private function _getRenderedScriptUrl($registeredScript) {
+ $renderedScripts = $this->_getRenderedScripts();
+ foreach ($renderedScripts as $compiledFile => $scripts) {
+ if (in_array($registeredScript, $scripts)) {
+ if (file_exists($compiledFile)) {
+ return $this->Application->AssetManager->getPublishedUrl(
+ $compiledFile
+ );
+ } else {
+ return $registeredScript;
+ }
+ }
+ }
+ return FALSE;
+ }
+
+ // Scripts - public interface overrides
+
+ // In application modes "higher" than Debug, compile JS assets to as few files as possible
+ public function renderScriptFiles($writer, Array $files) {
+ if ($this->getApplication()->getMode() !== TApplicationMode::Debug) {
+ if ($files) {
+ $localFiles = $this->_determineLocalFiles($files);
+ $this->_renderLocalScriptFiles($writer, $localFiles);
+ $externalFiles = array_diff($files, $localFiles);
+ $this->_renderExternalScriptFiles($writer, $externalFiles);
+ }
+ } else {
+ parent::renderScriptFiles($writer, $files);
+ }
+ }
+
+ // When above compilation occurs, list of JS URLs a callback requires
+ // significantly deviates from parent implementation.
+ // Also, new scripts that have been registered in current callback may need compiling, too.
+ public function getScriptUrls() {
+ if ($this->getApplication()->getMode() !== TApplicationMode::Debug) {
+ $registeredScripts = array_unique(
+ array_merge(
+ $this->_scripts, $this->_headScripts
+ )
+ );
+ $scriptUrls = [];
+ $newScripts = [];
+ foreach ($registeredScripts as $registeredScript) {
+ $renderedScriptUrl = $this->_getRenderedScriptUrl($registeredScript);
+ if ($renderedScriptUrl) {
+ $scriptUrls[] = $renderedScriptUrl;
+ } else {
+ $newScripts[] = $registeredScript;
+ }
+ }
+ $newLocalScripts = $this->_determineLocalFiles($newScripts);
+ $newRemoteScripts = array_diff($newScripts, $newLocalScripts);
+ if ($newLocalScripts) {
+ $scriptUrls[] = $this->_compileScriptFiles($newLocalScripts);
+ }
+ $scriptUrls = array_values(
+ array_unique(array_merge($scriptUrls, $newRemoteScripts))
+ );
+ return $scriptUrls;
+ }
+ return parent::getScriptUrls();
+ }
+
+ private $_scripts = [];
+ private $_headScripts = [];
+
+ // Keep track of what we're registering
+ public function registerScriptFile($key, $file) {
+ $this->_scripts[$key] = $file;
+ return parent::registerScriptFile($key, $file);
+ }
+
+ // Keep track of what we're registering
+ public function registerHeadScriptFile($key, $file) {
+ $this->_headScripts[$key] = $file;
+ return parent::registerHeadScriptFile($key, $file);
+ }
+
+ // Stylesheets - internal methods
+
+ private $_renderedSheetsInitialized = FALSE;
+
+ // Retrieve stylesheets already rendered on current page from application cache,
+ // maintaining the state over callbacks
+ private function _getRenderedSheets() {
+ $sessionKey = $this->_getRenderedSheetsStoreKey();
+ if ($this->_page->IsCallBack || $this->_renderedSheetsInitialized) {
+ return $this->_getCache()->get($sessionKey) ?: [];
+ } else {
+ $this->_getCache()->delete($sessionKey);
+ $this->_renderedSheetsInitialized = TRUE;
+ return [];
+ }
+ }
+
+ // Store information on rendered stylesheets in application cache
+ private function _appendRenderedSheets(array $newSheets, $compiledFileKey) {
+ $sheets = $this->_getRenderedSheets();
+ if (!isset($sheets[$compiledFileKey])) {
+ $sheets[$compiledFileKey] = [];
+ }
+ $sheets[$compiledFileKey] = array_merge(
+ $sheets[$compiledFileKey],
+ $newSheets
+ );
+ $this->_getCache()->set(
+ $this->_getRenderedSheetsStoreKey(),
+ $sheets
+ );
+ }
+
+ // Resolve all "url(FILE)" CSS directives pointing
+ // to relative resources to specified path
+ private function _fixStyleSheetPaths($content, $originalUrl) {
+ $originalDir = dirname($originalUrl . '.');
+ return preg_replace_callback(
+ '/url\s*\([\'"]?(.*?)[\'"]?\)/',
+ function($matches) use($originalDir) {
+ $url = parse_url($matches[1]);
+ // ignore absolute URLs and paths
+ if (isset($url['scheme'])
+ || isset($url['host'])
+ || $url['path'][0] == '/') {
+ return $matches[0];
+ }
+ // resolve relative paths
+ return str_replace(
+ $matches[1],
+ $originalDir . '/' . $matches[1],
+ $matches[0]
+ );
+ },
+ $content
+ );
+ }
+
+ // Compress CSS file and return its content
+ private function _getCompressedSheet($origPath, $path) {
+ Prado::using('Lib.cssmin.CssMin');
+ return trim(CssMin::minify(
+ $this->_fixStyleSheetPaths(
+ file_get_contents($path),
+ $origPath
+ )
+ ));
+ }
+
+ // Join multiple stylesheet files into single asset
+ private function _compileSheetFiles(array $files) {
+ $paths = $this->_getBasePaths(
+ array_map('reset', $files)
+ );
+ // determine if file was registered as a themed sheet
+ $correctedPaths = [];
+ foreach ($paths as $url => $path) {
+ $correctedPaths[
+ in_array($url, $this->_themeStyles) && $this->_page->Theme
+ ? $this->_page->Theme->BaseUrl . DIRECTORY_SEPARATOR
+ : $url
+ ] = $path;
+ }
+ $cacheFile = $this->_getCacheFilePath(
+ $this->_getFileCollectionCacheKey($paths) . '.css',
+ 'styles'
+ );
+ $this->_appendRenderedSheets($files, $cacheFile);
+ if (!$this->_isCacheValid($cacheFile, $paths)) {
+ $styleContent = implode(
+ PHP_EOL,
+ array_map(
+ [$this, '_getCompressedSheet'],
+ array_keys($correctedPaths),
+ $correctedPaths
+ )
+ );
+ file_put_contents($cacheFile, $styleContent);
+ }
+ return $this->Application->AssetManager->publishFilePath($cacheFile);
+ }
+
+ // Filter only local stylesheet file entries
+ private function _determineLocalSheetFiles(array $files) {
+ $basePath = $this->_getBasePath();
+ return array_filter(
+ $files,
+ function($file) use($basePath) {
+ return file_exists($basePath . DIRECTORY_SEPARATOR . $file[0]);
+ }
+ );
+ }
+
+ // Write HTML markup for CSS stylesheet
+ private function _renderSheetFileTag(THtmlWriter $writer, $href, $media) {
+ $writer->addAttribute('rel', 'stylesheet');
+ $writer->addAttribute('type', 'text/css');
+ $writer->addAttribute('media', $media);
+ $writer->addAttribute('href', $href);
+ $writer->renderBeginTag('link');
+ $writer->write(PHP_EOL);
+ }
+
+ // Group registered local CSS assets by media query string and render markup for compiled sheets
+ private function _renderLocalSheetFiles(THtmlWriter $writer, array $localFiles) {
+ if ($localFiles) {
+ $fileTypes = [];
+ foreach ($localFiles as $file) {
+ $type = $file[1] ?: 'all';
+ if (!isset($fileTypes[$type])) {
+ $fileTypes[$type] = [];
+ }
+ $fileTypes[$type][] = $file;
+ }
+ foreach ($fileTypes as $type => $files) {
+ $assetPath = $this->_compileSheetFiles($files);
+ $this->_renderSheetFileTag($writer, $assetPath, $type);
+ }
+ }
+ }
+
+ // Render markup for external stylesheets
+ private function _renderExternalSheetFiles(THtmlWriter $writer, array $externalFiles) {
+ if ($externalFiles) {
+ foreach ($externalFiles as $file) {
+ $this->_appendRenderedSheets([$file], $file[0]);
+ $this->_renderSheetFileTag($writer, $file[0], $file[1] ?: 'all');
+ }
+ }
+ }
+
+ // Determine actual asset URL that a source CSS file was rendered as (after compilation/compression)
+ // FALSE means sheet wasn't rendered at all (i.e. was just registered in current callback)
+ // Media query types can easily be ignored in a callback request, only URLs matter
+ private function _getRenderedSheetUrl($registeredSheet) {
+ $renderedSheets = $this->_getRenderedSheets();
+ foreach ($renderedSheets as $compiledFile => $sheets) {
+ foreach ($sheets as $sheet) {
+ if ($registeredSheet[0] == $sheet[0]) {
+ if (file_exists($compiledFile)) {
+ return $this->Application->AssetManager->getPublishedUrl(
+ $compiledFile
+ );
+ } else {
+ return $registeredSheet[0];
+ }
+ }
+ }
+ }
+ return FALSE;
+ }
+
+ // Stylesheets - public interface overrides
+
+ // In application modes "higher" than Debug, compile CSS assets to as few files as possible
+ public function renderStyleSheetFiles($writer) {
+ if ($this->getApplication()->getMode() !== TApplicationMode::Debug) {
+ $files = $this->_styles;
+ if ($files) {
+ $localFiles = $this->_determineLocalSheetFiles($files);
+ $this->_renderLocalSheetFiles($writer, $localFiles);
+ $externalFiles = array_diff_key($files, $localFiles);
+ $this->_renderExternalSheetFiles($writer, $externalFiles);
+ }
+ } else {
+ parent::renderStyleSheetFiles($writer);
+ }
+ }
+
+ // When above compilation occurs, list of CSS URLs a callback requires
+ // significantly deviates from parent implementation.
+ // New stylesheets may need compiling, as well.
+ public function getStyleSheetUrls() {
+ if ($this->getApplication()->getMode() !== TApplicationMode::Debug) {
+ $registeredSheets = $this->_styles;
+ $sheetUrls = [];
+ $newSheets = [];
+ foreach ($registeredSheets as $registeredSheet) {
+ $renderedSheetUrl = $this->_getRenderedSheetUrl(
+ $registeredSheet
+ );
+ if ($renderedSheetUrl) {
+ $sheetUrls[] = $renderedSheetUrl;
+ } else {
+ $newSheets[] = $registeredSheet;
+ }
+ }
+ $newLocalSheets = $this->_determineLocalSheetFiles($newSheets);
+ $newLocalUrls = array_map('reset', $newLocalSheets);
+ $newRemoteSheets = array_filter(
+ $newSheets,
+ function($sheet) use($newLocalUrls) {
+ return !in_array($sheet[0], $newLocalUrls);
+ }
+ );
+ $newRemoteUrls = array_map('reset', $newRemoteSheets);
+ if ($newLocalSheets) {
+ $sheetUrls[] = $this->_compileSheetFiles($newLocalSheets);
+ }
+ $sheetUrls = array_values(
+ array_unique(array_merge($sheetUrls, $newRemoteUrls))
+ );
+ return $sheetUrls;
+ }
+ // And even in Debug mode, theme sheets before fixing paths
+ // might also have been published via assets manager,
+ // so we have to discard these from parent list.
+ return array_diff(
+ parent::getStyleSheetUrls(),
+ $this->_fixedStyleFiles
+ );
+ }
+
+ private $_styles = [];
+
+ // Keep track of what we're registering
+ public function registerStyleSheetFile($key, $file, $media = '') {
+ $this->_styles[$key] = [$file, $media];
+ return parent::registerStyleSheetFile($key, $file, $media ?: 'all');
+ }
+
+ private $_themeStyles = [];
+ private $_fixedStyleFiles = [];
+
+ // New method, automatically corrects URLs within stylesheet to current page theme
+ // when sheets are not compiled (when they are, it's done on compilation anyways).
+ // Such files are rewritten (not in place, though) and registered so that they don't show up
+ // within published assets returned from asset manager, as they don't end up in the markup.
+ public function registerThemeStyleSheetFile($key, $file, $media = '') {
+ if ($this->getApplication()->getMode() !== TApplicationMode::Debug) {
+ $this->_themeStyles[$key] = $file;
+ } else {
+ if ($this->_isFileLocal($file) && $this->_page->Theme) {
+ $tempFile = $this->_getCacheFilePath(
+ $this->_getFileCollectionCacheKey([
+ $this->_getBasePath()
+ . DIRECTORY_SEPARATOR
+ . $file
+ ])
+ . '.' . basename($file),
+ $this->_page->Theme->Name
+ );
+ file_put_contents(
+ $tempFile,
+ $this->_fixStyleSheetPaths(
+ file_get_contents(
+ $this->_getBasePath() . DIRECTORY_SEPARATOR . $file
+ ),
+ $this->_page->Theme->BaseUrl . DIRECTORY_SEPARATOR
+ )
+ );
+ $this->_fixedStyleFiles[] = $file;
+ $file = $this->Application->AssetManager->publishFilePath($tempFile);
+ }
+ }
+ return $this->registerStyleSheetFile($key, $file, $media ?: 'all');
+ }
+
+}
+
+?>