diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 32e9f8dea..8bf0c1bfe 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -51,6 +51,8 @@ class DOMFormatException extends Exception */ class MFCompiler { + const DATA_PRECOMPILED_FOLDER = 'data' . DIRECTORY_SEPARATOR . 'precompiled_styles' . DIRECTORY_SEPARATOR; + /** @var \ModelFactory */ protected $oFactory; @@ -2862,6 +2864,11 @@ EOF; $aThemes[$aDefaultThemeInfo['name']] = $aDefaultThemeInfo['parameters']; } + $sPostCompilationPrecompiledThemeFolder = APPROOT . self::DATA_PRECOMPILED_FOLDER; + if (! is_dir($sPostCompilationPrecompiledThemeFolder)){ + mkdir($sPostCompilationPrecompiledThemeFolder); + } + // Compile themes $fStart = microtime(true); foreach($aThemes as $sThemeId => $aThemeParameters) @@ -2871,30 +2878,87 @@ EOF; { SetupUtils::builddir($sThemeDir); } + // Check if a precompiled version of the theme is supplied - $sPrecompiledFile = $sTempTargetDir.$aThemeParameters['precompiled_stylesheet']; - if (file_exists($sPrecompiledFile) && !is_dir($sPrecompiledFile)) - { - copy($sPrecompiledFile, $sThemeDir.'/main.css'); + $sPostCompilationLatestPrecompiledFile = $sPostCompilationPrecompiledThemeFolder . $sThemeId . ".css"; + + $sPrecompiledFileToUse = $this->UseLatestPrecompiledFile($sTempTargetDir, $aThemeParameters['precompiled_stylesheet'], $sPostCompilationLatestPrecompiledFile, $sThemeId); + if ($sPrecompiledFileToUse != null){ + copy($sPrecompiledFileToUse, $sThemeDir.'/main.css'); // Make sure that the copy of the precompiled file is older than any other files to force a validation of the signature touch($sThemeDir.'/main.css', 1577836800 /* 2020-01-01 00:00:00 */); - - } - else if ($sPrecompiledFile != '') - { - $this->Log("Precompiled file not found: '$sPrecompiledFile'"); } $bHasCompiled = ThemeHandler::CompileTheme($sThemeId, true, $this->sCompilationTimeStamp, $aThemeParameters, $aImportsPaths, $sTempTargetDir); - $sInitialPrecompiledFilePath = APPROOT.'datamodels/2.x/'.$aThemeParameters['precompiled_stylesheet']; - if ($bHasCompiled && is_file($sInitialPrecompiledFilePath)) + if ($bHasCompiled) { - SetupLog::Info("Replacing precompiled file $sInitialPrecompiledFilePath for theme $sThemeId for next setup."); - copy($sThemeDir.'/main.css', $sInitialPrecompiledFilePath); + SetupLog::Info("Replacing theme '$sThemeId' precompiled file in file $sPostCompilationLatestPrecompiledFile for next setup."); + copy($sThemeDir.'/main.css', $sPostCompilationLatestPrecompiledFile); + } + } + $this->Log(sprintf('Themes compilation took: %.3f ms for %d themes.', (microtime(true) - $fStart)*1000.0, count($aThemes))); + } + + /** + * Choose between precompiled files declared in datamodel XMLs or latest precompiled files generated after latest setup. + * @param $sTempTargetDir + * @param $sPrecompiledFileUri + * @param $sPostCompilationLatestPrecompiledFile + * @param $sThemeId + * + * @return string : file path of latest precompiled file to use for setup + */ + public function UseLatestPrecompiledFile($sTempTargetDir, $sPrecompiledFileUri, $sPostCompilationLatestPrecompiledFile, $sThemeId) : ?string { + $bDataXmlPrecompiledFileExists = false; + clearstatcache(); + if (!empty($sPrecompiledFileUri)){ + $sDataXmlProvidedPrecompiledFile = $sTempTargetDir . DIRECTORY_SEPARATOR . $sPrecompiledFileUri; + $bDataXmlPrecompiledFileExists = file_exists($sDataXmlProvidedPrecompiledFile) ; + if (!$bDataXmlPrecompiledFileExists){ + SetupLog::Warning("Missing defined theme '$sThemeId' precompiled file configured with: '$sPrecompiledFileUri'"); + } else { + $sSourceDir = APPROOT . utils::GetConfig()->Get('source_dir'); + + $aDirToCheck = [ + $sSourceDir, + APPROOT . DIRECTORY_SEPARATOR . 'extensions/' + ]; + + $iDataXmlFileLastModified = 0; + foreach ($aDirToCheck as $sDir){ + $sCurrentFile = $sDir . DIRECTORY_SEPARATOR . $sPrecompiledFileUri; + if (is_file($sCurrentFile)){ + $iDataXmlFileLastModified = max($iDataXmlFileLastModified, @filemtime($sCurrentFile)); + } + } + + if ($iDataXmlFileLastModified == 0){ + SetupLog::Warning("Missing defined theme '$sThemeId' precompiled file in datamodels/X.x or extensions directory configured with: '$sPrecompiledFileUri'. That should not happen!"); + $bDataXmlPrecompiledFileExists = false; + } } } - $this->Log(sprintf('Themes compilation took: %.3f ms for %d themes.', (microtime(true) - $fStart)*1000.0, count($aThemes))); + + $bPostCompilationPrecompiledFileExists = file_exists($sPostCompilationLatestPrecompiledFile); + + if (!$bDataXmlPrecompiledFileExists && !$bPostCompilationPrecompiledFileExists){ + return null; + } + + if (!$bDataXmlPrecompiledFileExists){ + $sPrecompiledFileToUse = $sPostCompilationLatestPrecompiledFile; + } else if (!$bPostCompilationPrecompiledFileExists){ + $sPrecompiledFileToUse = $sDataXmlProvidedPrecompiledFile; + } else{ + $iPostCompilationFileLastModified = @filemtime($sPostCompilationLatestPrecompiledFile); + SetupLog::Debug("Theme '$sThemeId' check mtime between data XML file " . $iDataXmlFileLastModified . " and latest postcompilation file: " . $iPostCompilationFileLastModified); + + $sPrecompiledFileToUse = $iDataXmlFileLastModified > $iPostCompilationFileLastModified ? $sDataXmlProvidedPrecompiledFile : $sPostCompilationLatestPrecompiledFile; + } + + SetupLog::Info("For theme '$sThemeId' precompiled file used: '$sPrecompiledFileToUse'"); + return $sPrecompiledFileToUse; } /** diff --git a/test/setup/MFCompilerTest.php b/test/setup/MFCompilerTest.php new file mode 100644 index 000000000..ac16ba1f3 --- /dev/null +++ b/test/setup/MFCompilerTest.php @@ -0,0 +1,131 @@ +oMFCompiler = new MFCompiler($this->createMock(\ModelFactory::class), ''); + } + + public static function Init(){ + if (!is_null(self::$aFoldersToCleanup)){ + return; + } + clearstatcache(); + $sPrefix = 'scsstest_'; + $sAppRootForProvider = dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR; + $sTempTargetDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'UseLatestPrecompiledFileProvider'; + $sExtensionTargetDir = $sAppRootForProvider . 'extensions/UseLatestPrecompiledFileProvider'; + $sSourceDir = $sAppRootForProvider . 'datamodels' . DIRECTORY_SEPARATOR . '2.x'; + $sDatamodel2xTargetDir = $sSourceDir . DIRECTORY_SEPARATOR . '/UseLatestPrecompiledFileProvider'; + + mkdir($sTempTargetDir); + mkdir($sExtensionTargetDir); + mkdir($sDatamodel2xTargetDir); + + self::$aFoldersToCleanup = [ $sTempTargetDir, $sExtensionTargetDir, $sDatamodel2xTargetDir ]; + + self::$aRessources['sPostCompilation1'] = tempnam($sTempTargetDir, $sPrefix); + sleep(1); + + //datamodel XML file in extension folder + self::$aRessources['sPrecompiledInExtensionFile1'] = tempnam($sExtensionTargetDir, $sPrefix); + self::$aRessources['sPrecompiledInExtensionFileUri1'] = "UseLatestPrecompiledFileProvider" . DIRECTORY_SEPARATOR . basename(self::$aRessources['sPrecompiledInExtensionFile1']); + + //datamodel XML file in source dir /datamodels/2.x folder + self::$aRessources['sPrecompiledInDataModelXXFile1'] = tempnam($sDatamodel2xTargetDir, $sPrefix); + self::$aRessources['sPrecompiledInDataModelXXFileUri1'] = "UseLatestPrecompiledFileProvider" . DIRECTORY_SEPARATOR . basename(self::$aRessources['sPrecompiledInDataModelXXFile1']); + + sleep(1); + + + //generate ressources from a previous setup: called postcompiled + self::$aRessources['sPostCompilation2'] = tempnam($sTempTargetDir, $sPrefix); + sleep(1); + + //simulate copy of /data/models.2.x or extensions ressources during setup in a temp directory + self::$aRessources['sCopiedExtensionFile1'] = $sTempTargetDir . DIRECTORY_SEPARATOR . basename(self::$aRessources['sPrecompiledInExtensionFile1']); + copy(self::$aRessources['sPrecompiledInExtensionFile1'], self::$aRessources['sCopiedExtensionFile1']); + + self::$aRessources['sCopiedDataModelXXFile1'] = $sTempTargetDir . DIRECTORY_SEPARATOR . basename(self::$aRessources['sPrecompiledInDataModelXXFile1']); + copy(self::$aRessources['sPrecompiledInDataModelXXFile1'], self::$aRessources['sCopiedDataModelXXFile1']); + + self::$aRessources['sMissingFile'] = tempnam($sTempTargetDir, $sPrefix); + unlink(self::$aRessources['sMissingFile']); + + /*foreach (self::$aRessources as $sKey => $sRessource){ + if (is_file($sRessource)) { + var_dump("$sKey $sRessource:" . filemtime($sRessource)); + } + }*/ + } + + public static function tearDownAfterClass() + { + if (is_null(self::$aFoldersToCleanup)){ + return; + } + + foreach (self::$aFoldersToCleanup as $sFolder){ + if (is_dir($sFolder)){ + foreach (glob("$sFolder/**") as $sFile){ + unlink($sFile); + } + rmdir($sFolder); + } + } + } + + /** + * @dataProvider UseLatestPrecompiledFileProvider + */ + public function testUseLatestPrecompiledFile(string $sTempTargetDir, string $sPrecompiledFileUri, string $sPostCompilationLatestPrecompiledFile, string $sThemeDir, $sExpectedReturn){ + $sRes = $this->oMFCompiler->UseLatestPrecompiledFile($sTempTargetDir, $sPrecompiledFileUri, $sPostCompilationLatestPrecompiledFile, $sThemeDir); + $this->assertEquals($sExpectedReturn, $sRes); + } + + public function UseLatestPrecompiledFileProvider(){ + self::init(); + return [ + 'no precompiled file configured in precompiled_stylesheet XM section' => $this->BuildProviderUseCaseArray('', self::$aRessources['sPostCompilation1'], self::$aRessources['sPostCompilation1']), + 'missing precompiled file in precompiled_stylesheet section' => $this->BuildProviderUseCaseArray(self::$aRessources['sMissingFile'], self::$aRessources['sPostCompilation1'], self::$aRessources['sPostCompilation1'] ), + 'no precompiled file generated in previous setup in /data/precompiled_styles' => $this->BuildProviderUseCaseArray(self::$aRessources['sPrecompiledInExtensionFileUri1'], self::$aRessources['sMissingFile'], self::$aRessources['sCopiedExtensionFile1'] ), + '(extensions) XML precompiled_stylesheet file older than last post setup generated file in /data/precompiled_styles' => $this->BuildProviderUseCaseArray(self::$aRessources['sPrecompiledInExtensionFileUri1'], self::$aRessources['sPostCompilation2'], self::$aRessources['sPostCompilation2'] ), + 'last post setup generated file in /data/precompiled_styles older than (extensions) XML precompiled_stylesheet file' => $this->BuildProviderUseCaseArray(self::$aRessources['sPrecompiledInExtensionFileUri1'], self::$aRessources['sPostCompilation1'], self::$aRessources['sCopiedExtensionFile1'] ), + '(datamodels/N.x) XML precompiled_stylesheet file older than last post setup generated file in /data/precompiled_styles' => $this->BuildProviderUseCaseArray(self::$aRessources['sPrecompiledInDataModelXXFileUri1'], self::$aRessources['sPostCompilation2'], self::$aRessources['sPostCompilation2'] ), + '(datamodels/N.x) last post setup generated file in /data/precompiled_styles older than (extensions) XML precompiled_stylesheet file' => $this->BuildProviderUseCaseArray(self::$aRessources['sPrecompiledInDataModelXXFileUri1'], self::$aRessources['sPostCompilation1'], self::$aRessources['sCopiedDataModelXXFile1'] ), + ]; + } + + private function BuildProviderUseCaseArray(string $sPrecompiledFileUri, string $sPostCompilationLatestPrecompiledFile, $sExpectedReturn) : array{ + return [ + "sTempTargetDir" => sys_get_temp_dir(), + "sPrecompiledFileUri" => $sPrecompiledFileUri, + "sPostCompilationLatestPrecompiledFile" => $sPostCompilationLatestPrecompiledFile, + "sThemeDir" => "test", + "sExpectedReturn" => $sExpectedReturn + ]; + } + + +}