mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
Merge remote-tracking branch 'origin/support/3.2' into develop
This commit is contained in:
@@ -185,6 +185,11 @@
|
||||
<decoration_classes>fas fa-cog</decoration_classes>
|
||||
</style>
|
||||
</menu>
|
||||
<menu id="ConfigEditor" xsi:type="TemplateMenuNode" _delta="define">
|
||||
<rank>10</rank>
|
||||
<parent>ConfigurationTools</parent>
|
||||
<template_file/>
|
||||
</menu>
|
||||
<menu id="DataSources" xsi:type="OQLMenuNode" _delta="define">
|
||||
<rank>20</rank>
|
||||
<parent>ConfigurationTools</parent>
|
||||
|
||||
@@ -60,6 +60,24 @@ class CoreCannotSaveObjectException extends CoreException
|
||||
return $sContent;
|
||||
}
|
||||
|
||||
public function getTextMessage()
|
||||
{
|
||||
$sTitle = Dict::S('UI:Error:SaveFailed');
|
||||
$sContent = utils::HtmlEntities($sTitle);
|
||||
|
||||
if (count($this->aIssues) == 1) {
|
||||
$sIssue = reset($this->aIssues);
|
||||
$sContent .= utils::HtmlEntities($sIssue);
|
||||
} else {
|
||||
foreach ($this->aIssues as $sError) {
|
||||
$sContent .= " ".utils::HtmlEntities($sError).", ";
|
||||
}
|
||||
}
|
||||
|
||||
return $sContent;
|
||||
}
|
||||
|
||||
|
||||
public function getIssues()
|
||||
{
|
||||
return $this->aIssues;
|
||||
|
||||
@@ -53,6 +53,9 @@ class LoginDefaultBefore extends AbstractLoginFSMExtension
|
||||
{
|
||||
// Force login mode
|
||||
LoginWebPage::SetLoginModeAndReload($sProposedLoginMode);
|
||||
} else if (empty($sProposedLoginMode)) {
|
||||
$sRawLoginMode = utils::ReadParam('login_mode', '', false, utils::ENUM_SANITIZATION_FILTER_RAW_DATA);
|
||||
IssueLog::Error("Authentication issue due to login_mode parameter sanitization ($sRawLoginMode). Please avoid special characters");
|
||||
}
|
||||
return LoginWebPage::LOGIN_FSM_CONTINUE;
|
||||
}
|
||||
|
||||
@@ -6873,6 +6873,9 @@ abstract class MetaModel
|
||||
/**
|
||||
* Instantiate an object already persisted to the Database.
|
||||
*
|
||||
* Note that LinkedSet attributes are not loaded.
|
||||
* DBObject::Reload() will be called when getting a LinkedSet attribute
|
||||
*
|
||||
* @api
|
||||
* @see MetaModel::GetObjectWithArchive to get object even if it's archived
|
||||
* @see utils::PushArchiveMode() to enable search on archived objects
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.2">
|
||||
<menus>
|
||||
<menu id="ConfigEditor" xsi:type="WebPageMenuNode" _delta="define">
|
||||
<menu id="ConfigFileEditor" xsi:type="WebPageMenuNode" _delta="define">
|
||||
<rank>10</rank>
|
||||
<parent>ConfigurationTools</parent>
|
||||
<parent>ConfigEditor</parent>
|
||||
<url>config.php</url>
|
||||
<enable_admin_only>1</enable_admin_only>
|
||||
</menu>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
Dict::Add('EN US', 'English', 'English', array(
|
||||
|
||||
'Menu:ConfigEditor' => 'General configuration',
|
||||
'Menu:ConfigFileEditor' => 'Plain text editor',
|
||||
'config-edit-title' => 'Configuration File Editor',
|
||||
'config-edit-intro' => 'Be very cautious when editing the configuration file.',
|
||||
'config-apply' => 'Apply',
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*
|
||||
*/
|
||||
Dict::Add('FR FR', 'French', 'Français', [
|
||||
'Menu:ConfigEditor' => 'Configuration générale',
|
||||
'Menu:ConfigFileEditor' => 'Éditeur de texte brut',
|
||||
'config-apply' => 'Enregistrer',
|
||||
'config-apply-title' => 'Enregistrer (Ctrl+S)',
|
||||
'config-cancel' => 'Annuler (restaurer)',
|
||||
|
||||
@@ -41,16 +41,11 @@ if (!defined('MODULESROOT'))
|
||||
require_once APPROOT.'/application/startup.inc.php';
|
||||
}
|
||||
|
||||
// Load cached env vars if the .env.local.php file exists
|
||||
// Run "composer dump-env prod" to create it (requires symfony/flex >=1.2)
|
||||
if (file_exists(dirname(__DIR__).'/.env.local.php')) {
|
||||
if (is_array($sEnv = @include dirname(__DIR__).'/.env.local.php')) {
|
||||
$_ENV += $sEnv;
|
||||
}
|
||||
} elseif (!class_exists(Dotenv::class)) {
|
||||
// Load cached env vars if the .env.local file exists
|
||||
if (!class_exists(Dotenv::class)) {
|
||||
throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
|
||||
} else {
|
||||
$sPath = dirname(__DIR__).'/.env';
|
||||
$sPath = file_exists(dirname(__DIR__).'/.env.local') ? dirname(__DIR__).'/.env.local' : dirname(__DIR__).'/.env';
|
||||
$oDotenv = new Dotenv();
|
||||
$oDotenv->usePutenv();
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1148,7 +1148,7 @@ class ObjectFormManager extends FormManager
|
||||
{
|
||||
$this->oObject->DBWrite();
|
||||
} catch (CoreCannotSaveObjectException $e) {
|
||||
throw new Exception($e->getHtmlMessage());
|
||||
throw new Exception($e->getTextMessage());
|
||||
} catch (InvalidExternalKeyValueException $e) {
|
||||
ExceptionLog::LogException($e, $e->getContextData());
|
||||
$bExceptionLogged = true;
|
||||
@@ -1224,7 +1224,7 @@ class ObjectFormManager extends FormManager
|
||||
}
|
||||
catch (CoreCannotSaveObjectException $e) {
|
||||
$aData['valid'] = false;
|
||||
$aData['messages']['error'] += array('_main' => array($e->getHtmlMessage()));
|
||||
$aData['messages']['error'] += array('_main' => array($e->getTextMessage()));
|
||||
if (false === $bExceptionLogged) {
|
||||
IssueLog::Error(__METHOD__.' at line '.__LINE__.' : '.$e->getMessage());
|
||||
}
|
||||
@@ -1232,7 +1232,7 @@ class ObjectFormManager extends FormManager
|
||||
catch (Exception $e) {
|
||||
$aData['valid'] = false;
|
||||
$aData['messages']['error'] += [
|
||||
'_main' => [ ($e instanceof CoreCannotSaveObjectException) ? $e->getHtmlMessage() : $e->getMessage()]
|
||||
'_main' => [ ($e instanceof CoreCannotSaveObjectException) ? $e->getTextMessage() : $e->getMessage()]
|
||||
];
|
||||
if (false === $bExceptionLogged) {
|
||||
IssueLog::Error(__METHOD__.' at line '.__LINE__.' : '.$e->getMessage());
|
||||
|
||||
@@ -13,6 +13,7 @@ use CorePortalInvalidActionRuleException;
|
||||
use DBObject;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use DBProperty;
|
||||
use DBSearch;
|
||||
use DeprecatedCallsLog;
|
||||
use DOMFormatException;
|
||||
@@ -20,8 +21,10 @@ use DOMNodeList;
|
||||
use Exception;
|
||||
use FieldExpression;
|
||||
use IssueLog;
|
||||
use MetaModel;
|
||||
use ModuleDesign;
|
||||
use ScalarExpression;
|
||||
use SimpleCrypt;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use TrueExpression;
|
||||
use UserRights;
|
||||
@@ -49,6 +52,8 @@ class ContextManipulatorHelper
|
||||
/** @var string DEFAULT_RULE_CALLBACK_OPEN */
|
||||
const DEFAULT_RULE_CALLBACK_OPEN = self::ENUM_RULE_CALLBACK_OPEN_VIEW;
|
||||
|
||||
const PRIVATE_KEY = 'portal-priv-key';
|
||||
|
||||
/** @var array $aRules */
|
||||
protected $aRules;
|
||||
/** @var \Symfony\Component\Routing\RouterInterface */
|
||||
@@ -524,8 +529,11 @@ class ContextManipulatorHelper
|
||||
*/
|
||||
public static function EncodeRulesToken($aTokenRules)
|
||||
{
|
||||
// Returning tokenised data
|
||||
return base64_encode(json_encode($aTokenRules));
|
||||
$aTokenRules['salt'] = base64_encode(random_bytes(8));
|
||||
|
||||
$sPPrivateKey = self::GetPrivateKey();
|
||||
$oCrypt = new SimpleCrypt(MetaModel::GetConfig()->GetEncryptionLibrary());
|
||||
return base64_encode($oCrypt->Encrypt($sPPrivateKey, json_encode($aTokenRules)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -549,9 +557,41 @@ class ContextManipulatorHelper
|
||||
* @param string $sToken
|
||||
*
|
||||
* @return array
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*/
|
||||
public static function DecodeRulesToken($sToken)
|
||||
{
|
||||
return json_decode(base64_decode($sToken), true);
|
||||
$sPrivateKey = self::GetPrivateKey();
|
||||
$oCrypt = new SimpleCrypt(MetaModel::GetConfig()->GetEncryptionLibrary());
|
||||
$sDecryptedToken = $oCrypt->Decrypt($sPrivateKey, base64_decode($sToken));
|
||||
|
||||
$aTokenRules = json_decode($sDecryptedToken, true);
|
||||
if (!is_array($aTokenRules))
|
||||
{
|
||||
throw new Exception('DecodeRulesToken not a proper json structure.');
|
||||
}
|
||||
|
||||
return $aTokenRules;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
private static function GetPrivateKey()
|
||||
{
|
||||
$sPrivateKey = DBProperty::GetProperty(self::PRIVATE_KEY);
|
||||
if (is_null($sPrivateKey)) {
|
||||
$sPrivateKey = bin2hex(random_bytes(32));
|
||||
DBProperty::SetProperty(self::PRIVATE_KEY, $sPrivateKey);
|
||||
}
|
||||
|
||||
return $sPrivateKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1712,5 +1712,7 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'Menu:UserManagement' => 'User management',
|
||||
'Menu:Queries' => 'Queries',
|
||||
'Menu:ConfigurationTools' => 'Configuration',
|
||||
'Menu:ConfigEditor' => 'General configuration',
|
||||
'Menu:ConfigEditor+' => 'Configuration File editors',
|
||||
|
||||
));
|
||||
|
||||
@@ -315,6 +315,8 @@ Elle s\'applique à tous les objets dans le périmètre de sa catégorie d\'audi
|
||||
'Menu:CSVImportMenu' => 'Import CSV',
|
||||
'Menu:CSVImportMenu+' => 'Import ou mise à jour en masse',
|
||||
'Menu:ConfigurationTools' => 'Configuration',
|
||||
'Menu:ConfigEditor' => 'Configuration générale',
|
||||
'Menu:ConfigEditor+' => 'Édition du fichier de configuration',
|
||||
'Menu:DataAdministration' => 'Administration des données',
|
||||
'Menu:DataAdministration+' => 'Administration des données',
|
||||
'Menu:DataModelMenu' => 'Modèle de données',
|
||||
|
||||
@@ -64,9 +64,6 @@ class iTopNewsroomController extends Controller
|
||||
$oBulkActionsBlock = PanelUIBlockFactory::MakeForInformation(Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:Title'));
|
||||
$oBulkActionsBlock->AddSubTitleBlock(new Html(Dict::S('UI:Newsroom:iTopNotification:ViewAllPage:SubTitle')));
|
||||
$sPictureUrl = UserRights::GetUserPictureAbsUrl();
|
||||
if (empty($sPictureUrl)) {
|
||||
$sPictureUrl = utils::GetAbsoluteUrlAppRoot().'images/user-pictures/'.appUserPreferences::GetPref('user_picture_placeholder', 'user-profile-default-256px.png');
|
||||
}
|
||||
$oBulkActionsBlock->SetIcon($sPictureUrl,Panel::ENUM_ICON_COVER_METHOD_CONTAIN, true);
|
||||
|
||||
$oNotificationsCenterButton = ButtonUIBlockFactory::MakeIconLink(
|
||||
|
||||
@@ -63,9 +63,6 @@ class NotificationsCenterController extends Controller
|
||||
|
||||
$oNotificationsPanel->AddSubTitleBlock(new Html(Dict::S('UI:NotificationsCenter:Panel:SubTitle')));
|
||||
$sPictureUrl = UserRights::GetUserPictureAbsUrl();
|
||||
if (empty($sPictureUrl)) {
|
||||
$sPictureUrl = utils::GetAbsoluteUrlAppRoot().'images/user-pictures/'.appUserPreferences::GetPref('user_picture_placeholder', 'user-profile-default-256px.png');
|
||||
}
|
||||
$oNotificationsPanel->SetIcon($sPictureUrl,Panel::ENUM_ICON_COVER_METHOD_CONTAIN, true);
|
||||
|
||||
$oNotificationsCenterTableColumns = [
|
||||
|
||||
@@ -676,7 +676,7 @@ HTML
|
||||
<div class="caselog-thread--block-medallion" style="{$sEntryMedallionStyle}" data-tooltip-content="{$sEntryMedallionTooltip}" data-placement="{$sEntryMedallionTooltipPlacement}">
|
||||
$sEntryMedallionContent
|
||||
</div>
|
||||
<div class="caselog-thread--block-user">{$sEntryUserLogin}</div>
|
||||
<div class="caselog-thread--block-user">{$sEntryMedallionTooltip}</div>
|
||||
HTML
|
||||
);
|
||||
|
||||
|
||||
@@ -169,6 +169,27 @@ abstract class ItopTestCase extends TestCase
|
||||
return $sAppRootPath . '/';
|
||||
}
|
||||
|
||||
private static function GetFirstDirUpContainingFile(string $sSearchPath, string $sFileToFindGlobPattern): ?string
|
||||
{
|
||||
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
|
||||
$aGlobFiles = glob($sSearchPath . '/' . $sFileToFindGlobPattern);
|
||||
if (is_array($aGlobFiles) && (count($aGlobFiles) > 0)) {
|
||||
return $sSearchPath . '/';
|
||||
}
|
||||
$iOffsetSep = strrpos($sSearchPath, '/');
|
||||
if ($iOffsetSep === false) {
|
||||
$iOffsetSep = strrpos($sSearchPath, '\\');
|
||||
if ($iOffsetSep === false) {
|
||||
// Do not throw an exception here as PHPUnit will not show it clearly when determing the list of test to perform
|
||||
return 'Could not find the approot file in ' . $sSearchPath;
|
||||
}
|
||||
}
|
||||
$sSearchPath = substr($sSearchPath, 0, $iOffsetSep);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overload this method to require necessary files through {@see \Combodo\iTop\Test\UnitTest\ItopTestCase::RequireOnceItopFile()}
|
||||
*
|
||||
@@ -206,23 +227,6 @@ abstract class ItopTestCase extends TestCase
|
||||
require_once $this->GetAppRoot() . $sFileRelPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to load a module file. The caller test must be in that module !
|
||||
* Will browse dir up to find a module.*.php
|
||||
*
|
||||
* @param string $sFileRelPath for example 'portal/src/Helper/ApplicationHelper.php'
|
||||
* @since 2.7.10 3.1.1 3.2.0 N°6709 method creation
|
||||
*/
|
||||
protected function RequireOnceCurrentModuleFile(string $sFileRelPath): void
|
||||
{
|
||||
$aStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
|
||||
$sCallerFileFullPath = $aStack[0]['file'];
|
||||
$sCallerDir = dirname($sCallerFileFullPath);
|
||||
|
||||
$sModuleRootPath = static::GetFirstDirUpContainingFile($sCallerDir, 'module.*.php');
|
||||
require_once $sModuleRootPath . $sFileRelPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require once a unit test file (eg. a mock class) from its relative path from the *current* dir.
|
||||
* This ensure that required files don't crash when unit tests dir is moved in the iTop structure (see N°5608)
|
||||
@@ -240,26 +244,6 @@ abstract class ItopTestCase extends TestCase
|
||||
require_once $sCallerDirAbsPath . DIRECTORY_SEPARATOR . $sFileRelPath;
|
||||
}
|
||||
|
||||
private static function GetFirstDirUpContainingFile(string $sSearchPath, string $sFileToFindGlobPattern): ?string
|
||||
{
|
||||
for ($iDepth = 0; $iDepth < 8; $iDepth++) {
|
||||
$aGlobFiles = glob($sSearchPath . '/' . $sFileToFindGlobPattern);
|
||||
if (is_array($aGlobFiles) && (count($aGlobFiles) > 0)) {
|
||||
return $sSearchPath . '/';
|
||||
}
|
||||
$iOffsetSep = strrpos($sSearchPath, '/');
|
||||
if ($iOffsetSep === false) {
|
||||
$iOffsetSep = strrpos($sSearchPath, '\\');
|
||||
if ($iOffsetSep === false) {
|
||||
// Do not throw an exception here as PHPUnit will not show it clearly when determing the list of test to perform
|
||||
return 'Could not find the approot file in ' . $sSearchPath;
|
||||
}
|
||||
}
|
||||
$sSearchPath = substr($sSearchPath, 0, $iOffsetSep);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function debug($sMsg)
|
||||
{
|
||||
if (static::$DEBUG_UNIT_TEST) {
|
||||
@@ -402,11 +386,11 @@ abstract class ItopTestCase extends TestCase
|
||||
*/
|
||||
private function GetProperty(string $sClass, string $sProperty): \ReflectionProperty
|
||||
{
|
||||
$class = new \ReflectionClass($sClass);
|
||||
$property = $class->getProperty($sProperty);
|
||||
$property->setAccessible(true);
|
||||
$oClass = new \ReflectionClass($sClass);
|
||||
$oProperty = $oClass->getProperty($sProperty);
|
||||
$oProperty->setAccessible(true);
|
||||
|
||||
return $property;
|
||||
return $oProperty;
|
||||
}
|
||||
|
||||
|
||||
@@ -417,7 +401,7 @@ abstract class ItopTestCase extends TestCase
|
||||
*
|
||||
* @since 2.7.8 3.0.3 3.1.0
|
||||
*/
|
||||
public function SetNonPublicProperty(object $oObject, string $sProperty, $value)
|
||||
public function SetNonPublicProperty($oObject, string $sProperty, $value)
|
||||
{
|
||||
$oProperty = $this->GetProperty(get_class($oObject), $sProperty);
|
||||
$oProperty->setValue($oObject, $value);
|
||||
|
||||
@@ -374,6 +374,27 @@ class CRUDEventTest extends ItopDataTestCase
|
||||
$this->assertStringStartsWith('CRUD', $oPerson->Get('first_name'), 'The object should have been modified and recorded in DB by EVENT_DB_AFTER_WRITE handler');
|
||||
}
|
||||
|
||||
public function testAfterDeleteObjectAttributesExceptLinkedSetAreUsable()
|
||||
{
|
||||
$oPerson = $this->createObject('Person', [
|
||||
'name' => 'Person_1',
|
||||
'first_name' => 'Test',
|
||||
'org_id' => $this->getTestOrgId(),
|
||||
]);
|
||||
|
||||
$oFetchPerson = MetaModel::GetObject('Person', $oPerson->GetKey());
|
||||
|
||||
$oEventReceiver = new CRUDEventReceiver($this);
|
||||
// Set the person's first name during Compute Values
|
||||
$oEventReceiver->AddCallback(EVENT_DB_AFTER_DELETE, Person::class, 'GetObjectAttributesValues');
|
||||
$oEventReceiver->RegisterCRUDEventListeners(EVENT_DB_AFTER_DELETE);
|
||||
$oEventReceiver->RegisterCRUDEventListeners(EVENT_DB_OBJECT_RELOAD);
|
||||
|
||||
$oFetchPerson->DBDelete();
|
||||
|
||||
$this->assertEquals(1, self::$aEventCallsCount[EVENT_DB_AFTER_DELETE], 'EVENT_DB_AFTER_DELETE must be called when deleting an object and the object attributes must remain accessible');
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify one object during EVENT_DB_AFTER_WRITE
|
||||
* Check that the CRUD is protected against infinite loops (when modifying an object in its EVENT_DB_AFTER_WRITE)
|
||||
@@ -881,6 +902,20 @@ class CRUDEventReceiver extends ClassesWithDebug
|
||||
$oObject->Set('first_name', 'CRUD_first_name_'.rand());
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
|
||||
*/
|
||||
private function GetObjectAttributesValues(EventData $oData): void
|
||||
{
|
||||
$this->Debug(__METHOD__);
|
||||
$oObject = $oData->Get('object');
|
||||
foreach (MetaModel::ListAttributeDefs(get_class($oObject)) as $sAttCode => $oAttDef) {
|
||||
if (!$oAttDef->IsLinkSet()) {
|
||||
$oObject->Get($sAttCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnusedPrivateMethodInspection Used as a callback
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user