diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php
index 9a99fd736..8889996ec 100644
--- a/addons/userrights/userrightsprofile.class.inc.php
+++ b/addons/userrights/userrightsprofile.class.inc.php
@@ -921,8 +921,9 @@ class UserRightsProfile extends UserRightsAddOnAPI
}
/**
- * Find out which attribute is corresponding the the dimension 'owner org'
- * returns null if no such attribute has been found (no filtering should occur)
+ * @param string $sClass
+ * @return string|null Find out which attribute is corresponding the dimension 'owner org'
+ * returns null if no such attribute has been found (no filtering should occur)
*/
public static function GetOwnerOrganizationAttCode($sClass)
{
diff --git a/addons/userrights/userrightsprofile.db.class.inc.php b/addons/userrights/userrightsprofile.db.class.inc.php
index ac5b2277b..1eaac996d 100644
--- a/addons/userrights/userrightsprofile.db.class.inc.php
+++ b/addons/userrights/userrightsprofile.db.class.inc.php
@@ -586,10 +586,10 @@ class UserRightsProfile extends UserRightsAddOnAPI
/**
* Read and cache organizations allowed to the given user
*
- * @param $oUser
- * @param $sClass (not used here but can be used in overloads)
+ * @param User $oUser
+ * @param string $sClass (not used here but can be used in overloads)
*
- * @return array
+ * @return array keys of the User allowed org
* @throws \CoreException
* @throws \Exception
*/
diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php
index 1b64d2668..f0cdbca53 100644
--- a/application/applicationextension.inc.php
+++ b/application/applicationextension.inc.php
@@ -1949,6 +1949,8 @@ class RestUtils
*
* @return DBObject The object found
* @throws Exception If the input structure is not valid or it could not find exactly one object
+ *
+ * @see DBObject::CheckChangedExtKeysValues() generic method to check that we can access the linked object isn't used in that use case because values can be literal, OQL, friendlyname
*/
public static function FindObjectFromKey($sClass, $key, $bAllowNullValue = false)
{
diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php
index 0a96af064..e08ec23db 100644
--- a/application/cmdbabstract.class.inc.php
+++ b/application/cmdbabstract.class.inc.php
@@ -5281,6 +5281,11 @@ EOF
'errors' => '
'.($bResult ? '' : implode('
', $aErrorsToDisplay)).'
',
);
if ($bResult && (!$bPreview)) {
+ // doing the check will load multiple times same objects :/
+ // but it shouldn't cost too much on execution time
+ // user can mitigate by selecting less extkeys/lnk to set and/or less objects to update 🤷‍♂️
+ $oObj->CheckChangedExtKeysValues();
+
$oObj->DBUpdate();
}
}
diff --git a/application/exceptions/InvalidExternalKeyValueException.php b/application/exceptions/InvalidExternalKeyValueException.php
new file mode 100644
index 000000000..cd8a08990
--- /dev/null
+++ b/application/exceptions/InvalidExternalKeyValueException.php
@@ -0,0 +1,36 @@
+GetKey();
+ $aContextData[self::ENUM_PARAMS_ATTCODE] = $sAttCode;
+ $aContextData[self::ENUM_PARAMS_ATTVALUE] = $oObject->Get($sAttCode);
+
+ $oCurrentUser = UserRights::GetUserObject();
+ if (false === is_null($oCurrentUser)) {
+ $aContextData[self::ENUM_PARAMS_USER] = get_class($oCurrentUser) . '::' . $oCurrentUser->GetKey();
+ }
+
+ parent::__construct('Attribute pointing to an object that is either non existing or not readable by the current user', $aContextData, '', $oPrevious);
+ }
+
+ public function GetAttCode(): string
+ {
+ return $this->getContextData()[self::ENUM_PARAMS_ATTCODE];
+ }
+
+ public function GetAttValue(): string
+ {
+ return $this->getContextData()[self::ENUM_PARAMS_ATTVALUE];
+ }
+}
diff --git a/application/utils.inc.php b/application/utils.inc.php
index 6086f3874..8e4981093 100644
--- a/application/utils.inc.php
+++ b/application/utils.inc.php
@@ -2877,6 +2877,7 @@ HTML;
*
* @return bool if string null or empty
* @since 3.0.2 N°5302
+ * @since 2.7.10 N°6458 add method in the 2.7 branch
*/
public static function IsNullOrEmptyString(?string $sString): bool
{
@@ -2892,6 +2893,7 @@ HTML;
*
* @return bool if string is not null and not empty
* @since 3.0.2 N°5302
+ * @since 2.7.10 N°6458 add method in the 2.7 branch
*/
public static function IsNotNullOrEmptyString(?string $sString): bool
{
diff --git a/core/dbobject.class.php b/core/dbobject.class.php
index 1dc156895..2b912aaef 100644
--- a/core/dbobject.class.php
+++ b/core/dbobject.class.php
@@ -2331,6 +2331,83 @@ abstract class DBObject implements iDisplay
return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue);
}
+ /**
+ * Checks for extkey attributes values. This will throw exception on non-existing as well as non-accessible objects (silo, scopes).
+ * That's why the test is done for all users including Administrators
+ *
+ * Note that due to perf issues, this isn't called directly by the ORM, but has to be called by consumers when possible.
+ *
+ * @param callable(string, string):bool|null $oIsObjectLoadableCallback Override to check if object is accessible.
+ * Parameters are object class and key
+ * Return value should be false if cannot access object, true otherwise
+ * @return void
+ *
+ * @throws ArchivedObjectException
+ * @throws CoreException if cannot get object attdef list
+ * @throws CoreUnexpectedValue
+ * @throws InvalidExternalKeyValueException
+ * @throws MySQLException
+ * @throws SecurityException if one extkey is pointing to an invalid value
+ *
+ * @link https://github.com/Combodo/iTop/security/advisories/GHSA-245j-66p9-pwmh
+ * @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6458
+ *
+ * @see \RestUtils::FindObjectFromKey for the same check in the REST endpoint
+ */
+ final public function CheckChangedExtKeysValues(callable $oIsObjectLoadableCallback = null)
+ {
+ if (is_null($oIsObjectLoadableCallback)) {
+ $oIsObjectLoadableCallback = function ($sClass, $sId) {
+ $oRemoteObject = MetaModel::GetObject($sClass, $sId, false);
+ if (is_null($oRemoteObject)) {
+ return false;
+ }
+ return true;
+ };
+ }
+
+ $aChanges = $this->ListChanges();
+ $aAttCodesChanged = array_keys($aChanges);
+ foreach ($aAttCodesChanged as $sAttDefCode) {
+ $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttDefCode);
+
+ if ($oAttDef instanceof AttributeLinkedSetIndirect) {
+ /** @var ormLinkSet $oOrmSet */
+ $oOrmSet = $this->Get($sAttDefCode);
+ while ($oLnk = $oOrmSet->Fetch()) {
+ $oLnk->CheckChangedExtKeysValues($oIsObjectLoadableCallback);
+ }
+ continue;
+ }
+
+ /** @noinspection PhpConditionCheckedByNextConditionInspection */
+ /** @noinspection NotOptimalIfConditionsInspection */
+ if (($oAttDef instanceof AttributeHierarchicalKey) || ($oAttDef instanceof AttributeExternalKey)) {
+ $sRemoteObjectClass = $oAttDef->GetTargetClass();
+ $sRemoteObjectKey = $this->Get($sAttDefCode);
+ } else if ($oAttDef instanceof AttributeObjectKey) {
+ $sRemoteObjectClassAttCode = $oAttDef->Get('class_attcode');
+ $sRemoteObjectClass = $this->Get($sRemoteObjectClassAttCode);
+ $sRemoteObjectKey = $this->Get($sAttDefCode);
+ } else {
+ continue;
+ }
+
+ /** @noinspection NotOptimalIfConditionsInspection */
+ /** @noinspection TypeUnsafeComparisonInspection */
+ if (utils::IsNullOrEmptyString($sRemoteObjectClass)
+ || utils::IsNullOrEmptyString($sRemoteObjectKey)
+ || ($sRemoteObjectKey == 0) // non-strict comparison as we might have bad surprises
+ ) {
+ continue;
+ }
+
+ if (false === $oIsObjectLoadableCallback($sRemoteObjectClass, $sRemoteObjectKey)) {
+ throw new InvalidExternalKeyValueException($this, $sAttDefCode);
+ }
+ }
+ }
+
/**
* Check if it is allowed to delete the existing object from the database
*
@@ -2476,11 +2553,11 @@ abstract class DBObject implements iDisplay
* @api
* @api-advanced
*
- * @see \DBObject::ListPreviousValuesForUpdatedAttributes() to get previous values anywhere in the CRUD stack
- * @see https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Asequence_crud iTop CRUD stack documentation
- * @return array attname => currentvalue List the attributes that have been changed using {@see DBObject::Set()}.
+ * @return array attcode => currentvalue List the attributes that have been changed using {@see DBObject::Set()}.
* Reset during {@see DBObject::DBUpdate()}
* @throws Exception
+ * @see \DBObject::ListPreviousValuesForUpdatedAttributes() to get previous values anywhere in the CRUD stack
+ * @see https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Asequence_crud iTop CRUD stack documentation
* @uses m_aCurrValues
*/
public function ListChanges()
@@ -2772,7 +2849,6 @@ abstract class DBObject implements iDisplay
* @throws \Exception
*
* @internal
- *
*/
public function DBInsertNoReload()
{
@@ -3056,8 +3132,6 @@ abstract class DBObject implements iDisplay
* Persist an object to the DB, for the first time
*
* @api
- * @see DBWrite
- *
* @return int|null inserted object key
*
* @throws \ArchivedObjectException
@@ -3067,10 +3141,12 @@ abstract class DBObject implements iDisplay
* @throws \CoreWarning
* @throws \MySQLException
* @throws \OQLException
+ *
+ * @see DBWrite
*/
public function DBInsert()
{
- $this->DBInsertNoReload();
+ $this->DBInsertNoReload();
if (MetaModel::DBIsReadOnly())
{
@@ -3129,13 +3205,13 @@ abstract class DBObject implements iDisplay
* Update an object in DB
*
* @api
- * @see DBObject::DBWrite()
- *
* @return int object key
*
* @throws \CoreException
* @throws \CoreCannotSaveObjectException if CheckToWrite() returns issues
* @throws \Exception
+ *
+ * @see DBObject::DBWrite()
*/
public function DBUpdate()
{
@@ -3143,6 +3219,7 @@ abstract class DBObject implements iDisplay
{
throw new CoreException("DBUpdate: could not update a newly created object, please call DBInsert instead");
}
+
// Protect against reentrance (e.g. cascading the update of ticket logs)
static $aUpdateReentrance = array();
$sKey = get_class($this).'::'.$this->GetKey();
@@ -3519,13 +3596,18 @@ abstract class DBObject implements iDisplay
/**
* Make the current changes persistent - clever wrapper for Insert or Update
- *
- * @api
+ *
+ * @api
*
* @return int
- *
- * @throws \CoreCannotSaveObjectException
- * @throws \CoreException
+ *
+ * @throws ArchivedObjectException
+ * @throws CoreCannotSaveObjectException
+ * @throws CoreException
+ * @throws CoreUnexpectedValue
+ * @throws CoreWarning
+ * @throws MySQLException
+ * @throws OQLException
*/
public function DBWrite()
{
diff --git a/core/metamodel.class.php b/core/metamodel.class.php
index 057c2ca5a..8291bb049 100644
--- a/core/metamodel.class.php
+++ b/core/metamodel.class.php
@@ -1430,8 +1430,10 @@ abstract class MetaModel
*
* @return AttributeDefinition[]
* @throws \CoreException
+ *
+ * @see GetAttributesList for attcode list
*/
- final static public function ListAttributeDefs($sClass)
+ final public static function ListAttributeDefs($sClass)
{
self::_check_subclass($sClass);
return self::$m_aAttribDefs[$sClass];
@@ -1444,8 +1446,10 @@ abstract class MetaModel
* @param string[] $aDesiredAttTypes Array of AttributeDefinition classes to filter the list on
* @param string|null $sListCode If provided, attributes will be limited to those in this zlist
*
- * @return array
+ * @return string[] list of attcodes
* @throws \CoreException
+ *
+ * @see ListAttributeDefs to get AttributeDefinition array instead
*/
final public static function GetAttributesList(string $sClass, array $aDesiredAttTypes = [], ?string $sListCode = null)
{
diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php
index 15e58083b..c9693c4fa 100644
--- a/core/userrights.class.inc.php
+++ b/core/userrights.class.inc.php
@@ -1110,9 +1110,7 @@ class UserRights
}
/**
- * Return the current user login or an empty string if nobody connected.
- *
- * @return string
+ * @return string connected {@see User} login field value, otherwise empty string
*/
public static function GetUser()
{
@@ -1560,9 +1558,9 @@ class UserRights
/**
* @param string $sClass
- * @param int $iActionCode
- * @param \DBObjectSet $oInstanceSet
- * @param \User $oUser
+ * @param int $iActionCode see UR_ACTION_* constants
+ * @param DBObjectSet $oInstanceSet
+ * @param User $oUser
*
* @return int (UR_ALLOWED_YES|UR_ALLOWED_NO|UR_ALLOWED_DEPENDS)
* @throws \CoreException
diff --git a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
index a092770a5..8e10bf7ed 100644
--- a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
+++ b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php
@@ -31,6 +31,7 @@ use Combodo\iTop\Form\Field\LabelField;
use Combodo\iTop\Form\Form;
use Combodo\iTop\Form\FormManager;
use Combodo\iTop\Portal\Helper\ApplicationHelper;
+use Combodo\iTop\Portal\Helper\SecurityHelper;
use CoreCannotSaveObjectException;
use DBObject;
use DBObjectSearch;
@@ -42,6 +43,7 @@ use DOMXPath;
use Exception;
use ExceptionLog;
use InlineImage;
+use InvalidExternalKeyValueException;
use IssueLog;
use MetaModel;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -49,6 +51,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use UserRights;
use utils;
+use const UR_ACTION_READ;
/**
* Description of ObjectFormManager
@@ -1150,8 +1153,11 @@ class ObjectFormManager extends FormManager
$bWasModified = $this->oObject->IsModified();
$bActivateTriggers = (!$bIsNew && $bWasModified);
+ /** @var SecurityHelper $oSecurityHelper */
+ $oSecurityHelper = $this->oContainer->get('security_helper');
+
// Forcing allowed writing on the object if necessary. This is used in some particular cases.
- $bAllowWrite = $this->oContainer->get('security_helper')->IsActionAllowed($bIsNew ? UR_ACTION_CREATE : UR_ACTION_MODIFY, $sObjectClass, $this->oObject->GetKey());
+ $bAllowWrite = $oSecurityHelper->IsActionAllowed($bIsNew ? UR_ACTION_CREATE : UR_ACTION_MODIFY, $sObjectClass, $this->oObject->GetKey());
if ($bAllowWrite) {
$this->oObject->AllowWrite(true);
}
@@ -1159,12 +1165,15 @@ class ObjectFormManager extends FormManager
// Writing object to DB
try
{
+ $this->oObject->CheckChangedExtKeysValues(function ($sClass, $sId) use ($oSecurityHelper): bool {
+ return $oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sClass, $sId);
+ });
$this->oObject->DBWrite();
- }
- catch (CoreCannotSaveObjectException $e) {
+ } catch (CoreCannotSaveObjectException $e) {
throw new Exception($e->getHtmlMessage());
- }
- catch (Exception $e) {
+ } catch (InvalidExternalKeyValueException $e) {
+ throw new Exception($e->getIssue());
+ } catch (Exception $e) {
$aContext = [
'origin' => __CLASS__.'::'.__METHOD__,
'obj_class' => get_class($this->oObject),
diff --git a/lib/autoload.php b/lib/autoload.php
index f37a5b80e..4daf9b579 100644
--- a/lib/autoload.php
+++ b/lib/autoload.php
@@ -2,6 +2,24 @@
// autoload.php @generated by Composer
+if (PHP_VERSION_ID < 50600) {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, $err);
+ } elseif (!headers_sent()) {
+ echo $err;
+ }
+ }
+ trigger_error(
+ $err,
+ E_USER_ERROR
+ );
+}
+
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit5e7efdfe4e8f9526eb41991410b96239::getLoader();
diff --git a/lib/composer/ClassLoader.php b/lib/composer/ClassLoader.php
index 0cd6055d1..7824d8f7e 100644
--- a/lib/composer/ClassLoader.php
+++ b/lib/composer/ClassLoader.php
@@ -42,35 +42,37 @@ namespace Composer\Autoload;
*/
class ClassLoader
{
- /** @var ?string */
+ /** @var \Closure(string):void */
+ private static $includeFile;
+
+ /** @var string|null */
private $vendorDir;
// PSR-4
/**
- * @var array[]
- * @psalm-var array>
+ * @var array>
*/
private $prefixLengthsPsr4 = array();
/**
- * @var array[]
- * @psalm-var array>
+ * @var array>
*/
private $prefixDirsPsr4 = array();
/**
- * @var array[]
- * @psalm-var array
+ * @var list
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
- * @var array[]
- * @psalm-var array>
+ * List of PSR-0 prefixes
+ *
+ * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+ *
+ * @var array>>
*/
private $prefixesPsr0 = array();
/**
- * @var array[]
- * @psalm-var array
+ * @var list
*/
private $fallbackDirsPsr0 = array();
@@ -78,8 +80,7 @@ class ClassLoader
private $useIncludePath = false;
/**
- * @var string[]
- * @psalm-var array
+ * @var array
*/
private $classMap = array();
@@ -87,29 +88,29 @@ class ClassLoader
private $classMapAuthoritative = false;
/**
- * @var bool[]
- * @psalm-var array
+ * @var array
*/
private $missingClasses = array();
- /** @var ?string */
+ /** @var string|null */
private $apcuPrefix;
/**
- * @var self[]
+ * @var array
*/
private static $registeredLoaders = array();
/**
- * @param ?string $vendorDir
+ * @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
}
/**
- * @return string[]
+ * @return array>
*/
public function getPrefixes()
{
@@ -121,8 +122,7 @@ class ClassLoader
}
/**
- * @return array[]
- * @psalm-return array>
+ * @return array>
*/
public function getPrefixesPsr4()
{
@@ -130,8 +130,7 @@ class ClassLoader
}
/**
- * @return array[]
- * @psalm-return array
+ * @return list
*/
public function getFallbackDirs()
{
@@ -139,8 +138,7 @@ class ClassLoader
}
/**
- * @return array[]
- * @psalm-return array
+ * @return list
*/
public function getFallbackDirsPsr4()
{
@@ -148,8 +146,7 @@ class ClassLoader
}
/**
- * @return string[] Array of classname => path
- * @psalm-var array
+ * @return array Array of classname => path
*/
public function getClassMap()
{
@@ -157,8 +154,7 @@ class ClassLoader
}
/**
- * @param string[] $classMap Class to filename map
- * @psalm-param array $classMap
+ * @param array $classMap Class to filename map
*
* @return void
*/
@@ -175,24 +171,25 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
- * @param string $prefix The prefix
- * @param string[]|string $paths The PSR-0 root directories
- * @param bool $prepend Whether to prepend the directories
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
+ $paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
- (array) $paths,
+ $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
- (array) $paths
+ $paths
);
}
@@ -201,19 +198,19 @@ class ClassLoader
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
- $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+ $this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
- (array) $paths,
+ $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
- (array) $paths
+ $paths
);
}
}
@@ -222,9 +219,9 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
- * @param string $prefix The prefix/namespace, with trailing '\\'
- * @param string[]|string $paths The PSR-4 base directories
- * @param bool $prepend Whether to prepend the directories
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
@@ -232,17 +229,18 @@ class ClassLoader
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
+ $paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
- (array) $paths,
+ $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
- (array) $paths
+ $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@ class ClassLoader
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
- $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ $this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
- (array) $paths,
+ $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
- (array) $paths
+ $paths
);
}
}
@@ -272,8 +270,8 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
- * @param string $prefix The prefix
- * @param string[]|string $paths The PSR-0 base directories
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 base directories
*
* @return void
*/
@@ -290,8 +288,8 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
- * @param string $prefix The prefix/namespace, with trailing '\\'
- * @param string[]|string $paths The PSR-4 base directories
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
@@ -425,7 +423,8 @@ class ClassLoader
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
- includeFile($file);
+ $includeFile = self::$includeFile;
+ $includeFile($file);
return true;
}
@@ -476,9 +475,9 @@ class ClassLoader
}
/**
- * Returns the currently registered loaders indexed by their corresponding vendor directories.
+ * Returns the currently registered loaders keyed by their corresponding vendor directories.
*
- * @return self[]
+ * @return array
*/
public static function getRegisteredLoaders()
{
@@ -555,18 +554,26 @@ class ClassLoader
return false;
}
-}
-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
- include $file;
+ /**
+ * @return void
+ */
+ private static function initializeIncludeClosure()
+ {
+ if (self::$includeFile !== null) {
+ return;
+ }
+
+ /**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ */
+ self::$includeFile = \Closure::bind(static function($file) {
+ include $file;
+ }, null, null);
+ }
}
diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php
index acd509563..1666e694f 100644
--- a/lib/composer/autoload_classmap.php
+++ b/lib/composer/autoload_classmap.php
@@ -2,7 +2,7 @@
// autoload_classmap.php @generated by Composer
-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
@@ -662,6 +662,7 @@ return array(
'IntervalOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
'Introspection' => $baseDir . '/core/introspection.class.inc.php',
'InvalidConfigParamException' => $baseDir . '/application/exceptions/InvalidConfigParamException.php',
+ 'InvalidExternalKeyValueException' => $baseDir . '/application/exceptions/InvalidExternalKeyValueException.php',
'InvalidPasswordAttributeOneWayPassword' => $baseDir . '/application/exceptions/InvalidPasswordAttributeOneWayPassword.php',
'IssueLog' => $baseDir . '/core/log.class.inc.php',
'ItopCounter' => $baseDir . '/core/counter.class.inc.php',
diff --git a/lib/composer/autoload_files.php b/lib/composer/autoload_files.php
index 7be757bea..e2cb88955 100644
--- a/lib/composer/autoload_files.php
+++ b/lib/composer/autoload_files.php
@@ -2,25 +2,25 @@
// autoload_files.php @generated by Composer
-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
+ '7e9bd612cc444b3eed788ebbe46263a0' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/autoload.php',
'5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php',
'023d27dca8066ef29e6739335ea73bad' => $vendorDir . '/symfony/polyfill-php70/bootstrap.php',
- '32dcc8afd4335739640db7d200c1971d' => $vendorDir . '/symfony/polyfill-apcu/bootstrap.php',
- '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
- 'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php',
- '7e9bd612cc444b3eed788ebbe46263a0' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/autoload.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
+ 'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
+ '32dcc8afd4335739640db7d200c1971d' => $vendorDir . '/symfony/polyfill-apcu/bootstrap.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
+ '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
);
diff --git a/lib/composer/autoload_namespaces.php b/lib/composer/autoload_namespaces.php
index d12922d08..e6117c750 100644
--- a/lib/composer/autoload_namespaces.php
+++ b/lib/composer/autoload_namespaces.php
@@ -2,7 +2,7 @@
// autoload_namespaces.php @generated by Composer
-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php
index ca8b4b9f6..651c9f0c1 100644
--- a/lib/composer/autoload_psr4.php
+++ b/lib/composer/autoload_psr4.php
@@ -2,7 +2,7 @@
// autoload_psr4.php @generated by Composer
-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
diff --git a/lib/composer/autoload_real.php b/lib/composer/autoload_real.php
index bc3e8fca0..9fb5fdc92 100644
--- a/lib/composer/autoload_real.php
+++ b/lib/composer/autoload_real.php
@@ -25,46 +25,31 @@ class ComposerAutoloaderInit5e7efdfe4e8f9526eb41991410b96239
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit5e7efdfe4e8f9526eb41991410b96239', 'loadClassLoader'), true, true);
- self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
+ self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit5e7efdfe4e8f9526eb41991410b96239', 'loadClassLoader'));
$includePaths = require __DIR__ . '/include_paths.php';
$includePaths[] = get_include_path();
set_include_path(implode(PATH_SEPARATOR, $includePaths));
- $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
- if ($useStaticLoader) {
- require __DIR__ . '/autoload_static.php';
-
- call_user_func(\Composer\Autoload\ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239::getInitializer($loader));
- } else {
- $classMap = require __DIR__ . '/autoload_classmap.php';
- if ($classMap) {
- $loader->addClassMap($classMap);
- }
- }
+ require __DIR__ . '/autoload_static.php';
+ call_user_func(\Composer\Autoload\ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239::getInitializer($loader));
$loader->setClassMapAuthoritative(true);
$loader->register(true);
- if ($useStaticLoader) {
- $includeFiles = Composer\Autoload\ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239::$files;
- } else {
- $includeFiles = require __DIR__ . '/autoload_files.php';
- }
- foreach ($includeFiles as $fileIdentifier => $file) {
- composerRequire5e7efdfe4e8f9526eb41991410b96239($fileIdentifier, $file);
+ $filesToLoad = \Composer\Autoload\ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239::$files;
+ $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
+ if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+ $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+ require $file;
+ }
+ }, null, null);
+ foreach ($filesToLoad as $fileIdentifier => $file) {
+ $requireFile($fileIdentifier, $file);
}
return $loader;
}
}
-
-function composerRequire5e7efdfe4e8f9526eb41991410b96239($fileIdentifier, $file)
-{
- if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
- require $file;
-
- $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
- }
-}
diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php
index 6fc0575bf..664bbc8c9 100644
--- a/lib/composer/autoload_static.php
+++ b/lib/composer/autoload_static.php
@@ -9,20 +9,20 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
public static $files = array (
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
+ '7e9bd612cc444b3eed788ebbe46263a0' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/autoload.php',
'5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php',
'023d27dca8066ef29e6739335ea73bad' => __DIR__ . '/..' . '/symfony/polyfill-php70/bootstrap.php',
- '32dcc8afd4335739640db7d200c1971d' => __DIR__ . '/..' . '/symfony/polyfill-apcu/bootstrap.php',
- '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
- 'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php',
- '7e9bd612cc444b3eed788ebbe46263a0' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/autoload.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
+ 'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
+ '32dcc8afd4335739640db7d200c1971d' => __DIR__ . '/..' . '/symfony/polyfill-apcu/bootstrap.php',
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => __DIR__ . '/..' . '/symfony/polyfill-iconv/bootstrap.php',
+ '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
'2c102faa651ef8ea5874edb585946bce' => __DIR__ . '/..' . '/swiftmailer/swiftmailer/lib/swift_required.php',
);
@@ -1030,6 +1030,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239
'IntervalOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
'Introspection' => __DIR__ . '/../..' . '/core/introspection.class.inc.php',
'InvalidConfigParamException' => __DIR__ . '/../..' . '/application/exceptions/InvalidConfigParamException.php',
+ 'InvalidExternalKeyValueException' => __DIR__ . '/../..' . '/application/exceptions/InvalidExternalKeyValueException.php',
'InvalidPasswordAttributeOneWayPassword' => __DIR__ . '/../..' . '/application/exceptions/InvalidPasswordAttributeOneWayPassword.php',
'IssueLog' => __DIR__ . '/../..' . '/core/log.class.inc.php',
'ItopCounter' => __DIR__ . '/../..' . '/core/counter.class.inc.php',
diff --git a/lib/composer/include_paths.php b/lib/composer/include_paths.php
index d4fb96718..af33c1491 100644
--- a/lib/composer/include_paths.php
+++ b/lib/composer/include_paths.php
@@ -2,7 +2,7 @@
// include_paths.php @generated by Composer
-$vendorDir = dirname(dirname(__FILE__));
+$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
diff --git a/pages/UI.php b/pages/UI.php
index f8ac61627..eb6941fa9 100644
--- a/pages/UI.php
+++ b/pages/UI.php
@@ -870,6 +870,7 @@ EOF
{
throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors));
}
+ $oObj->CheckChangedExtKeysValues();
// Transactions are now handled in DBUpdate
$oObj->DBUpdate();
$sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName());
@@ -1108,6 +1109,7 @@ EOF
throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors));
}
+ $oObj->CheckChangedExtKeysValues();
$oObj->DBInsertNoReload();// No need to reload
IssueLog::Trace('Object created', $sClass, array(
diff --git a/tests/php-unit-tests/README.md b/tests/php-unit-tests/README.md
index 0b5d06b3d..3f556426f 100644
--- a/tests/php-unit-tests/README.md
+++ b/tests/php-unit-tests/README.md
@@ -59,6 +59,16 @@ Fix that in the XML configuration in the PHP section
```
+
+### Measure the time spent in a test
+
+Simply cut'n paste the following line at several places within the test function:
+
+```php
+if (isset($fStarted)) {echo 'L'.__LINE__.': '.round(microtime(true) - $fStarted, 3)."\n";} $fStarted = microtime(true);
+```
+
+
### Understand tests interactions
With PHPStorm, select two tests, right click to get the context menu, then `run`.
@@ -119,4 +129,3 @@ This won't work because the comment MUST start with `/**` (two stars) to be cons
Therefore, if the tests are isolated, then `setupBeforeClass` will be called as often as `setUp`.
-This has been proven with [`runClassInSeparateProcessTest.php`](experiments/runClassInSeparateProcessTest.php)
\ No newline at end of file
diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php
index a1f86d44f..1d9555813 100644
--- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php
+++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php
@@ -709,7 +709,7 @@ abstract class ItopDataTestCase extends ItopTestCase
* @return array
* @throws Exception
*/
- protected function AddCIToTicket($oCI, $oTicket, $sImpactCode)
+ protected function AddCIToTicket($oCI, $oTicket, $sImpactCode = 'manual')
{
$oNewLink = new lnkFunctionalCIToTicket();
$oNewLink->Set('functionalci_id', $oCI->GetKey());
diff --git a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php
index a63758fff..32ac9fc85 100644
--- a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php
+++ b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php
@@ -17,18 +17,20 @@
// along with iTop. If not, see
//
-/**
- * Created by PhpStorm.
- * User: Eric
- * Date: 02/10/2017
- * Time: 13:58
- */
-
namespace Combodo\iTop\Test\UnitTest\Core;
+use Attachment;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use DBObject;
+use InvalidExternalKeyValueException;
+use lnkPersonToTeam;
use MetaModel;
+use Organization;
+use Person;
+use Team;
+use User;
+use UserRights;
+use utils;
/**
@@ -37,6 +39,7 @@ use MetaModel;
class DBObjectTest extends ItopDataTestCase
{
const CREATE_TEST_ORG = true;
+ const INVALID_OBJECT_KEY = 123456789;
protected function setUp(): void
{
@@ -280,4 +283,265 @@ class DBObjectTest extends ItopDataTestCase
}
}
}
+
+ private function GetAlwaysTrueCallback(): callable
+ {
+ return static function () {
+ return true;
+ };
+ }
+
+ private function GetAlwaysFalseCallback(): callable
+ {
+ return static function () {
+ return false;
+ };
+ }
+
+ /**
+ * @covers DBObject::CheckChangedExtKeysValues()
+ * @runInSeparateProcess MetaModel::GetObject returning wrong values :(
+ */
+ public function testCheckExtKeysSiloOnAttributeExternalKey()
+ {
+ //--- Preparing data...
+ $oAlwaysTrueCallback = $this->GetAlwaysTrueCallback();
+ $oAlwaysFalseCallback = $this->GetAlwaysFalseCallback();
+
+ /** @var Organization $oDemoOrg */
+ $oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo');
+ /** @var Organization $oMyCompanyOrg */
+ $oMyCompanyOrg = MetaModel::GetObjectByName(Organization::class, 'My Company/Department');
+
+ /** @var Person $oPersonOfDemoOrg */
+ $oPersonOfDemoOrg = MetaModel::GetObjectByName(Person::class, 'Agatha Christie');
+ /** @var Person $oPersonOfMyCompanyOrg */
+ $oPersonOfMyCompanyOrg = MetaModel::GetObjectByName(Person::class, 'My first name My last name');
+
+ $sConfigurationManagerProfileId = 3; // Access to Person objects
+ $oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigurationManagerProfileId);
+
+ $oAdminUser = MetaModel::GetObjectByName(User::class, 'admin', false);
+ if (is_null($oAdminUser)) {
+ $oAdminUser = $this->CreateUser('admin', 1);
+ }
+
+ /** @var Person $oPersonObject */
+ $oPersonObject = $this->CreatePerson(0, $oMyCompanyOrg->GetKey());
+
+ //--- Now we can do some tests !
+ UserRights::Login($oUserWithAllowedOrgs->Get('login'));
+ $this->ResetMetaModelQueyCacheGetObject();
+
+ try {
+ $oPersonObject->CheckChangedExtKeysValues();
+ } catch (InvalidExternalKeyValueException $eCannotSave) {
+ $this->fail('Should skip external keys already written in Database');
+ }
+
+ $oPersonObject->Set('manager_id', $oPersonOfDemoOrg->GetKey());
+ try {
+ $oPersonObject->CheckChangedExtKeysValues();
+ } catch (InvalidExternalKeyValueException $eCannotSave) {
+ $this->fail('Should allow objects in the same org as the current user');
+ }
+
+ try {
+ $oPersonObject->CheckChangedExtKeysValues($oAlwaysFalseCallback);
+ $this->fail('Should consider the callback returning "false"');
+ } catch (InvalidExternalKeyValueException $eCannotSave) {
+ // Ok, the exception was expected
+ }
+
+ $oPersonObject->Set('manager_id', $oPersonOfMyCompanyOrg->GetKey());
+ try {
+ $oPersonObject->CheckChangedExtKeysValues();
+ $this->fail('Should not allow objects not being in the allowed orgs of the current user');
+ } catch (InvalidExternalKeyValueException $eCannotSave) {
+ $this->assertEquals('manager_id', $eCannotSave->GetAttCode(), 'Should report the wrong external key attcode');
+ $this->assertEquals($oMyCompanyOrg->GetKey(), $eCannotSave->GetAttValue(), 'Should report the unauthorized external key value');
+ }
+
+ try {
+ $oPersonObject->CheckChangedExtKeysValues($oAlwaysTrueCallback);
+ } catch (InvalidExternalKeyValueException $eCannotSave) {
+ $this->fail('Should consider the callback returning "true"');
+ }
+
+ UserRights::Logoff();
+ $this->ResetMetaModelQueyCacheGetObject();
+
+ UserRights::Login($oAdminUser->Get('login'));
+ $oPersonObject->CheckChangedExtKeysValues();
+ $this->assertTrue(true, 'Admin user can create objects in any org');
+ }
+
+ /**
+ * @covers DBObject::CheckChangedExtKeysValues()
+ * @runInSeparateProcess MetaModel::GetObject returning wrong values :(
+ */
+ public function testCheckExtKeysOnAttributeLinkedSetIndirect()
+ {
+ //--- Preparing data...
+ /** @var Organization $oDemoOrg */
+ $oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo');
+ /** @var Person $oPersonOnItDepartmentOrg */
+ $oPersonOnItDepartmentOrg = MetaModel::GetObjectByName(Person::class, 'Anna Gavalda');
+ /** @var Person $oPersonOnDemoOrg */
+ $oPersonOnDemoOrg = MetaModel::GetObjectByName(Person::class, 'Claude Monet');
+
+ $sConfigManagerProfileId = 3; // access to Team and Contact objects
+ $oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigManagerProfileId);
+
+ //--- Now we can do some tests !
+ UserRights::Login($oUserWithAllowedOrgs->Get('login'));
+ $this->ResetMetaModelQueyCacheGetObject();
+
+ $oTeam = MetaModel::NewObject(Team::class, [
+ 'name' => 'The A Team',
+ 'org_id' => $oDemoOrg->GetKey()
+ ]);
+
+ // Part 1 - Test with an invalid id (non-existing object)
+ //
+ $oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class);
+ $oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [
+ 'person_id' => self::INVALID_OBJECT_KEY,
+ ]));
+ $oTeam->Set('persons_list', $oPersonLinks);
+
+ try {
+ $oTeam->CheckChangedExtKeysValues();
+ $this->fail('An unknown object should be detected as invalid');
+ } catch (InvalidExternalKeyValueException $e) {
+ // we are getting the exception on the lnk class
+ // In consequence attcode is `lnkPersonToTeam.person_id` instead of `Team.persons_list`
+ $this->assertEquals('person_id', $e->GetAttCode(), 'The reported attcode should be the external key on the link');
+ $this->assertEquals(self::INVALID_OBJECT_KEY, $e->GetAttValue(), 'The reported value should be the external key on the link');
+ }
+
+ try {
+ $oTeam->CheckChangedExtKeysValues($this->GetAlwaysTrueCallback());
+ } catch (InvalidExternalKeyValueException $e) {
+ $this->fail('Should have no error when callback returns true');
+ }
+
+ // Part 2 - Test with an allowed object
+ //
+ $oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class);
+ $oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [
+ 'person_id' => $oPersonOnDemoOrg->GetKey(),
+ ]));
+ $oTeam->Set('persons_list', $oPersonLinks);
+
+ try {
+ $oTeam->CheckChangedExtKeysValues();
+ } catch (InvalidExternalKeyValueException $e) {
+ $this->fail('An authorized object should be detected as valid');
+ }
+
+ try {
+ $oTeam->CheckChangedExtKeysValues($this->GetAlwaysFalseCallback());
+ $this->fail('Should cascade the callback result when it is "false"');
+ } catch (InvalidExternalKeyValueException $e) {
+ // Ok, the exception was expected
+ }
+
+ // Part 3 - Test with a not allowed object
+ //
+ $oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class);
+ $oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [
+ 'person_id' => $oPersonOnItDepartmentOrg->GetKey(),
+ ]));
+ $oTeam->Set('persons_list', $oPersonLinks);
+
+ try {
+ $oTeam->CheckChangedExtKeysValues();
+ $this->fail('An unauthorized object should be detected as invalid');
+ }
+ catch (InvalidExternalKeyValueException $e) {
+ // Ok, the exception was expected
+ }
+
+ try {
+ $oTeam->CheckChangedExtKeysValues($this->GetAlwaysTrueCallback());
+ } catch (InvalidExternalKeyValueException $e) {
+ $this->fail('Should cascade the callback result when it is "true"');
+ }
+
+ $oTeam->DBInsert(); // persisting invalid value and resets the object changed values
+ try {
+ $oTeam->CheckChangedExtKeysValues();
+ }
+ catch (InvalidExternalKeyValueException $e) {
+ $this->fail('An unauthorized value should be ignored when it is not being modified');
+ }
+ }
+
+ /**
+ * @covers DBObject::CheckChangedExtKeysValues()
+ * @runInSeparateProcess MetaModel::GetObject returning wrong values :(
+ */
+ public function testCheckExtKeysSiloOnAttributeObjectKey()
+ {
+ //--- Preparing data...
+ /** @var Organization $oDemoOrg */
+ $oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo');
+ /** @var Person $oPersonOnItDepartmentOrg */
+ $oPersonOnItDepartmentOrg = MetaModel::GetObjectByName(Person::class, 'Anna Gavalda');
+ /** @var Person $oPersonOnDemoOrg */
+ $oPersonOnDemoOrg = MetaModel::GetObjectByName(Person::class, 'Claude Monet');
+
+ $sConfigManagerProfileId = 3; // access to Team and Contact objects
+ $oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigManagerProfileId);
+
+ //--- Now we can do some tests !
+ UserRights::Login($oUserWithAllowedOrgs->Get('login'));
+ $this->ResetMetaModelQueyCacheGetObject();
+
+ $oAttachment = MetaModel::NewObject(Attachment::class, [
+ 'item_class' => Person::class,
+ 'item_id' => $oPersonOnDemoOrg->GetKey(),
+ ]);
+ try {
+ $oAttachment->CheckChangedExtKeysValues();
+ } catch (InvalidExternalKeyValueException $e) {
+ $this->fail('Should be allowed to create an attachment pointing to a ticket in the allowed org list');
+ }
+
+ $oAttachment = MetaModel::NewObject(Attachment::class, [
+ 'item_class' => Person::class,
+ 'item_id' => $oPersonOnItDepartmentOrg->GetKey(),
+ ]);
+ $this->ResetMetaModelQueyCacheGetObject();
+ try {
+ $oAttachment->CheckChangedExtKeysValues();
+ $this->fail('There should be an error on attachment pointing to a non allowed org object');
+ } catch (InvalidExternalKeyValueException $e) {
+ $this->assertEquals('item_id', $e->GetAttCode(), 'Should report the object key attribute');
+ $this->assertEquals($oPersonOnItDepartmentOrg->GetKey(), $e->GetAttValue(), 'Should report the object key value');
+ }
+ }
+
+ /**
+ * Helper to reset the metamodel cache
+ * We might need to create something generic and add it to {@see UserRights::Logoff()} ?
+ */
+ private function ResetMetaModelQueyCacheGetObject() {
+ $this->SetNonPublicStaticProperty(MetaModel::class, 'aQueryCacheGetObject', []);
+ }
+
+ private function CreateDemoOrgUser(Organization $oDemoOrg, string $sProfileId): User
+ {
+ utils::GetConfig()->SetModuleSetting('authent-local', 'password_validation.pattern', '');
+ $oUserWithAllowedOrgs = $this->CreateContactlessUser('demo_test_' . __CLASS__, $sProfileId);
+ /** @var \URP_UserOrg $oUserOrg */
+ $oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oDemoOrg->GetKey(),]);
+ $oAllowedOrgList = $oUserWithAllowedOrgs->Get('allowed_org_list');
+ $oAllowedOrgList->AddItem($oUserOrg);
+ $oUserWithAllowedOrgs->Set('allowed_org_list', $oAllowedOrgList);
+ $oUserWithAllowedOrgs->DBWrite();
+
+ return $oUserWithAllowedOrgs;
+ }
}