Files
oberstufe-alt/typo3conf/ext/scriptmerger/Classes/ScriptmergerCss.php
2018-04-02 08:07:38 +02:00

454 lines
16 KiB
PHP

<?php
namespace SGalinski\Scriptmerger;
/***************************************************************
* Copyright notice
*
* (c) Stefan Galinski <stefan@sgalinski.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
/**
* This class contains the parsing and replacing functionality for css files
*/
class ScriptmergerCss extends ScriptmergerBase {
/**
* holds the javascript code
*
* Structure:
* - $relation (rel attribute)
* - $media (media attribute)
* - $file
* |-content => string
* |-basename => string (base name of $file without file prefix)
* |-minify-ignore => bool
* |-merge-ignore => bool
*
* @var array
*/
protected $css = array();
/**
* Injects the extension configuration
*
* @param array $configuration
* @return void
*/
public function injectExtensionConfiguration(array $configuration) {
parent::injectExtensionConfiguration($configuration);
if (!class_exists('Minify_ImportProcessor', FALSE)) {
require_once(ExtensionManagementUtility::extPath('scriptmerger') . 'Resources/Minify/ImportProcessor.php');
}
\Minify_ImportProcessor::$extensionConfiguration = $this->configuration;
}
/**
* Controller for the css parsing and replacement
*
* @return void
*/
public function process() {
// fetch all remaining css contents
$this->getFiles();
// minify, compress and merging
foreach ($this->css as $relation => $cssByRelation) {
foreach ($cssByRelation as $media => $cssByMedia) {
$mergedContent = '';
$positionOfMergedFile = NULL;
foreach ($cssByMedia as $index => $cssProperties) {
$newFile = '';
// file should be minified
if ($this->configuration['css.']['minify.']['enable'] === '1' &&
!$cssProperties['minify-ignore']
) {
$newFile = $this->minifyFile($cssProperties);
}
// file should be merged
if ($this->configuration['css.']['merge.']['enable'] === '1' &&
!$cssProperties['merge-ignore']
) {
if ($positionOfMergedFile === NULL) {
$positionOfMergedFile = $cssProperties['position-key'];
}
$mergedContent .= $cssProperties['content'] . LF;
unset($this->css[$relation][$media][$index]);
continue;
}
// file should be compressed instead?
if ($this->configuration['css.']['compress.']['enable'] === '1' &&
function_exists('gzcompress') && !$cssProperties['compress-ignore']
) {
$newFile = $this->compressFile($cssProperties);
}
// minification or compression was used
if ($newFile !== '') {
$this->css[$relation][$media][$index]['file'] = $newFile;
$this->css[$relation][$media][$index]['content'] =
$cssProperties['content'];
$this->css[$relation][$media][$index]['basename'] =
$cssProperties['basename'];
}
}
// save merged content inside a new file
if ($this->configuration['css.']['merge.']['enable'] === '1' && $mergedContent !== '') {
if ($this->configuration['css.']['uniqueCharset.']['enable'] === '1') {
$mergedContent = $this->uniqueCharset($mergedContent);
}
// create property array
$properties = array(
'content' => $mergedContent,
'basename' => 'head-' . md5($mergedContent) . '.merged'
);
// write merged file in any case
$newFile = $this->tempDirectories['merged'] . $properties['basename'] . '.css';
if (!file_exists($newFile)) {
$this->writeFile($newFile, $properties['content']);
}
// file should be compressed
if ($this->configuration['css.']['compress.']['enable'] === '1' &&
function_exists('gzcompress')
) {
$newFile = $this->compressFile($properties);
}
// add new entry
$this->css[$relation][$media][] = array(
'file' => $newFile,
'content' => $properties['content'],
'basename' => $properties['basename'],
'position-key' => $positionOfMergedFile,
);
}
}
}
// write the conditional comments and possibly merged css files back to the document
$this->writeToDocument();
}
/**
* Some browser fail on parsing merged CSS files if multiple charset definitions are found.
* Therefor we replace all charset definition's with an empty string and add a single charset
* definition to the beginning of the content. At least Webkit engines fail badly.
*
* @param string $content
* @return string
*/
protected function uniqueCharset($content) {
if (!empty($this->configuration['css.']['uniqueCharset.']['value'])) {
$content = preg_replace('/@charset[^;]+;/', '', $content);
$content = $this->configuration['css.']['uniqueCharset.']['value'] . $content;
}
return $content;
}
/**
* This method parses the output content and saves any found css files or inline code
* into the "css" class property. The output content is cleaned up of the found results.
*
* @return void
*/
protected function getFiles() {
// filter pattern for the inDoc styles (fetches the content)
$filterInDocumentPattern = '/' .
'<style.*?>' . // This expression removes the opening style tag
'(?:.*?\/\*<!\[CDATA\[\*\/)?' . // and the optionally prefixed CDATA string.
'\s*(.*?)' . // We save the pure css content,
'(?:\s*\/\*\]\]>\*\/)?' . // remove the possible closing CDATA string
'\s*<\/style>' . // and closing style tag
'/is';
// parse all available css code inside link and style tags
$cssTags = array();
$pattern = '/' .
'<(link|sty)' . // Parse any link and style tags.
'(?=.+?(?:media="(.*?)"|>))' . // Fetch the media attribute
'(?=.+?(?:href="(.*?)"|>))' . // and the href attribute
'(?=.+?(?:rel="(.*?)"|>))' . // and the rel attribute
'(?=.+?(?:title="(.*?)"|>))' . // and the title attribute of the tag.
'(?=.+?(?:data-ignore="(.*?)"|>))' . // and the data-ignore attribute of the tag.
'(?:[^>]+?\.css[^>]+?\/?>' . // Continue parsing from \1 to the closing tag.
'|le[^>]*?>[^>]+?<\/style>)\s*' .
'/is';
preg_match_all($pattern, $GLOBALS['TSFE']->content, $cssTags);
$amountOfResults = count($cssTags[0]);
if (!$amountOfResults) {
return;
}
$function = create_function('', 'static $i = 0; return \'###MERGER\' . $i++ . \'MERGER###\';');
$GLOBALS['TSFE']->content = preg_replace_callback(
$pattern, $function, $GLOBALS['TSFE']->content, $amountOfResults
);
for ($i = 0; $i < $amountOfResults; ++$i) {
$content = '';
$media = (trim($cssTags[2][$i]) === '') ? 'all' : $cssTags[2][$i];
$media = implode(',', array_map('trim', explode(',', $media)));
$relation = (trim($cssTags[4][$i]) === '') ? 'stylesheet' : $cssTags[4][$i];
$source = $cssTags[3][$i];
$title = trim($cssTags[5][$i]);
$ignoreDataFlagSet = intval($cssTags[6][$i]);
// add basic entry
$this->css[$relation][$media][$i]['minify-ignore'] = FALSE;
$this->css[$relation][$media][$i]['compress-ignore'] = FALSE;
$this->css[$relation][$media][$i]['merge-ignore'] = FALSE;
$this->css[$relation][$media][$i]['addInDocument'] = FALSE;
$this->css[$relation][$media][$i]['useOriginalCodeLine'] = FALSE;
$this->css[$relation][$media][$i]['file'] = $source;
$this->css[$relation][$media][$i]['content'] = '';
$this->css[$relation][$media][$i]['basename'] = '';
$this->css[$relation][$media][$i]['title'] = $title;
$this->css[$relation][$media][$i]['position-key'] = $i;
$this->css[$relation][$media][$i]['original'] = $cssTags[0][$i];
// styles which are added inside the document must be parsed again to fetch the pure css code
$cssTags[1][$i] = (strtolower($cssTags[1][$i]) === 'sty' ? 'style' : strtolower($cssTags[1][$i]));
if ($cssTags[1][$i] === 'style') {
$cssContent = array();
preg_match_all($filterInDocumentPattern, $cssTags[0][$i], $cssContent);
// we doesn't need to continue if it was an empty style tag
if ($cssContent[1][0] === '') {
unset($this->css[$relation][$media][$i]);
continue;
}
// ignore this file if the content could not be fetched
$doNotRemoveInDoc = $this->configuration['css.']['doNotRemoveInDoc'] === '1';
if ($doNotRemoveInDoc || $ignoreDataFlagSet) {
$this->css[$relation][$media][$i]['minify-ignore'] = TRUE;
$this->css[$relation][$media][$i]['compress-ignore'] = TRUE;
$this->css[$relation][$media][$i]['merge-ignore'] = TRUE;
$this->css[$relation][$media][$i]['addInDocument'] = TRUE;
$this->css[$relation][$media][$i]['useOriginalCodeLine'] = TRUE;
$this->css[$relation][$media][$i]['content'] = $cssContent[1][0];
continue;
}
// save the content into a temporary file
$hash = md5($cssContent[1][0]);
$source = $this->tempDirectories['temp'] . 'inDocument-' . $hash;
$tempFile = $source . '.css';
if (!file_exists($source . '.css')) {
$this->writeFile($tempFile, $cssContent[1][0]);
}
// try to resolve any @import occurrences
/** @noinspection PhpUndefinedClassInspection */
$content = \Minify_ImportProcessor::process($tempFile);
$this->css[$relation][$media][$i]['file'] = $tempFile;
$this->css[$relation][$media][$i]['content'] = $content;
$this->css[$relation][$media][$i]['basename'] = basename($source);
} elseif ($source !== '') {
// try to fetch the content of the css file
$file = $source;
if ($GLOBALS['TSFE']->absRefPrefix !== '' && strpos($file, $GLOBALS['TSFE']->absRefPrefix) === 0) {
$file = substr($file, strlen($GLOBALS['TSFE']->absRefPrefix) - 1);
}
if (file_exists(PATH_site . $file)) {
$content = \Minify_ImportProcessor::process(PATH_site . $file);
} else {
$tempFile = $this->getExternalFile($source);
$content = \Minify_ImportProcessor::process($tempFile);
}
// ignore this file if the content could not be fetched
if ($content === '' || $ignoreDataFlagSet) {
$this->css[$relation][$media][$i]['minify-ignore'] = TRUE;
$this->css[$relation][$media][$i]['compress-ignore'] = TRUE;
$this->css[$relation][$media][$i]['merge-ignore'] = TRUE;
$this->css[$relation][$media][$i]['useOriginalCodeLine'] = TRUE;
continue;
}
// check if the file should be ignored for some processes
$ignoreAmount = 0;
if ($this->configuration['css.']['minify.']['ignore'] !== '') {
if (preg_match($this->configuration['css.']['minify.']['ignore'], $source)) {
$this->css[$relation][$media][$i]['minify-ignore'] = TRUE;
++$ignoreAmount;
}
}
if ($this->configuration['css.']['compress.']['ignore'] !== '') {
if (preg_match($this->configuration['css.']['compress.']['ignore'], $source)) {
$this->css[$relation][$media][$i]['compress-ignore'] = TRUE;
++$ignoreAmount;
}
}
if ($this->configuration['css.']['merge.']['ignore'] !== '') {
if (preg_match($this->configuration['css.']['merge.']['ignore'], $source)) {
$this->css[$relation][$media][$i]['merge-ignore'] = TRUE;
++$ignoreAmount;
}
}
if ($ignoreAmount === 3) {
$this->css[$relation][$media][$i]['useOriginalCodeLine'] = TRUE;
}
// set the css file with it's content
$this->css[$relation][$media][$i]['content'] = $content;
}
// get base name for later usage
// base name without file prefix and prefixed hash of the content
$filename = basename($source);
$hash = md5($content);
$this->css[$relation][$media][$i]['basename'] =
substr($filename, 0, strrpos($filename, '.')) . '-' . $hash;
}
}
/**
* This method minifies a css file. It's based upon the Minify_CSS class
* of the project minify.
*
* @param array $properties properties of an entry (copy-by-reference is used!)
* @return string new filename
*/
protected function minifyFile(&$properties) {
// get new filename
$newFile = $this->tempDirectories['minified'] .
$properties['basename'] . '.min.css';
// stop further processing if the file already exists
if (file_exists($newFile)) {
$properties['basename'] .= '.min';
$properties['content'] = file_get_contents($newFile);
return $newFile;
}
// minify content
if (!class_exists('Minify_CSS', FALSE)) {
require_once(ExtensionManagementUtility::extPath('scriptmerger') . 'Resources/Minify/CSS.php');
}
/** @noinspection PhpUndefinedClassInspection */
$properties['content'] = \Minify_CSS::minify($properties['content']);
// save content inside the new file
$this->writeFile($newFile, $properties['content']);
// save new part of the base name
$properties['basename'] .= '.min';
return $newFile;
}
/**
* This method compresses a css file.
*
* @param array $properties properties of an entry (copy-by-reference is used!)
* @return string new filename
*/
protected function compressFile(&$properties) {
$newFile = $this->tempDirectories['compressed'] . $properties['basename'] . '.gz.css';
if (file_exists($newFile)) {
return $newFile;
}
$this->writeFile($newFile, gzencode($properties['content'], 5));
return $newFile;
}
/**
* This method writes the css back to the document.
*
* @return void
*/
protected function writeToDocument() {
$pattern = '';
if (trim($this->configuration['css.']['mergedFilePosition']) !== '') {
$pattern = '/' . preg_quote($this->configuration['css.']['mergedFilePosition'], '/') . '/i';
}
$contentShouldBeAddedInline = $this->configuration['css.']['addContentInDocument'] === '1';
foreach ($this->css as $relation => $cssByRelation) {
$cssByRelation = array_reverse($cssByRelation);
foreach ($cssByRelation as $media => $cssByMedia) {
$cssByMedia = array_reverse($cssByMedia);
foreach ($cssByMedia as $cssProperties) {
if ($cssProperties['useOriginalCodeLine']) {
$content = $cssProperties['original'];
} elseif ($cssProperties['addInDocument'] || $contentShouldBeAddedInline) {
$content = LF . "\t" .
'<style media="' . $media . '" type="text/css">' . LF .
"\t" . '/* <![CDATA[ */' . LF .
"\t" . $cssProperties['content'] . LF .
"\t" . '/* ]]> */' . LF .
"\t" . '</style>' . LF;
} else {
$file = $cssProperties['file'];
if (file_exists($file)) {
$file = $GLOBALS['TSFE']->absRefPrefix .
(PATH_site === '/' ? $file : str_replace(PATH_site, '', $file));
}
$title = (trim($cssProperties['title']) !== '' ?
'title="' . $cssProperties['title'] . '"' : '');
$content = LF . "\t" . '<link rel="' . $relation . '" type="text/css" ' .
'media="' . $media . '" ' . $title . ' href="' . $file . '" />' . LF;
}
if ($pattern === '' || $cssProperties['merge-ignore']) {
$GLOBALS['TSFE']->content = str_replace(
'###MERGER' . $cssProperties['position-key'] . 'MERGER###',
$content,
$GLOBALS['TSFE']->content
);
continue;
} else {
$GLOBALS['TSFE']->content = preg_replace(
$pattern, $content . '\0', $GLOBALS['TSFE']->content, 1
);
}
}
}
}
// remove any empty markers
$pattern = '/###MERGER[0-9]*?MERGER###/is';
$GLOBALS['TSFE']->content = preg_replace($pattern, '', $GLOBALS['TSFE']->content);
}
}
?>