Compare commits

...

7 Commits

17 changed files with 664 additions and 55 deletions

View File

@@ -0,0 +1,23 @@
<?php
/**
* Login page extensibility
*
* @api
* @package UIExtensibilityAPI
* @since 3.x.x
*/
interface iTokenLoginUIExtension
{
/**
* @return array
* @api
*/
public function GetTokenInfo(): array;
/**
* @return array
* @api
*/
public function GetUserLogin(array $aTokenInfo): string;
}

View File

@@ -9,7 +9,7 @@ use Combodo\iTop\Application\Helper\Session;
* @license http://opensource.org/licenses/AGPL-3.0
*/
class LoginBasic extends AbstractLoginFSMExtension
class LoginBasic extends AbstractLoginFSMExtension implements iTokenLoginUIExtension
{
/**
* Return the list of supported login modes for this plugin
@@ -120,4 +120,20 @@ class LoginBasic extends AbstractLoginFSMExtension
}
return [$sAuthUser, $sAuthPwd];
}
public function GetTokenInfo(): array
{
return $this->GetAuthUserAndPassword();
}
public function GetUserLogin(array $aTokenInfo): string
{
$sLogin = $aTokenInfo[0];
$sLoginMode = 'basic';
if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) {
return $sLogin;
}
throw new Exception("Cannot CheckCredentials user login ($sLogin) with ($sLoginMode) mode");
}
}

View File

@@ -12,7 +12,7 @@ use Combodo\iTop\Application\Helper\Session;
*
* @since 2.7.0
*/
class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension, iTokenLoginUIExtension
{
private $bForceFormOnError = false;
@@ -32,8 +32,7 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
protected function OnReadCredentials(&$iErrorCode)
{
if (!Session::IsSet('login_mode') || Session::Get('login_mode') == 'form') {
$sAuthUser = utils::ReadPostedParam('auth_user', '', 'raw_data');
$sAuthPwd = utils::ReadPostedParam('auth_pwd', null, 'raw_data');
list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo();
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
@@ -65,8 +64,7 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
protected function OnCheckCredentials(&$iErrorCode)
{
if (Session::Get('login_mode') == 'form') {
$sAuthUser = utils::ReadPostedParam('auth_user', '', 'raw_data');
$sAuthPwd = utils::ReadPostedParam('auth_pwd', null, 'raw_data');
list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo();
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal')) {
$iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS;
return LoginWebPage::LOGIN_FSM_ERROR;
@@ -145,4 +143,23 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
return $oLoginContext;
}
public function GetTokenInfo(): array
{
$sAuthUser = utils::ReadPostedParam('auth_user', '', 'raw_data');
$sAuthPwd = utils::ReadPostedParam('auth_pwd', null, 'raw_data');
return [$sAuthUser, $sAuthPwd];
}
public function GetUserLogin(array $aTokenInfo): string
{
$sLogin = $aTokenInfo[0];
$sLoginMode = 'form';
if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) {
return $sLogin;
}
throw new Exception("Cannot CheckCredentials user login ($sLogin) with ($sLoginMode) mode");
}
}

View File

@@ -9,7 +9,7 @@ use Combodo\iTop\Application\Helper\Session;
* @license http://opensource.org/licenses/AGPL-3.0
*/
class LoginURL extends AbstractLoginFSMExtension
class LoginURL extends AbstractLoginFSMExtension implements iTokenLoginUIExtension
{
/**
* @var bool
@@ -29,9 +29,8 @@ class LoginURL extends AbstractLoginFSMExtension
protected function OnModeDetection(&$iErrorCode)
{
if (!Session::IsSet('login_mode') && !$this->bErrorOccurred) {
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data');
if (!empty($sAuthUser) && !empty($sAuthPwd)) {
list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo();
{
Session::Set('login_mode', 'url');
}
}
@@ -49,8 +48,7 @@ class LoginURL extends AbstractLoginFSMExtension
protected function OnCheckCredentials(&$iErrorCode)
{
if (Session::Get('login_mode') == 'url') {
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data');
list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo();
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal')) {
$iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS;
return LoginWebPage::LOGIN_FSM_ERROR;
@@ -84,4 +82,22 @@ class LoginURL extends AbstractLoginFSMExtension
}
return LoginWebPage::LOGIN_FSM_CONTINUE;
}
public function GetTokenInfo(): array
{
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data');
return [$sAuthUser, $sAuthPwd];
}
public function GetUserLogin(array $aTokenInfo): string
{
$sLogin = $aTokenInfo[0];
$sLoginMode = 'url';
if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) {
return $sLogin;
}
throw new Exception("Cannot CheckCredentials user login ($sLogin) with ($sLoginMode) mode");
}
}

View File

@@ -530,6 +530,23 @@ class LoginWebPage extends NiceWebPage
return $aPlugins;
}
public static function GetCurrentLoginPlugin(string $sCurrentLoginMode): iLoginExtension
{
/** @var iLoginExtension $oLoginExtensionInstance */
foreach (MetaModel::EnumPlugins('iLoginFSMExtension') as $oLoginExtensionInstance) {
$aLoginModes = $oLoginExtensionInstance->ListSupportedLoginModes();
$aLoginModes = (is_array($aLoginModes) ? $aLoginModes : []);
foreach ($aLoginModes as $sLoginMode) {
// Keep only the plugins for the current login mode + before + after
if ($sLoginMode == $sCurrentLoginMode) {
return $oLoginExtensionInstance;
}
}
}
throw new \Exception("should not happen");
}
/**
* Advance Login Finite State Machine to the next step
*

View File

@@ -3226,4 +3226,43 @@ TXT
return $aTrace;
}
/**
* @author Ain Tohvri <https://mstdn.social/@tekkie>
*
* @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);
}
}

View File

@@ -14,7 +14,10 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
throw new RuntimeException($err);
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';

View File

@@ -3150,6 +3150,7 @@ return array(
'iRestServiceProvider' => $baseDir . '/application/applicationextension.inc.php',
'iScheduledProcess' => $baseDir . '/core/backgroundprocess.inc.php',
'iSelfRegister' => $baseDir . '/core/userrights.class.inc.php',
'iTokenLoginUIExtension' => $baseDir . '/application/applicationextension/login/iTokenLoginUIExtension.php',
'iTopConfigParser' => $baseDir . '/core/iTopConfigParser.php',
'iTopMutex' => $baseDir . '/core/mutex.class.inc.php',
'iTopOwnershipLock' => $baseDir . '/core/ownershiplock.class.inc.php',

View File

@@ -56,7 +56,7 @@ return array(
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'Pelago\\Emogrifier\\' => array($vendorDir . '/pelago/emogrifier/src'),
'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-google/src', $vendorDir . '/league/oauth2-client/src'),
'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src', $vendorDir . '/league/oauth2-google/src'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),

View File

@@ -314,8 +314,8 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
),
'League\\OAuth2\\Client\\' =>
array (
0 => __DIR__ . '/..' . '/league/oauth2-google/src',
1 => __DIR__ . '/..' . '/league/oauth2-client/src',
0 => __DIR__ . '/..' . '/league/oauth2-client/src',
1 => __DIR__ . '/..' . '/league/oauth2-google/src',
),
'GuzzleHttp\\Psr7\\' =>
array (
@@ -3509,6 +3509,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'iRestServiceProvider' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iScheduledProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
'iSelfRegister' => __DIR__ . '/../..' . '/core/userrights.class.inc.php',
'iTokenLoginUIExtension' => __DIR__ . '/../..' . '/application/applicationextension/login/iTokenLoginUIExtension.php',
'iTopConfigParser' => __DIR__ . '/../..' . '/core/iTopConfigParser.php',
'iTopMutex' => __DIR__ . '/../..' . '/core/mutex.class.inc.php',
'iTopOwnershipLock' => __DIR__ . '/../..' . '/core/ownershiplock.class.inc.php',

View File

@@ -36,7 +36,8 @@ if ($issues) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
/**

View File

@@ -0,0 +1,287 @@
<?php
namespace Combodo\iTop\Test\UnitTest\Webservices;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Exception;
use iTopMutex;
use MetaModel;
use utils;
/**
* @group itopRequestMgmt
* @group restApi
* @group defaultProfiles
*/
class CronTest extends ItopDataTestCase
{
public const USE_TRANSACTION = false;
public const CREATE_TEST_ORG = false;
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).
*/
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
}
/**
* This method is called after the last test of this test class is run (in the current process).
*/
public static function tearDownAfterClass(): void
{
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');
$this->CreateTestOrganization();
}
/**
* @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',
'auth_user' => static::$sLogin,
'auth_pwd' => static::$sPassword,
'json_data' => '{"operation": "list_operations"}',
];
$sJSONResult = $this->CallItopUri("/webservices/rest.php", $aPostFields);
$this->assertEquals($this->GetExpectedRestResponse(), $sJSONResult);
}
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 $iProfileId) {
if (is_null($oUser)) {
$oUser = $this->CreateContactlessUser(static::$sLogin, $iProfileId, static::$sPassword);
} else {
$this->AddProfileToUser($oUser, $iProfileId);
}
$oUser->DBWrite();
}
return $oUser->GetKey();
}
return null;
}
private function GetExpectedRestResponse(): string
{
return <<<JSON
{"code":0,"message":"Operations: 7","version":"1.3","operations":[{"verb":"core\/create","description":"Create an object","extension":"CoreServices"},{"verb":"core\/update","description":"Update an object","extension":"CoreServices"},{"verb":"core\/apply_stimulus","description":"Apply a stimulus to change the state of an object","extension":"CoreServices"},{"verb":"core\/get","description":"Search for objects","extension":"CoreServices"},{"verb":"core\/delete","description":"Delete objects","extension":"CoreServices"},{"verb":"core\/get_related","description":"Get related objects through the specified relation","extension":"CoreServices"},{"verb":"core\/check_credentials","description":"Check user credentials","extension":"CoreServices"}]}
JSON;
}
private function GetExpectedCronResponse(): string
{
return '{"message":"OK"}';
}
private function CheckLogFileIsGeneratedAndGetFullPath(string $sLogFileName): string
{
$sLogFile = APPROOT."log/$sLogFileName";
$this->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@");
}
}

View File

@@ -457,23 +457,40 @@ 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'
//
$sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data');
$sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data');
if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd)) {
UserRights::Login($sAuthUser); // Login & set the user's language
$sTokenInfo = utils::ReadParam('auth_info', null, true, 'raw_data');
$sLoginMode = utils::ReadParam('login_mode', null, true, 'raw_data');
if (is_null($sLoginMode) || is_null($sTokenInfo)) {
$sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data');
$sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data');
if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd)) {
UserRights::Login($sAuthUser); // Login & set the user's language
} else {
$oP->p("Access wrong credentials ('$sAuthUser')");
$oP->output();
exit(EXIT_CODE_ERROR);
}
} else {
$oP->p("Access wrong credentials ('$sAuthUser')");
$oP->output();
exit(EXIT_CODE_ERROR);
$oLoginFSMExtensionInstance = LoginWebPage::GetCurrentLoginPlugin($sLoginMode);
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("Access wrong credentials via current login mode $sLoginMode");
$oP->output();
exit(EXIT_CODE_ERROR);
}
}
} else {
require_once(APPROOT.'/application/loginwebpage.class.inc.php');

View File

@@ -0,0 +1,65 @@
<?php
require_once(__DIR__.'/../approot.inc.php');
require_once(APPROOT.'/application/application.inc.php');
require_once(APPROOT.'/application/startup.inc.php');
const ERROR_ALREADY_RUNNING = "error_already_running";
const RUNNING = "running";
const STOPPED = "stopped";
const ERROR = "error";
try {
$oCtx = new ContextTag(ContextTag::TAG_CRON);
LoginWebPage::ResetSession();
$iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN);
if ($iRet != LoginWebPage::EXIT_CODE_OK) {
throw new Exception("Unknown authentication error (retCode=$iRet)", RestResult::UNAUTHORIZED);
}
$sLogFilename = ReadParam("cron_log_file", "cron.log");
$sStatus = RUNNING;
$sMsg = "";
$sLogFile = APPROOT."log/$sLogFilename";
$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 {
$sMsg = "$sContent";
$sStatus = STOPPED;
}
}
http_response_code(200);
$oP = new JsonPage();
$oP->add_header('Access-Control-Allow-Origin: *');
$oP->SetData(["status" => $sStatus, 'message' => $sMsg]);
$oP->SetOutputDataOnly(true);
$oP->Output();
} 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: *');
$oP->SetData(["message" => $e->getMessage()]);
$oP->SetOutputDataOnly(true);
$oP->Output();
}
function ReadParam($sParam, $sDefaultValue = null, $sSanitizationFilter = utils::ENUM_SANITIZATION_FILTER_RAW_DATA)
{
$sValue = utils::ReadParam($sParam, null, true, $sSanitizationFilter);
if (is_null($sValue)) {
$sValue = utils::ReadPostedParam($sParam, $sDefaultValue, $sSanitizationFilter);
}
return trim($sValue);
}

View File

@@ -0,0 +1,112 @@
<?php
use Hybridauth\Storage\Session;
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();
$iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN);
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) {
throw new \Exception("cannot call cron asynchronously via current login mode $sCurrentLoginMode");
}
$aCronValues = [];
foreach ([ 'verbose', 'debug'] as $sParam) {
$value = ReadParam($sParam, false);
$aCronValues[] = "--$sParam=".escapeshellarg($value);
}
/** @var iTokenLoginUIExtension $oLoginFSMExtensionInstance */
$aTokenInfo = $oLoginFSMExtensionInstance->GetTokenInfo();
$sTokenInfo = base64_encode(json_encode($aTokenInfo));
$aCronValues[] = "--login_mode=".escapeshellarg($sCurrentLoginMode);
$sLogFilename = ReadParam("cron_log_file", "cron.log");
$sLogFile = APPROOT."log/$sLogFilename";
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");
}
$aCronValues[] = "--auth_info=".escapeshellarg($sTokenInfo);
$sCli = GetCliCommand($sPHPExec, $sLogFile, $aCronValues);
$process = popen($sCli, 'r');
$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);
$oP = new JsonPage();
$oP->add_header('Access-Control-Allow-Origin: *');
$oP->SetData(["message" => "OK"]);
$oP->SetOutputDataOnly(true);
$oP->Output();
} catch (Exception $e) {
\IssueLog::Error("Cannot run cron", null, ['msg' => $e->getMessage(), 'stack' => $e->getTraceAsString()]);
http_response_code(500);
$oP = new JsonPage();
$oP->add_header('Access-Control-Allow-Origin: *');
$oP->SetData(["message" => $e->getMessage()]);
$oP->SetOutputDataOnly(true);
$oP->Output();
}
function ReadParam($sParam, $sDefaultValue = null, $sSanitizationFilter = utils::ENUM_SANITIZATION_FILTER_RAW_DATA)
{
$sValue = utils::ReadParam($sParam, null, true, $sSanitizationFilter);
if (is_null($sValue)) {
$sValue = utils::ReadPostedParam($sParam, $sDefaultValue, $sSanitizationFilter);
}
return trim($sValue);
}