diff --git a/application/applicationextension/login/iTokenLoginUIExtension.php b/application/applicationextension/login/iTokenLoginUIExtension.php index adfc82fd0..16fd0c699 100644 --- a/application/applicationextension/login/iTokenLoginUIExtension.php +++ b/application/applicationextension/login/iTokenLoginUIExtension.php @@ -9,15 +9,15 @@ */ interface iTokenLoginUIExtension { - /** - * @return array - * @api - */ - public function GetTokenInfo() : array; + /** + * @return array + * @api + */ + public function GetTokenInfo(): array; /** * @return array * @api */ - public function GetUserLogin(array $aTokenInfo) : string; -} \ No newline at end of file + public function GetUserLogin(array $aTokenInfo): string; +} diff --git a/application/loginbasic.class.inc.php b/application/loginbasic.class.inc.php index 06d1cc50d..71b45fae5 100644 --- a/application/loginbasic.class.inc.php +++ b/application/loginbasic.class.inc.php @@ -130,8 +130,7 @@ class LoginBasic extends AbstractLoginFSMExtension implements iTokenLoginUIExten { $sLogin = $aTokenInfo[0]; $sLoginMode = 'basic'; - if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) - { + if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) { return $sLogin; } diff --git a/application/loginform.class.inc.php b/application/loginform.class.inc.php index 95f168d41..30cd86710 100644 --- a/application/loginform.class.inc.php +++ b/application/loginform.class.inc.php @@ -33,10 +33,8 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension, { if (!Session::IsSet('login_mode') || Session::Get('login_mode') == 'form') { list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo(); - if ($this->bForceFormOnError || empty($sAuthUser) || empty($sAuthPwd)) - { - if (array_key_exists('HTTP_X_COMBODO_AJAX', $_SERVER)) - { + if ($this->bForceFormOnError || empty($sAuthUser) || empty($sAuthPwd)) { + if (array_key_exists('HTTP_X_COMBODO_AJAX', $_SERVER)) { // X-Combodo-Ajax is a special header automatically added to all ajax requests // Let's reply that we're currently logged-out header('HTTP/1.0 401 Unauthorized'); @@ -65,11 +63,9 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension, */ protected function OnCheckCredentials(&$iErrorCode) { - if (Session::Get('login_mode') == 'form') - { + if (Session::Get('login_mode') == 'form') { list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo(); - if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal')) - { + if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal')) { $iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS; return LoginWebPage::LOGIN_FSM_ERROR; } @@ -159,8 +155,8 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension, { $sLogin = $aTokenInfo[0]; $sLoginMode = 'form'; - if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) - { + + if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) { return $sLogin; } diff --git a/application/loginurl.class.inc.php b/application/loginurl.class.inc.php index 533ce9a9f..ef0258366 100644 --- a/application/loginurl.class.inc.php +++ b/application/loginurl.class.inc.php @@ -28,8 +28,7 @@ class LoginURL extends AbstractLoginFSMExtension implements iTokenLoginUIExtensi protected function OnModeDetection(&$iErrorCode) { - if (!Session::IsSet('login_mode') && !$this->bErrorOccurred) - { + if (!Session::IsSet('login_mode') && !$this->bErrorOccurred) { list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo(); { Session::Set('login_mode', 'url'); @@ -48,11 +47,9 @@ class LoginURL extends AbstractLoginFSMExtension implements iTokenLoginUIExtensi protected function OnCheckCredentials(&$iErrorCode) { - if (Session::Get('login_mode') == 'url') - { + if (Session::Get('login_mode') == 'url') { list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo(); - if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal')) - { + if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal')) { $iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS; return LoginWebPage::LOGIN_FSM_ERROR; } @@ -97,8 +94,7 @@ class LoginURL extends AbstractLoginFSMExtension implements iTokenLoginUIExtensi { $sLogin = $aTokenInfo[0]; $sLoginMode = 'url'; - if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) - { + if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) { return $sLogin; } diff --git a/application/loginwebpage.class.inc.php b/application/loginwebpage.class.inc.php index ffb153736..97b3232d3 100644 --- a/application/loginwebpage.class.inc.php +++ b/application/loginwebpage.class.inc.php @@ -530,18 +530,15 @@ class LoginWebPage extends NiceWebPage return $aPlugins; } - public static function GetCurrentLoginPlugin(string $sCurrentLoginMode) : iLoginExtension + public static function GetCurrentLoginPlugin(string $sCurrentLoginMode): iLoginExtension { /** @var iLoginExtension $oLoginExtensionInstance */ - foreach (MetaModel::EnumPlugins('iLoginFSMExtension') as $oLoginExtensionInstance) - { + foreach (MetaModel::EnumPlugins('iLoginFSMExtension') as $oLoginExtensionInstance) { $aLoginModes = $oLoginExtensionInstance->ListSupportedLoginModes(); - $aLoginModes = (is_array($aLoginModes) ? $aLoginModes : array()); - foreach ($aLoginModes as $sLoginMode) - { + $aLoginModes = (is_array($aLoginModes) ? $aLoginModes : []); + foreach ($aLoginModes as $sLoginMode) { // Keep only the plugins for the current login mode + before + after - if ($sLoginMode == $sCurrentLoginMode) - { + if ($sLoginMode == $sCurrentLoginMode) { return $oLoginExtensionInstance; } } diff --git a/application/utils.inc.php b/application/utils.inc.php index c360a1d03..1e44344f4 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -3226,4 +3226,43 @@ TXT return $aTrace; } + + /** + * @author Ain Tohvri + * + * @since ??? + */ + public static function ReadTail($sFilename, $iLines = 1): array + { + $handle = fopen($sFilename, "r"); + if (false === $handle) { + throw new \Exception("Cannot read file $sFilename"); + } + + $iLineCounter = $iLines; + $iPos = -2; + $bBeginning = false; + $aLines = []; + while ($iLineCounter > 0) { + $sChar = " "; + while ($sChar != "\n") { + if (fseek($handle, $iPos, SEEK_END) == -1) { + $bBeginning = true; + break; + } + $sChar = fgetc($handle); + $iPos--; + } + $iLineCounter--; + if ($bBeginning) { + rewind($handle); + } + $aLines[$iLines - $iLineCounter - 1] = fgets($handle); + if ($bBeginning) { + break; + } + } + fclose($handle); + return array_reverse($aLines); + } } diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index 966eaae10..2665bfc75 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -976,7 +976,7 @@ abstract class ItopDataTestCase extends ItopTestCase protected function AssertLastErrorLogEntryContains(string $sNeedle, string $sMessage = ''): void { - $aLastLines = self::ReadTail(APPROOT.'/log/error.log'); + $aLastLines = Utils::ReadTail(APPROOT.'/log/error.log'); $this->assertStringContainsString($sNeedle, $aLastLines[0], $sMessage); } @@ -1470,4 +1470,21 @@ abstract class ItopDataTestCase extends ItopTestCase @chmod($sConfigPath, 0440); @unlink($this->sConfigTmpBackupFile); } + + protected function AddLoginModeAndSaveConfiguration($sLoginMode) + { + $aAllowedLoginTypes = $this->oiTopConfig->GetAllowedLoginTypes(); + if (!in_array($sLoginMode, $aAllowedLoginTypes)) { + $aAllowedLoginTypes[] = $sLoginMode; + $this->oiTopConfig->SetAllowedLoginTypes($aAllowedLoginTypes); + $this->SaveItopConfFile(); + } + } + + private function SaveItopConfFile() + { + @chmod($this->oiTopConfig->GetLoadedFile(), 0770); + $this->oiTopConfig->WriteToFile(); + @chmod($this->oiTopConfig->GetLoadedFile(), 0440); + } } diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php index 9507f4dfa..e5d5a30f8 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php @@ -15,6 +15,8 @@ use SetupUtils; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpKernel\KernelInterface; +use Utils; + use const DEBUG_BACKTRACE_IGNORE_ARGS; /** @@ -89,7 +91,7 @@ abstract class ItopTestCase extends KernelTestCase if (method_exists('utils', 'GetConfig')) { // Reset the config by forcing the load from disk - $oConfig = \utils::GetConfig(true); + $oConfig = utils::GetConfig(true); if (method_exists('MetaModel', 'SetConfig')) { \MetaModel::SetConfig($oConfig); } @@ -625,32 +627,7 @@ abstract class ItopTestCase extends KernelTestCase */ protected static function ReadTail($sFilename, $iLines = 1) { - $handle = fopen($sFilename, "r"); - $iLineCounter = $iLines; - $iPos = -2; - $bBeginning = false; - $aLines = []; - while ($iLineCounter > 0) { - $sChar = " "; - while ($sChar != "\n") { - if (fseek($handle, $iPos, SEEK_END) == -1) { - $bBeginning = true; - break; - } - $sChar = fgetc($handle); - $iPos--; - } - $iLineCounter--; - if ($bBeginning) { - rewind($handle); - } - $aLines[$iLines - $iLineCounter - 1] = fgets($handle); - if ($bBeginning) { - break; - } - } - fclose($handle); - return array_reverse($aLines); + return Utils::ReadTail($sFilename, $iLines); } /** diff --git a/tests/php-unit-tests/unitary-tests/webservices/CronTest.php b/tests/php-unit-tests/unitary-tests/webservices/CronTest.php index 989ab5556..165828d49 100644 --- a/tests/php-unit-tests/unitary-tests/webservices/CronTest.php +++ b/tests/php-unit-tests/unitary-tests/webservices/CronTest.php @@ -4,6 +4,7 @@ namespace Combodo\iTop\Test\UnitTest\Webservices; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use Exception; +use iTopMutex; use MetaModel; use utils; @@ -17,8 +18,8 @@ class CronTest extends ItopDataTestCase public const USE_TRANSACTION = false; public const CREATE_TEST_ORG = false; - private static $sLogin; - private static $sPassword = "Iuytrez9876543ç_è-("; + public static $sLogin; + public static $sPassword = "Iuytrez9876543ç_è-("; /** * This method is called before the first test of this test class is run (in the current process). @@ -36,20 +37,43 @@ class CronTest extends ItopDataTestCase parent::tearDownAfterClass(); } + public function ModeProvider() + { + $aModes = ['form', 'url', 'basic']; + $aUsecases = []; + foreach ($aModes as $sMode) { + $aUsecases[$sMode] = [$sMode]; + } + + return $aUsecases; + } + /** * @throws Exception */ protected function setUp(): void { parent::setUp(); + $this->BackupConfiguration(); - static::$sLogin = "rest-user-".date('dmYHis'); + static::$sLogin = "rest-user-";//.date('dmYHis'); $this->CreateTestOrganization(); } - public function testListOperationsAndJSONPCallback() + /** + * @throws Exception + */ + protected function tearDown(): void { + parent::tearDown(); + $this->ReleaseCronIfBusy(); + + } + + public function testRestWithFormMode() + { + $this->AddLoginModeAndSaveConfiguration('form'); $this->CreateUserWithProfiles([self::$aURP_Profiles['Administrator'], self::$aURP_Profiles['REST Services User']]); $aPostFields = [ 'version' => '1.3', @@ -58,27 +82,206 @@ class CronTest extends ItopDataTestCase 'json_data' => '{"operation": "list_operations"}', ]; - // Test regular JSON result $sJSONResult = $this->CallItopUri("/webservices/rest.php", $aPostFields); - $sExpected = <<assertEquals($sExpected, $sJSONResult); + $this->assertEquals($this->GetExpectedRestResponse(), $sJSONResult); } - private function CreateUserWithProfiles(array $aProfileIds): void + public function testRestWithBasicMode() + { + $this->AddLoginModeAndSaveConfiguration('basic'); + $this->CreateUserWithProfiles([self::$aURP_Profiles['Administrator'], self::$aURP_Profiles['REST Services User']]); + $aPostFields = [ + 'version' => '1.3', + 'json_data' => '{"operation": "list_operations"}', + ]; + + $sToken = base64_encode(sprintf("%s:%s", static::$sLogin, static::$sPassword)); + $aCurlOptions = [ + CURLOPT_HTTPHEADER => ["Authorization: Basic $sToken"], + ]; + + // Test regular JSON result + $sJSONResult = $this->CallItopUri("/webservices/rest.php", $aPostFields, $aCurlOptions); + + $this->assertEquals($this->GetExpectedRestResponse(), $sJSONResult); + } + + public function testRestWithUrlMode() + { + $this->AddLoginModeAndSaveConfiguration('url'); + $this->CreateUserWithProfiles([self::$aURP_Profiles['Administrator'], self::$aURP_Profiles['REST Services User']]); + $aPostFields = [ + 'version' => '1.3', + 'json_data' => '{"operation": "list_operations"}', + ]; + + $aGetFields = [ + 'auth_user' => static::$sLogin, + 'auth_pwd' => static::$sPassword, + ]; + $sJSONResult = $this->CallItopUri("/webservices/rest.php?".http_build_query($aGetFields), $aPostFields); + + $this->assertEquals($this->GetExpectedRestResponse(), $sJSONResult); + } + + public function testLaunchCronWithFormModeFailWhenNotAdmin() + { + $this->ForceCronBusyError(); + + $this->AddLoginModeAndSaveConfiguration('form'); + $this->CreateUserWithProfiles([self::$aURP_Profiles['REST Services User']]); + + $sLogFileName = "crontest_".uniqid(); + $aPostFields = [ + 'version' => '1.3', + 'auth_user' => static::$sLogin, + 'auth_pwd' => static::$sPassword, + 'verbose' => 1, + 'debug' => 1, + 'cron_log_file' => $sLogFileName, + ]; + + $sJSONResult = $this->CallItopUri("/webservices/launch_cron_asynchronously.php", $aPostFields); + + $this->assertEquals($this->GetExpectedCronResponse(), $sJSONResult); + $sLogFile = $this->CheckLogFileIsGeneratedAndGetFullPath($sLogFileName); + $this->CheckAdminAccessIssueWithCron($sLogFile); + } + + public function testLaunchCronWithBasicModeFailWhenNotAdmin() + { + $this->ForceCronBusyError(); + + $this->AddLoginModeAndSaveConfiguration('basic'); + $this->CreateUserWithProfiles([self::$aURP_Profiles['REST Services User']]); + + $sLogFileName = "crontest_".uniqid(); + $aPostFields = [ + 'version' => '1.3', + 'verbose' => 1, + 'debug' => 1, + 'cron_log_file' => $sLogFileName, + ]; + + $sToken = base64_encode(sprintf("%s:%s", static::$sLogin, static::$sPassword)); + $aCurlOptions = [ + CURLOPT_HTTPHEADER => ["Authorization: Basic $sToken"], + ]; + + $sJSONResult = $this->CallItopUri("/webservices/launch_cron_asynchronously.php", $aPostFields, $aCurlOptions); + + $this->assertEquals($this->GetExpectedCronResponse(), $sJSONResult); + $sLogFile = $this->CheckLogFileIsGeneratedAndGetFullPath($sLogFileName); + $this->CheckAdminAccessIssueWithCron($sLogFile); + } + + public function testLaunchCronWithUrlModeFailWhenNotAdmin() + { + $this->ForceCronBusyError(); + + $this->AddLoginModeAndSaveConfiguration('url'); + $this->CreateUserWithProfiles([self::$aURP_Profiles['REST Services User']]); + + $sLogFileName = "crontest_".uniqid(); + $aPostFields = [ + 'version' => '1.3', + 'verbose' => 1, + 'debug' => 1, + 'cron_log_file' => $sLogFileName, + ]; + + $aGetFields = [ + 'auth_user' => static::$sLogin, + 'auth_pwd' => static::$sPassword, + ]; + + $sJSONResult = $this->CallItopUri("/webservices/launch_cron_asynchronously.php?".http_build_query($aGetFields), $aPostFields); + + $this->assertEquals($this->GetExpectedCronResponse(), $sJSONResult); + $sLogFile = $this->CheckLogFileIsGeneratedAndGetFullPath($sLogFileName); + $this->CheckAdminAccessIssueWithCron($sLogFile); + ; + } + + /** + * @dataProvider ModeProvider + */ + public function testGetUserLoginWithFormMode($sMode) + { + $this->AddLoginModeAndSaveConfiguration($sMode); + $this->CreateUserWithProfiles([self::$aURP_Profiles['Administrator']]); + + $oLoginMode = new \LoginForm(); + $sUserLogin = $oLoginMode->GetUserLogin([static::$sLogin, static::$sPassword]); + $this->assertEquals(static::$sLogin, $sUserLogin); + } + + private ?iTopMutex $oMutex = null; + private function ForceCronBusyError(): void + { + try { + $oMutex = new iTopMutex('cron'); + if ($oMutex->TryLock()) { + $this->oMutex = $oMutex; + } + } catch (Exception $e) { + } + } + + private function ReleaseCronIfBusy(): void + { + try { + if (! is_null($this->oMutex)) { + $this->oMutex->Unlock(); + } + } catch (Exception $e) { + } + } + + private function CreateUserWithProfiles(array $aProfileIds): ?string { if (count($aProfileIds) > 0) { $oUser = null; - foreach ($aProfileIds as $oProfileId) { + foreach ($aProfileIds as $iProfileId) { if (is_null($oUser)) { - $oUser = $this->CreateUser(static::$sLogin, $oProfileId, static::$sPassword); + $oUser = $this->CreateContactlessUser(static::$sLogin, $iProfileId, static::$sPassword); } else { - $this->AddProfileToUser($oUser, $oProfileId); + $this->AddProfileToUser($oUser, $iProfileId); } + $oUser->DBWrite(); } + + return $oUser->GetKey(); } + + return null; + } + + private function GetExpectedRestResponse(): string + { + return <<assertTrue(is_file($sLogFile)); + $this->aFileToClean[] = $sLogFile; + return $sLogFile; + } + + private function CheckAdminAccessIssueWithCron(string $sLogFile) + { + $aLines = Utils::ReadTail($sLogFile); + $sLastLine = array_shift($aLines); + $this->assertMatchesRegularExpression('/^Access restricted to administrators/', $sLastLine, "@$sLastLine@"); } } diff --git a/webservices/cron.php b/webservices/cron.php index 5133213e2..61c8d9475 100644 --- a/webservices/cron.php +++ b/webservices/cron.php @@ -457,12 +457,12 @@ if ($bIsModeCLI) { } try { - utils::UseParamFile(); - $bVerbose = utils::ReadParam('verbose', false, true /* Allow CLI */); $bDebug = utils::ReadParam('debug', false, true /* Allow CLI */); if ($bIsModeCLI) { + utils::UseParamFile(); + // Next steps: // specific arguments: 'csv file' // @@ -478,17 +478,16 @@ try { $oP->output(); exit(EXIT_CODE_ERROR); } - } else - { + } else { $oLoginFSMExtensionInstance = LoginWebPage::GetCurrentLoginPlugin($sLoginMode); - if ($oLoginFSMExtensionInstance instanceof iTokenLoginUIExtension){ + if ($oLoginFSMExtensionInstance instanceof iTokenLoginUIExtension) { $aTokenInfo = json_decode(base64_decode($sTokenInfo), true); /** @var iTokenLoginUIExtension $oLoginFSMExtensionInstance */ $sAuthUser = $oLoginFSMExtensionInstance->GetUserLogin($aTokenInfo); UserRights::Login($sAuthUser); // Login & set the user's language } else { - $oP->p("cannot call cron asynchronously via current login mode $sLoginMode"); + $oP->p("Access wrong credentials via current login mode $sLoginMode"); $oP->output(); exit(EXIT_CODE_ERROR); } diff --git a/webservices/cron_status.php b/webservices/cron_status.php index 649025294..e523e154d 100644 --- a/webservices/cron_status.php +++ b/webservices/cron_status.php @@ -11,38 +11,31 @@ const ERROR = "error"; try { $oCtx = new ContextTag(ContextTag::TAG_CRON); - LoginWebPage::ResetSession(true); + LoginWebPage::ResetSession(); $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); - if ($iRet != LoginWebPage::EXIT_CODE_OK){ + if ($iRet != LoginWebPage::EXIT_CODE_OK) { throw new Exception("Unknown authentication error (retCode=$iRet)", RestResult::UNAUTHORIZED); } $sLogFilename = ReadParam("cron_log_file", "cron.log"); - $sStatus = STOPPED; + $sStatus = RUNNING; $sMsg = ""; $sLogFile = APPROOT."log/$sLogFilename"; - if (is_file($sLogFile)) { - $sContent = exec("tail -n 1 $sLogFile"); - if (0 === strpos($sContent, 'Exiting: ')) { - exec("tail -n 2 $sLogFile", $aContent); - //var_dump($aContent); - $sContent = implode("\n", $aContent); - if (false !== strpos($sContent, 'Already running')) { - $sStatus = ERROR_ALREADY_RUNNING; - } else if (preg_match('/ERROR: (.*)\\n/', $sContent, $aMatches)) { - $sMsg = "$aMatches[1]"; - $sStatus = ERROR; - } else { - $sMsg = "$sContent"; - $sStatus = STOPPED; - } + + $aLines = Utils::ReadTail($sLogFile, 2); + $sLastLine = $aLines[1] ?? ''; + if (0 === strpos($sLastLine, 'Exiting: ')) { + $sContent = $aLines[0]; + if (false !== strpos($sContent, 'Already running')) { + $sStatus = ERROR_ALREADY_RUNNING; + } elseif (preg_match('/ERROR: (.*)\\n/', $sContent, $aMatches)) { + $sMsg = $aMatches[1]; + $sStatus = ERROR; } else { - $sStatus = RUNNING; + $sMsg = "$sContent"; + $sStatus = STOPPED; } - } else { - $sMsg = "missing $sLogFile"; - $sStatus = ERROR; } http_response_code(200); @@ -51,9 +44,8 @@ try { $oP->SetData(["status" => $sStatus, 'message' => $sMsg]); $oP->SetOutputDataOnly(true); $oP->Output(); -} -catch (Exception $e) { - \IssueLog::Error("Cannot cron status", null, ['msg' => $e->getMessage(), 'stack' => $e->getTraceAsString()]); +} catch (Exception $e) { + \IssueLog::Error("Cannot get cron status", null, ['msg' => $e->getMessage(), 'stack' => $e->getTraceAsString()]); http_response_code(500); $oP = new JsonPage(); $oP->add_header('Access-Control-Allow-Origin: *'); @@ -70,4 +62,4 @@ function ReadParam($sParam, $sDefaultValue = null, $sSanitizationFilter = utils: } return trim($sValue); -} \ No newline at end of file +} diff --git a/webservices/launch_cron_asynchronously.php b/webservices/launch_cron_asynchronously.php index bd7472f52..1f5eb8831 100644 --- a/webservices/launch_cron_asynchronously.php +++ b/webservices/launch_cron_asynchronously.php @@ -6,23 +6,48 @@ require_once(__DIR__.'/../approot.inc.php'); require_once(APPROOT.'/application/application.inc.php'); require_once(APPROOT.'/application/startup.inc.php'); +function GetCliCommand(string $sPHPExec, string $sLogFile, array $aCronValues): string +{ + $sCliParams = implode(" ", $aCronValues); + return sprintf("$sPHPExec %s/cron.php $sCliParams 2>&1 >>$sLogFile &", __DIR__); +} + +function IsErrorLine(string $sLine): bool +{ + if (preg_match('/^Access wrong credentials/', $sLine)) { + return true; + } + + if (preg_match('/^Access restricted/', $sLine)) { + return true; + } + + return false; + +} + +function IsCronStartingLine(string $sLine): bool +{ + return preg_match('/^Starting: /', $sLine); +} + try { $oCtx = new ContextTag(ContextTag::TAG_CRON); - LoginWebPage::ResetSession(true); + LoginWebPage::ResetSession(); $iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN); - if ($iRet != LoginWebPage::EXIT_CODE_OK){ + if ($iRet != LoginWebPage::EXIT_CODE_OK) { throw new Exception("Unknown authentication error (retCode=$iRet)", RestResult::UNAUTHORIZED); } $sCurrentLoginMode = \Combodo\iTop\Application\Helper\Session::Get('login_mode', ''); $oLoginFSMExtensionInstance = LoginWebPage::GetCurrentLoginPlugin($sCurrentLoginMode); - if (! $oLoginFSMExtensionInstance instanceof iTokenLoginUIExtension){ + if (! $oLoginFSMExtensionInstance instanceof iTokenLoginUIExtension) { throw new \Exception("cannot call cron asynchronously via current login mode $sCurrentLoginMode"); } $aCronValues = []; - foreach ([ 'status_only', 'verbose', 'debug'] as $sParam){ + foreach ([ 'verbose', 'debug'] as $sParam) { $value = ReadParam($sParam, false); $aCronValues[] = "--$sParam=".escapeshellarg($value); } @@ -30,41 +55,34 @@ try { /** @var iTokenLoginUIExtension $oLoginFSMExtensionInstance */ $aTokenInfo = $oLoginFSMExtensionInstance->GetTokenInfo(); $sTokenInfo = base64_encode(json_encode($aTokenInfo)); - $aCronValues[] = "--auth_info=".escapeshellarg($sTokenInfo); $aCronValues[] = "--login_mode=".escapeshellarg($sCurrentLoginMode); - $sCliParams=implode(" ", $aCronValues); - $sLogFilename = ReadParam("cron_log_file", "cron.log"); $sLogFile = APPROOT."log/$sLogFilename"; - touch($sLogFile); + if (! touch($sLogFile)) { + throw new \Exception("Cannot touch $sLogFile"); + } + $sPHPExec = trim(\MetaModel::GetConfig()->Get('php_path')); + $sCliForLogs = GetCliCommand($sPHPExec, $sLogFile, $aCronValues).PHP_EOL; + file_put_contents("$sLogFile", $sCliForLogs); + if (! is_file($sLogFile)) { + throw new \Exception("Cannot write in $sLogFile"); + } - if ($aCronValues['status_only']) { - //still synchronous - $sCli = sprintf("$sPHPExec %s/cron.php $sCliParams 2>&1 >>$sLogFile &", __DIR__); - file_put_contents($sLogFile, $sCli); - $process = popen($sCli, 'r'); - } else { - //asynchronous - $sCli = sprintf("\n $sPHPExec %s/cron.php $sCliParams", __DIR__); - $fp = fopen($sLogFile, 'a+'); - fwrite($fp, $sCli); + $aCronValues[] = "--auth_info=".escapeshellarg($sTokenInfo); + $sCli = GetCliCommand($sPHPExec, $sLogFile, $aCronValues); + $process = popen($sCli, 'r'); - $aDescriptorSpec = [ - 0 => ["pipe", "r"], // stdin - 1 => ["pipe", "w"], // stdout - ]; - $rProcess = proc_open($sCli, $aDescriptorSpec, $aPipes, __DIR__, null); - - $sStdOut = stream_get_contents($aPipes[1]); - fclose($aPipes[1]); - $iCode = proc_close($rProcess); - - fwrite($fp, $sStdOut); - fwrite($fp, "Exiting: ".time().' ('.date('Y-m-d H:i:s').')'); - fclose($fp); + $i = 0; + while ($aLines = Utils::ReadTail($sLogFile)) { + $sLastLine = array_shift($aLines); + if (IsErrorLine($sLastLine) || IsCronStartingLine($sLastLine)) { + //return answer once we are sure cron is starting or did not pass authentication + break; + } + usleep(100); } http_response_code(200); @@ -73,8 +91,7 @@ try { $oP->SetData(["message" => "OK"]); $oP->SetOutputDataOnly(true); $oP->Output(); -} -catch (Exception $e) { +} catch (Exception $e) { \IssueLog::Error("Cannot run cron", null, ['msg' => $e->getMessage(), 'stack' => $e->getTraceAsString()]); http_response_code(500); $oP = new JsonPage(); @@ -92,4 +109,4 @@ function ReadParam($sParam, $sDefaultValue = null, $sSanitizationFilter = utils: } return trim($sValue); -} \ No newline at end of file +}