<?php /** * Trait for assets handling in plugins. * * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> * @license GPL2 or any later version * @since Stud.IP 4.4 */ trait PluginAssetsTrait { /** * Adds an asset while detecting the type automatically. * * @param string $asset Asset to add * @param array $variables Variables for the LESS/SCSS compiler, unused for JS * @since Stud.IP 5.4 */ public function addAsset(string $asset, array $variables = []): void { $type = $this->detectAssetType($asset); if ($type === 'js') { $this->addScript($asset); } elseif ($type === 'css') { $this->addStylesheet($asset, $variables); } } /** * Adds many assets while detecting the type automatically. * * @param string[] $assets Assets to add * @param array $variables Variables for the LESS/SCSS compiler, unused * for JS * @param bool $combine If true, the assets will be combined into one * single file for each type * @since Stud.IP 5.4 */ public function addAssets(array $assets, array $variables = [], bool $combine = false): void { if (!$combine) { foreach ($assets as $asset) { $this->addAsset($asset, $variables); } } else { $temp = ['css' => [], 'js' => []]; foreach ($assets as $asset) { $temp[$this->detectAssetType($asset)] = $asset; } if (count($temp['css']) > 0) { $this->addStylesheets($temp['css'], $variables); } if (count($temp['js']) > 0) { $this->addScripts($temp['js']); } } } /** * Adds many stylesheeets at once. * @param array $filenames List of relative filenames * @param array $variables Optional array of variables to pass to the * LESS compiler * @param array $link_attr Attributes to pass to the link elements * @param string $path Common path prefix for all filenames */ protected function addStylesheets(array $filenames, array $variables = [], array $link_attr = [], $path = '') { if (Studip\ENV === 'development') { foreach ($filenames as $filename) { $this->addStylesheet("{$path}{$filename}", $variables, $link_attr); } } $hash = substr(md5(serialize($filenames)), -8); $filename = "combined-{$hash}.css"; // Get asset file from storage $asset = Assets\Storage::getFactory()->createCSSFile( $filename, $this->createMetaData() ); // Compile asset if neccessary if ($asset->isNew()) { $content = ''; foreach ($filenames as $filename) { $file = $this->resolveFilename($filename, $path); $content .= $this->readPluginAssetFile($file, $variables); } $asset->setContent($content); } $this->includeStyleAsset($asset, $link_attr); } /** * Includes given stylesheet in page, compiles less if neccessary * * @param string $filename Name of the stylesheet (css or less) to include * (relative to plugin directory) * @param array $variables Optional array of variables to pass to the * LESS compiler * @param array $link_attr Attributes to pass to the link element */ protected function addStylesheet($filename, array $variables = [], array $link_attr = []) { $extension = pathinfo($filename, PATHINFO_EXTENSION); if (!in_array($extension, ['less', 'scss'])) { PageLayout::addStylesheet( "{$this->getPluginURL()}/{$filename}?v={$this->getPluginVersion()}", $link_attr ); return; } // Create absolute path to assets file $file = $this->resolveFilename($filename); // Get asset file from storage $asset = Assets\Storage::getFactory()->createCSSFile( $file, $this->createMetaData() ); // Compile asset if neccessary if ($asset->isNew()) { $css = $this->readPluginAssetFile($file, $variables); $asset->setContent($css); } $this->includeStyleAsset($asset, $link_attr); } private function includeStyleAsset(Assets\PluginAsset $asset, array $link_attr) { // Include asset in page by reference or directly $download_uri = $asset->getDownloadLink(); if ($download_uri === false) { PageLayout::addStyle($asset->getContent(), $link_attr); } else { $link_attr['rel'] = 'stylesheet'; $link_attr['href'] = $download_uri; $link_attr['type'] = 'text/css'; PageLayout::addHeadElement('link', $link_attr); } } /** * Adds many scripts at once. * @param array $filenames List of relative filenames * @param array $link_attr Attributes to pass to the script elements * @param string $path Common path prefix for all filenames */ protected function addScripts(array $filenames, array $link_attr = [], $path = '') { if (Studip\ENV === 'development') { foreach ($filenames as $filename) { $this->addScript("{$path}{$filename}", $link_attr); } return; } $hash = substr(md5(serialize($filenames)), -8); $filename = "combined-{$hash}.js"; // Get asset file from storage $asset = Assets\Storage::getFactory()->createJSFile( $filename, $this->createMetaData() ); // Compile asset if neccessary if ($asset->isNew()) { $content = ''; foreach ($filenames as $filename) { $file = $this->resolveFilename($filename, $path); $content .= $this->readPluginAssetFile($file) . ';'; } $asset->setContent($content); } // Include asset in page by reference or directly $download_uri = $asset->getDownloadLink(); if ($download_uri === false) { PageLayout::addHeadElement('script', $link_attr, $asset->getContent()); } else { $link_attr['src'] = $download_uri; PageLayout::addHeadElement('script', $link_attr); } } /** * Includes given script in page. * * @param string $filename Name of script file * @param array $link_attr Attributes to pass to the script element */ protected function addScript($filename, array $link_attr = []) { PageLayout::addScript( "{$this->getPluginURL()}/{$filename}?v={$this->getPluginVersion()}", $link_attr ); } /** * Create metadata for plugin assets factory * @return array */ private function createMetaData() { return [ 'plugin_id' => $this->plugin_info['depends'] ?: $this->getPluginId(), 'plugin_version' => $this->getPluginVersion(), ]; } /** * Resolves relative filename to absolute filename. * * @param string $filename Relative filename * @param string $path Optional relative path the file is stored in * @return string * @throws RuntimeException when absolute file is missing */ private function resolveFilename($filename, $path = '') { $file = $GLOBALS['ABSOLUTE_PATH_STUDIP'] . $this->getPluginPath() . '/' . "{$path}{$filename}"; // Fail if file does not exist if (!file_exists($file)) { throw new RuntimeException("Could not locate assets file '{$filename}'"); } return $file; } /** * Reads assets file (and compiles if neccessary). * @param string $filename Name of the file to read * @param array $variables Additional variables for compiler (if appropriate) * @return string */ private function readPluginAssetFile($filename, array $variables = []) { $contents = file_get_contents($filename); $extension = pathinfo($filename, PATHINFO_EXTENSION); if ($extension === 'less') { $contents = Assets\LESSCompiler::getInstance()->compile($contents, $variables + [ 'plugin-path' => $this->getPluginURL(), ]); } elseif ($extension === 'scss') { $contents = Assets\SASSCompiler::getInstance()->compile($contents, $variables + [ 'plugin-path' => '"' . $this->getPluginURL() . '"', ]); } return $contents; } /** * Detects the asset type based on the extension of the asset. * * @param string $asset Asset to test * @return string Either 'css' or 'js' * @throws InvalidArgumentException if no valid type can be detected */ private function detectAssetType(string $asset): string { $extension = pathinfo($asset, PATHINFO_EXTENSION); if ($extension === 'js') { return 'js'; } if (in_array($extension, ['css', 'less', 'scss'])) { return 'css'; } throw new InvalidArgumentException("Unknown asset type {$extension}"); } }