diff --git a/application/compilecssservice.class.inc.php b/application/compilecssservice.class.inc.php new file mode 100644 index 000000000..dde4af42b --- /dev/null +++ b/application/compilecssservice.class.inc.php @@ -0,0 +1,38 @@ + + * @since 3.0.0 N°2982 + */ +class CompileCSSService +{ + /** + * CompileCSSService constructor. + */ + public function __construct() + { + } + + public function CompileCSSFromSASS($sSassContent, $aImportPaths = [], $aVariables = []){ + return utils::CompileCSSFromSASS($sSassContent, $aImportPaths, $aVariables); + } +} \ No newline at end of file diff --git a/application/findstylesheetobject.class.inc.php b/application/findstylesheetobject.class.inc.php new file mode 100644 index 000000000..43985f378 --- /dev/null +++ b/application/findstylesheetobject.class.inc.php @@ -0,0 +1,114 @@ + + * @since 3.0.0 N°3588 + */ +class FindStylesheetObject{ + + //file URIs + private $aStylesheetFileURIs; + + //fill paths + private $aStylesheetImportPaths; + private $aAllStylesheetFilePaths; + private $sLastStyleSheetPath; + + private $iLastModified; + + /** + * FindStylesheetObject constructor. + */ + public function __construct() + { + $this->aStylesheetFileURIs = []; + $this->aStylesheetImportPaths = []; + $this->aAllStylesheetFilePaths = []; + $this->sLastStyleSheetPath = ""; + $this->iLastModified = 0; + } + + public function GetLastStylesheetFile(): string + { + return $this->sLastStyleSheetPath; + } + + public function GetImportPaths(): array + { + return $this->aStylesheetImportPaths; + } + + /** + * @return array : main stylesheets URIs + */ + public function GetStylesheetFileURIs(): array + { + return $this->aStylesheetFileURIs; + } + + public function GetLastModified() : int + { + return $this->iLastModified; + } + + /** + * @return array : main stylesheets paths + included files paths + */ + public function GetAllStylesheetPaths(): array + { + return $this->aAllStylesheetFilePaths; + } + + /** + * @return string : last found stylesheet URI + */ + public function GetLastStyleSheetPath(): string + { + return $this->sLastStyleSheetPath; + } + + public function AddStylesheet(string $sStylesheetFileURI, string $sStylesheetFilePath): void + { + $this->aStylesheetFileURIs[] = $sStylesheetFileURI; + $this->aAllStylesheetFilePaths[] = $sStylesheetFilePath; + $this->sLastStyleSheetPath = $sStylesheetFilePath; + } + + public function AlreadyFetched(string $sStylesheetFilePath) : bool { + return in_array($sStylesheetFilePath, $this->aAllStylesheetFilePaths); + } + + public function AddImport(string $sStylesheetFileURI, string $sStylesheetFilePath): void + { + $this->aStylesheetImportPaths[$sStylesheetFileURI] = $sStylesheetFilePath; + $this->aAllStylesheetFilePaths[] = $sStylesheetFilePath; + } + + public function UpdateLastModified(string $sStylesheetFile): void + { + $this->iLastModified = max($this->iLastModified, @filemtime($sStylesheetFile)); + } + + public function ResetLastStyleSheet(): void + { + $this->sLastStyleSheetPath = ""; + } +} \ No newline at end of file diff --git a/application/themehandler.class.inc.php b/application/themehandler.class.inc.php index 4c4f6203b..e5144dd75 100644 --- a/application/themehandler.class.inc.php +++ b/application/themehandler.class.inc.php @@ -190,32 +190,27 @@ class ThemeHandler $aThemeParametersWithVersion = self::CloneThemeParameterAndIncludeVersion($aThemeParameters, $sSetupCompilationTimestampInSecunds); - $sTmpThemeScssContent = ''; - $iStyleLastModified = 0; clearstatcache(); - // Loading files to import and stylesheet to compile, also getting most recent modification time on overall files - $aStylesheetFiles = []; + // Loading files to import and stylesheet to compile, also getting most recent modification time on overall files + $sTmpThemeScssContent = ''; + $oFindStylesheetObject = new FindStylesheetObject(); foreach ($aThemeParameters['imports'] as $sImport) { - $sTmpThemeScssContent .= '@import "'.$sImport.'";'."\n"; - - $sFile = static::FindStylesheetFile($sImport, $aImportsPaths); - $iImportLastModified = @filemtime($sFile); - $aStylesheetFiles[] = $sFile; - $iStyleLastModified = $iStyleLastModified < $iImportLastModified ? $iImportLastModified : $iStyleLastModified; + static::FindStylesheetFile($sImport, $aImportsPaths, $oFindStylesheetObject); } foreach ($aThemeParameters['stylesheets'] as $sStylesheet) { - $sTmpThemeScssContent .= '@import "'.$sStylesheet.'";'."\n"; - - $sFile = static::FindStylesheetFile($sStylesheet, $aImportsPaths); - $iStylesheetLastModified = @filemtime($sFile); - $aStylesheetFiles[] = $sFile; - $iStyleLastModified = $iStyleLastModified < $iStylesheetLastModified ? $iStylesheetLastModified : $iStyleLastModified; + static::FindStylesheetFile($sStylesheet, $aImportsPaths, $oFindStylesheetObject); } - $aIncludedImages=static::GetIncludedImages($aThemeParametersWithVersion, $aStylesheetFiles, $sThemeId); + foreach ($oFindStylesheetObject->GetStylesheetFileURIs() as $sStylesheet){ + $sTmpThemeScssContent .= '@import "'.$sStylesheet.'";'."\n"; + } + + $iStyleLastModified = $oFindStylesheetObject->GetLastModified(); + + $aIncludedImages=static::GetIncludedImages($aThemeParametersWithVersion, $oFindStylesheetObject->GetAllStylesheetPaths(), $sThemeId); foreach ($aIncludedImages as $sImage) { if (is_file($sImage)) @@ -284,6 +279,7 @@ CSS; } /** + * @since 3.0.0 N°2982 * Compute the signature of a theme defined by its theme parameters. The signature is a JSON structure of * 1) one MD5 of all the variables/values (JSON encoded) * 2) the MD5 of each stylesheet file @@ -306,16 +302,33 @@ CSS; 'images' => [] ]; + $oFindStylesheetObject = new FindStylesheetObject(); + foreach ($aThemeParameters['imports'] as $key => $sImport) { - $sFile = static::FindStylesheetFile($sImport, $aImportsPaths); - $aSignature['stylesheets'][$key] = md5_file($sFile); + static::FindStylesheetFile($sImport, $aImportsPaths, $oFindStylesheetObject); + $sFile = $oFindStylesheetObject->GetLastStylesheetFile(); + if (!empty($sFile)){ + $aSignature['stylesheets'][$key] = md5_file($sFile); + } } foreach ($aThemeParameters['stylesheets'] as $key => $sStylesheet) { - $sFile = static::FindStylesheetFile($sStylesheet, $aImportsPaths); - $aSignature['stylesheets'][$key] = md5_file($sFile); + static::FindStylesheetFile($sStylesheet, $aImportsPaths, $oFindStylesheetObject); + $sFile = $oFindStylesheetObject->GetLastStylesheetFile(); + + if (!empty($sFile)){ + $aSignature['stylesheets'][$key] = md5_file($sFile); + } } + + $aFiles = $oFindStylesheetObject->GetImportPaths(); + if (count($aFiles) !== 0) { + foreach ($aFiles as $sFileURI => $sFilePath) { + $aSignature['imports'][$sFileURI] = md5_file($sFilePath); + } + } + foreach ($aIncludedImages as $sImage) { if (is_file($sImage)) { @@ -335,7 +348,7 @@ CSS; * @param string $sThemeId : used only for logging purpose * * @return array complete path of the images, but with slashes as dir separator instead of DIRECTORY_SEPARATOR - * @since 3.0.0 + * @since 3.0.0 N°2982 */ public static function GetIncludedImages($aThemeParametersVariables, $aStylesheetFiles, $sThemeId) { @@ -430,6 +443,7 @@ CSS; } /** + * @since 3.0.0 N°2982 * Complete url using provided variables. Example with $var=1: XX + $var => XX1 * @param $aMap * @param $aThemeParametersVariables @@ -462,6 +476,7 @@ CSS; } /** + * @since 3.0.0 N°2982 * Find missing variable values from SCSS content based on their name. * * @param $aThemeParametersVariables @@ -520,6 +535,7 @@ CSS; } /** + * @since 3.0.0 N°2982 * @param $aFoundVariables * @param array $aToCompleteUrls * @param array $aCompleteUrls @@ -564,6 +580,7 @@ CSS; } /** + * @since 3.0.0 N°2982 * Find all referenced URLs from a SCSS file. * @param $aThemeParametersVariables * @param $sStylesheetFile @@ -622,6 +639,7 @@ CSS; } /** + * @since 3.0.0 N°2982 * Calculate url based on its template + variables. * @param $sUrlTemplate * @param $aFoundVariables @@ -672,6 +690,7 @@ CSS; /** + * @since 3.0.0 N°2982 * Extract the signature for a generated CSS file. The signature MUST be alone one line immediately * followed (on the next line) by the === SIGNATURE END === pattern * @@ -700,6 +719,12 @@ CSS; return $sPreviousLine; } + /** + * @since 3.0.0 N°2982 + * @param $JsonSignature + * + * @return false|mixed + */ public static function GetVarSignature($JsonSignature) { $aJsonArray = json_decode($JsonSignature, true); @@ -711,31 +736,81 @@ CSS; } /** - * Find the given file in the list of ImportsPaths directory - * @param string $sFile + * @param string $sFileURI * @param string[] $aImportsPaths - * @throws Exception - * @return string + * @param FindStylesheetObject $oFindStylesheetObject + * @param bool $bImports + * + * @throws \Exception + *@since 3.0.0 N°2982 + * Find the given file in the list '$aImportsPaths' of directory and all included stylesheets as well + * Compute latest timestamp found among all found stylesheets + * */ - public static function FindStylesheetFile($sFile, $aImportsPaths) + public static function FindStylesheetFile(string $sFileURI, array $aImportsPaths, $oFindStylesheetObject, $bImports = false) { + if (! $bImports) { + $oFindStylesheetObject->ResetLastStyleSheet(); + } + foreach($aImportsPaths as $sPath) { - $sImportedFile = realpath($sPath.'/'.$sFile); - if (file_exists($sImportedFile)) + $sFilePath = $sPath.'/'.$sFileURI; + $sImportedFile = realpath($sFilePath); + if ($sImportedFile === false){ + // Handle shortcut syntax : @import "typo" ; + // file matched: typo.scss + $sFilePath2 = "$sFilePath.scss"; + $sImportedFile = realpath($sFilePath2); + if ($sImportedFile){ + self::FindStylesheetFile("$sFileURI.scss", [ $sPath ], $oFindStylesheetObject, $bImports); + $sImportedFile = false; + } + } + + if ($sImportedFile === false){ + // Handle shortcut syntax : @import "typo" ; + // file matched: _typo.scss + $sShortCut = substr($sFilePath, strrpos($sFilePath, '/') + 1); + $sFilePath = str_replace($sShortCut, "_$sShortCut.scss", $sFilePath); + $sFileURI = str_replace($sShortCut, "_$sShortCut.scss", $sFileURI); + $sImportedFile = realpath($sFilePath); + } + + if ((file_exists($sImportedFile)) + && (!$oFindStylesheetObject->AlreadyFetched($sImportedFile))) { - return $sImportedFile; + if ($bImports){ + $oFindStylesheetObject->AddImport($sFileURI, $sImportedFile); + }else{ + $oFindStylesheetObject->AddStylesheet($sFileURI, $sImportedFile); + } + $oFindStylesheetObject->UpdateLastModified($sImportedFile); + + //Regexp matching on all included scss files : @import 'XXX.scss'; + $sDirUri = dirname($sFileURI); + preg_match_all('/@import \s*[\"\']([^\"\']*)\s*[\"\']\s*;/', file_get_contents($sImportedFile), $aMatches); + if ( (is_array($aMatches)) && (count($aMatches)!==0) ){ + foreach ($aMatches[1] as $sImportedFile){ + self::FindStylesheetFile("$sDirUri/$sImportedFile", [ $sPath ], $oFindStylesheetObject, true); + } + } } } - return ''; // Not found, fail silently, maybe the SCSS compiler knowns better... } + /** + * @since 3.0.0 N°2982 + * Used for testing purpose + * @param $oCompileCSSServiceMock + */ public static function MockCompileCSSService($oCompileCSSServiceMock) { static::$oCompileCSSService = $oCompileCSSServiceMock; } /** + * @since 3.0.0 N°2982 * Clone variable array and include $version with bSetupCompilationTimestamp value * @param $aThemeParameters * @param $bSetupCompilationTimestamp @@ -758,18 +833,3 @@ CSS; } } -class CompileCSSService -{ - /** - * CompileCSSService constructor. - */ - public function __construct() - { - } - - public function CompileCSSFromSASS($sSassContent, $aImportPaths = [], $aVariables = []){ - return utils::CompileCSSFromSASS($sSassContent, $aImportPaths, $aVariables); - } - -} - diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 09842e794..5a3cf450f 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -335,7 +335,7 @@ return array( 'Combodo\\iTop\\Renderer\\FormRenderer' => $baseDir . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => $baseDir . '/sources/Renderer/RenderingOutput.php', 'Combodo\\iTop\\TwigExtension' => $baseDir . '/application/twigextension.class.inc.php', - 'CompileCSSService' => $baseDir . '/application/themehandler.class.inc.php', + 'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Config' => $baseDir . '/core/config.class.inc.php', 'ConfigException' => $baseDir . '/application/exceptions/ConfigException.php', @@ -445,6 +445,7 @@ return array( 'FilterDefinition' => $baseDir . '/core/filterdef.class.inc.php', 'FilterFromAttribute' => $baseDir . '/core/filterdef.class.inc.php', 'FilterPrivateKey' => $baseDir . '/core/filterdef.class.inc.php', + 'FindStylesheetObject' => $baseDir . '/application/findstylesheetobject.class.inc.php', 'FunctionExpression' => $baseDir . '/core/oql/expression.class.inc.php', 'FunctionOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php', 'GraphEdge' => $baseDir . '/core/simplegraph.class.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 507a4989b..6d95d97b0 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -565,7 +565,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'Combodo\\iTop\\Renderer\\FormRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => __DIR__ . '/../..' . '/sources/Renderer/RenderingOutput.php', 'Combodo\\iTop\\TwigExtension' => __DIR__ . '/../..' . '/application/twigextension.class.inc.php', - 'CompileCSSService' => __DIR__ . '/../..' . '/application/themehandler.class.inc.php', + 'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php', 'ConfigException' => __DIR__ . '/../..' . '/application/exceptions/ConfigException.php', @@ -675,6 +675,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'FilterDefinition' => __DIR__ . '/../..' . '/core/filterdef.class.inc.php', 'FilterFromAttribute' => __DIR__ . '/../..' . '/core/filterdef.class.inc.php', 'FilterPrivateKey' => __DIR__ . '/../..' . '/core/filterdef.class.inc.php', + 'FindStylesheetObject' => __DIR__ . '/../..' . '/application/findstylesheetobject.class.inc.php', 'FunctionExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php', 'FunctionOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php', 'GraphEdge' => __DIR__ . '/../..' . '/core/simplegraph.class.inc.php', diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index cf1f462a5..3c71d3e3c 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -2896,6 +2896,8 @@ EOF; { SetupLog::Info("Replacing theme '$sThemeId' precompiled file in file $sPostCompilationLatestPrecompiledFile for next setup."); copy($sThemeDir.'/main.css', $sPostCompilationLatestPrecompiledFile); + }else { + SetupLog::Info("No theme '$sThemeId' compilation was required during setup."); } } $this->Log(sprintf('Themes compilation took: %.3f ms for %d themes.', (microtime(true) - $fStart)*1000.0, count($aThemes))); diff --git a/test/application/ThemeHandlerTest.php b/test/application/ThemeHandlerTest.php index 466f1f99a..2f6a7f95f 100644 --- a/test/application/ThemeHandlerTest.php +++ b/test/application/ThemeHandlerTest.php @@ -6,7 +6,7 @@ use Combodo\iTop\Test\UnitTest\ItopTestCase; * @runTestsInSeparateProcesses * @preserveGlobalState disabled * @backupGlobals disabled - * @covers utils + * @covers ThemeHandler */ class ThemeHandlerTest extends ItopTestCase { @@ -159,15 +159,15 @@ class ThemeHandlerTest extends ItopTestCase $aThemeParameters['variables'][$sVariableId] = $oVariable->GetText(); } - $aStylesheetFiles = []; /** @var \DOMNodeList $oImports */ $oImports = $oTheme->GetNodes('imports/import'); + $oFindStylesheetObject = new FindStylesheetObject(); + foreach ($oImports as $oImport) { $sImportId = $oImport->getAttribute('id'); $aThemeParameters['imports'][$sImportId] = $oImport->GetText(); - $sFile = ThemeHandler::FindStylesheetFile($oImport->GetText(), $aImportsPaths); - $aStylesheetFiles[] = $sFile; + ThemeHandler::FindStylesheetFile($oImport->GetText(), $aImportsPaths, $oFindStylesheetObject); } /** @var \DOMNodeList $oStylesheets */ @@ -176,11 +176,10 @@ class ThemeHandlerTest extends ItopTestCase { $sStylesheetId = $oStylesheet->getAttribute('id'); $aThemeParameters['stylesheets'][$sStylesheetId] = $oStylesheet->GetText(); - $sFile = ThemeHandler::FindStylesheetFile($oStylesheet->GetText(), $aImportsPaths); - $aStylesheetFiles[] = $sFile; + ThemeHandler::FindStylesheetFile($oStylesheet->GetText(), $aImportsPaths, $oFindStylesheetObject); } - $aIncludedImages = ThemeHandler::GetIncludedImages($aThemeParameters['variables'], $aStylesheetFiles, $sThemeId); + $aIncludedImages = ThemeHandler::GetIncludedImages($aThemeParameters['variables'], $oFindStylesheetObject->GetStylesheetFileURIs(), $sThemeId); $compiled_json_sig = ThemeHandler::ComputeSignature($aThemeParameters, $aImportsPaths, $aIncludedImages); //echo " current signature: $compiled_json_sig\n"; @@ -603,6 +602,67 @@ SCSS; $this->assertEquals($aExpectedImages, $aIncludedImages); } + /** + * @dataProvider FindStylesheetFileProvider + * @throws \Exception + */ + public function testFindStylesheetFile(string $sFileToFind, array $aAllImports){ + $aImportsPath = $this->sTmpDir.'/branding/'; + $aExpectedAllImports =[]; + if (count($aAllImports)!==0){ + foreach ($aAllImports as $sFileURI){ + $aExpectedAllImports [$sFileURI] = $aImportsPath.$sFileURI; + } + } + + + $oFindStylesheetObject = new FindStylesheetObject(); + ThemeHandler::FindStylesheetFile($sFileToFind, [$aImportsPath], $oFindStylesheetObject); + + $this->assertEquals([$sFileToFind], $oFindStylesheetObject->GetStylesheetFileURIs()); + $this->assertEquals($aExpectedAllImports, $oFindStylesheetObject->GetImportPaths()); + $this->assertEquals($aImportsPath.$sFileToFind, $oFindStylesheetObject->GetLastStyleSheetPath()); + + $aExpectedAllStylesheetPaths = []; + foreach (array_merge([$sFileToFind], $aAllImports) as $sFileUri){ + $aExpectedAllStylesheetPaths [] = $aImportsPath.$sFileUri; + } + $this->assertEquals($aExpectedAllStylesheetPaths, $oFindStylesheetObject->GetAllStylesheetPaths()); + } + + public function FindStylesheetFileProvider(){ + $sFileToFind3 = "css/multi_imports.scss"; + $sFileToFind4 = "css/included_file1.scss"; + $sFileToFind5 = "css/included_scss/included_file2.scss"; + + return [ + "single file to find" => [ + "sFileToFind" => "css/DO_NOT_CHANGE.light-grey.scss", + "aAllImports" => [] + ], + "scss with simple @imports" => [ + "sFileToFind" => "css/simple_import.scss", + "aAllImports" => [$sFileToFind4] + ], + "scss with multi @imports" => [ + "sFileToFind" => $sFileToFind3, + "aAllImports" => [$sFileToFind4, $sFileToFind5] + ], + "scss with simple @imports in another folder" => [ + "sFileToFind" => "css/simple_import2.scss", + "aAllImports" => [$sFileToFind5] + ], + "scss with @imports shortcut typography => _typography.scss" => [ + "sFileToFind" => "css/shortcut.scss", + "aAllImports" => ["css/_included_file3.scss", "css/included_scss/included_file4.scss"] + ], + "cross_reference & infinite loop" => [ + "sFileToFind" => "css/cross_reference1.scss", + "aAllImports" => ["css/cross_reference2.scss"] + ], + ]; + } + /** * @param $sPath * @param $sExpectedCanonicalPath diff --git a/test/application/theme-handler/expected/css/cross_reference1.scss b/test/application/theme-handler/expected/css/cross_reference1.scss new file mode 100644 index 000000000..349ac72a0 --- /dev/null +++ b/test/application/theme-handler/expected/css/cross_reference1.scss @@ -0,0 +1,2 @@ + +@import 'cross_reference2.scss'; diff --git a/test/application/theme-handler/expected/css/cross_reference2.scss b/test/application/theme-handler/expected/css/cross_reference2.scss new file mode 100644 index 000000000..2ec467b2a --- /dev/null +++ b/test/application/theme-handler/expected/css/cross_reference2.scss @@ -0,0 +1,2 @@ + +@import 'cross_reference1.scss'; diff --git a/test/application/theme-handler/expected/css/included_scss/included_file4.scss b/test/application/theme-handler/expected/css/included_scss/included_file4.scss new file mode 100644 index 000000000..e69de29bb diff --git a/test/application/theme-handler/expected/css/shortcut.scss b/test/application/theme-handler/expected/css/shortcut.scss new file mode 100644 index 000000000..b407f740a --- /dev/null +++ b/test/application/theme-handler/expected/css/shortcut.scss @@ -0,0 +1,3 @@ + +@import 'included_file3'; +@import 'included_scss/included_file4';