diff --git a/app/availableplugins/ApiConnector/src/Model/Entity/ApiSourceRecord.php b/app/availableplugins/ApiConnector/src/Model/Entity/ApiSourceRecord.php index be2180e98..8df244ead 100644 --- a/app/availableplugins/ApiConnector/src/Model/Entity/ApiSourceRecord.php +++ b/app/availableplugins/ApiConnector/src/Model/Entity/ApiSourceRecord.php @@ -48,4 +48,6 @@ class ApiSourceRecord extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable-related']; } diff --git a/app/plugins/CoreAssigner/config/plugin.json b/app/plugins/CoreAssigner/config/plugin.json index 1ab180799..ac0e35660 100644 --- a/app/plugins/CoreAssigner/config/plugin.json +++ b/app/plugins/CoreAssigner/config/plugin.json @@ -34,7 +34,8 @@ "format_assigner_sequences_i1": { "columns": [ "format_assigner_id", "affix" ], "unique": true }, "format_assigner_sequences_i2": { "needed": false, "columns": [ "format_assigner_id" ] } }, - "changelog": false + "changelog": false, + "clone_relation": true }, "sql_assigners": { "columns": { diff --git a/app/plugins/CoreAssigner/src/Model/Entity/FormatAssignerSequence.php b/app/plugins/CoreAssigner/src/Model/Entity/FormatAssignerSequence.php index 67684a398..98a8b0f09 100644 --- a/app/plugins/CoreAssigner/src/Model/Entity/FormatAssignerSequence.php +++ b/app/plugins/CoreAssigner/src/Model/Entity/FormatAssignerSequence.php @@ -48,4 +48,6 @@ class FormatAssignerSequence extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable-related']; } diff --git a/app/plugins/CoreServer/config/plugin.json b/app/plugins/CoreServer/config/plugin.json index 6fe058286..9c5fce7c7 100644 --- a/app/plugins/CoreServer/config/plugin.json +++ b/app/plugins/CoreServer/config/plugin.json @@ -68,8 +68,7 @@ }, "indexes": { "oauth2_servers_i1": { "columns": [ "server_id" ] } - }, - "clone_relation": true + } }, "smtp_servers": { "columns": { @@ -86,8 +85,7 @@ }, "indexes": { "smtp_servers_i1": { "columns": [ "server_id" ] } - }, - "clone_relation": true + } }, "sql_servers": { "columns": { @@ -102,8 +100,7 @@ }, "indexes": { "sql_servers_i1": { "columns": [ "server_id" ]} - }, - "clone_relation": true + } } } } diff --git a/app/plugins/CoreServer/src/Model/Entity/MatchServerAttribute.php b/app/plugins/CoreServer/src/Model/Entity/MatchServerAttribute.php index 07eb8102e..932135764 100644 --- a/app/plugins/CoreServer/src/Model/Entity/MatchServerAttribute.php +++ b/app/plugins/CoreServer/src/Model/Entity/MatchServerAttribute.php @@ -48,4 +48,6 @@ class MatchServerAttribute extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable-related']; } diff --git a/app/resources/locales/en_US/field.po b/app/resources/locales/en_US/field.po index 386338c05..5afbfafcb 100644 --- a/app/resources/locales/en_US/field.po +++ b/app/resources/locales/en_US/field.po @@ -243,6 +243,9 @@ msgstr "Order" msgid "organization" msgstr "Organization" +msgid "originalid" +msgstr "Original Record ID" + msgid "parameters" msgstr "Parameters" diff --git a/app/src/Command/CloneCommand.php b/app/src/Command/CloneCommand.php index 86606389d..be0253faa 100644 --- a/app/src/Command/CloneCommand.php +++ b/app/src/Command/CloneCommand.php @@ -474,11 +474,25 @@ protected function cloneEntity( if(!empty($hasOne)) { if($targetDataSource != 'default') { - // Prefix the hasOne relations (we're assuming only one level for now, - // so no recursion) with the data source label. - foreach($hasOne as $h) { + // Prefix the hasOne relations (we're assuming only one level for now, + // so no recursion) with the data source label. + $targetHasOne[] = $prefix.$h; + + // Also, while we're here prefix the property, if set. + // (eg: $copy['api_source'] -> $copy['remote_api_source']) + // (This could arguably be done in filterMetadataForCopy, but as a general + // rule we seem to be managing datasource-specific prefixing here in + // CloneCommand.) + + $property = Inflector::singularize(Inflector::underscore($h)); + $targetProperty = $targetDataSource . "_" . $property; + + if(array_key_exists($property, $copy)) { + $copy[$targetProperty] = $copy[$property]; + unset($copy[$property]); + } } } else { $targetHasOne = $hasOne; @@ -657,9 +671,8 @@ protected function cloneEntity( $this->io->out($sourceIterator->count() . " records in source to sync"); foreach($sourceIterator as $srcent) { - $targetent = $TargetRelatedTable->find() - ->where(['original_id' => $srcent->id]) + ->where(['originalid' => $srcent->id]) // There should be at most one ->first(); @@ -734,8 +747,8 @@ protected function cloneEntity( // correct it, but this is clearer) $targetent->$parent_key = $cloneId; - // Insert original_id, _after_ fixing the foreign keys - $targetent->original_id = $srcent->id; + // Insert originalid, _after_ fixing the foreign keys + $targetent->originalid = $srcent->id; $TargetRelatedTable->saveOrFail($targetent); @@ -810,7 +823,20 @@ protected function cloneEntity( } } - $tcxn->commit(); + // Perform any table specific follow up tasks. Unlike checkCloneDependencies + // this call may perform work. + + if(method_exists($TargetTable, "postClone")) { + try { + $TargetTable->postClone($clone, $targetDataSource); + } + catch(\Exception $e) { + // We catch the Exception to provide context + throw new \Exception($clone->uuid . ": postClone failed: " . $e->getMessage()); + } + } + + $tcxn->commit(); } catch(\Exception $e) { $this->io->err($e->getMessage()); @@ -819,6 +845,43 @@ protected function cloneEntity( $tcxn->rollback(); } } + + if(method_exists($Table, "getCloneSuccessors")) { + // Now clone any successor entities + + $uuids = $Table->getCloneSuccessors($original); + + // We accept both UUIDs and PaginatedSqlIterators in the array. We have to accept + // multiple PaginatedSqlIterators because each one can only handle a single model. + // Failure to clone a successor entity does _not_ fail the primary clone action, + // and does not prevent other successors from being cloned. + + $this->io->out("== Processing Successor Entities =="); + + foreach($uuids as $uuid) { + if(is_string($uuid)) { + // Simple UUID + try { + $this->cloneByUuid($uuid, $sourceCoId, $targetCoId, $targetDataSource); + } + catch(\Exception $e) { + $this->io->err($uuid . ": " . $e->getMessage()); + } + } else { + // PaginatedSqlIterator + foreach($uuid as $entity) { + try { + $this->cloneByUuid($entity->uuid, $sourceCoId, $targetCoId, $targetDataSource); + } + catch(\Exception $e) { + $this->io->err($uuid . ": " . $e->getMessage()); + } + } + } + } + + $this->io->out("== Finished Processing Successor Entities =="); + } } /** diff --git a/app/src/Lib/Traits/PluggableModelTrait.php b/app/src/Lib/Traits/PluggableModelTrait.php index 8f715b3f5..fc102ec78 100644 --- a/app/src/Lib/Traits/PluggableModelTrait.php +++ b/app/src/Lib/Traits/PluggableModelTrait.php @@ -123,29 +123,21 @@ public function checkCloneDependencies( // model (attached to the remote data source) at save(). (For further discussion, see // the comments in CloneCommand::execute().) We don't actually need the Table // objects here, we just want to make sure the TableRegistry is correctly set up. - - $related = TableUtilities::normalizeAssociationArray($this->getCloneHasMany()); - - $fn = function($related, $targetDataSource, $pluginName) use (&$fn) { - foreach($related as $rm => $ra) { - TableUtilities::getTableWithDataSource( - tableName: $pluginName.".".$rm, - connectionName: $targetDataSource - ); - $fn($ra, $targetDataSource, $pluginName); - } - }; - - // getCloneRelations() will return all possible relations for all plugins for the - // current model (eg if $original is ExternalIdentitySource, we'll get back an - // array of all EIS plugins), but we only want to instantiate related models - // for $plugin. + // We now only need to handle hasOne relations because hasMany relations are + // handled by iteration + + $related = TableUtilities::normalizeAssociationArray($this->getCloneHasOne()); - $pluginModel = StringUtilities::pluginPlugin($original->plugin); + if(array_key_exists($original->plugin, $related)) { + // getCloneRelations() will return all possible relations for all plugins for the + // current model (eg if $original is ExternalIdentitySource, we'll get back an + // array of all EIS plugins), but we only want to instantiate the active model - if(!empty($related[$pluginModel])) { - $fn($related[$pluginModel], $targetDataSource, $pluginName); + TableUtilities::getTableWithDataSource( + tableName: $original->plugin, + connectionName: $targetDataSource + ); } } } @@ -188,40 +180,12 @@ public function getCloneHasOne(): array { $ret = []; foreach($this->_pluginModels as $entryPoint) { - $ret[] = $entryPoint; //StringUtilities::pluginModel($entryPoint); + $ret[] = $entryPoint; } return $ret; } - /** - * Get the set of entities that are to be cloned after $original. - * - * The returned array may include both UUIDs (strings) and PaginatedSqlIterators, - * where the Iterator returns only clonable entities. - * - * @since COmanage Registry v5.2.0 - * @param EntityInterface $original Current entity being cloned - * @return array Array of UUIDs and/or PaginatedSqlIterators - */ - - public function getCloneSuccessors( - \Cake\Datasource\EntityInterface $original - ): array { - // We don't really know whether we need to use PaginatedSqlIterator for every - // hasMany relation (without adding annotations of some form), and indeed in most - // cases we probably don't need it (smaller deployments, models with only a few - // related entities), but for the cases where we need it we really need it - // (ApiSourceRecords, EnvSourceIdentities) so we always use it. (The overhead - // for smoller data sets should be marginal.) - - // Because PaginatedSqlIterators only operate over a single table, we need to - // return one per hasMany relation. - $ret = []; - - return $ret; - } - /** * Determine the plugin type used by this Pluggable Model. This is the lowercased * singular prefix of the Pluggable Model Table name. eg: For "ReportsTable" the diff --git a/app/src/Lib/Util/SchemaManager.php b/app/src/Lib/Util/SchemaManager.php index d9b2a0a97..e6eb9f7ac 100644 --- a/app/src/Lib/Util/SchemaManager.php +++ b/app/src/Lib/Util/SchemaManager.php @@ -272,10 +272,11 @@ protected function processSchema( // Add metadata fields for Clonable Related Models. Eventually this setting // should probably be the default, and disabled when not needed (like changelog). - // original_id is _not_ given a foreign key constraint because the Original - // Object may be in a different database. - $table->addColumn("original_id", "integer", ['notnull' => false]); - $table->addIndex(["original_id"], $tablePrefix.$tName."_icb", [], []); + // We call this "originalid" and not "original_id" because it's not a proper + // foreign key, since minimally it crosses a CO (which would violate AR-GMR-2) + // and maybe crosses CMPs (in which case it's a foreign key to another database). + $table->addColumn("originalid", "integer", ['notnull' => false]); + $table->addIndex(["originalid"], $tablePrefix.$tName."_icb", [], []); } // (For Registry) If MVEA models are specified, emit the appropriate diff --git a/app/src/Model/Entity/ApiUser.php b/app/src/Model/Entity/ApiUser.php index 91628df84..3b40fe77f 100644 --- a/app/src/Model/Entity/ApiUser.php +++ b/app/src/Model/Entity/ApiUser.php @@ -44,6 +44,8 @@ class ApiUser extends Entity { // can set api_key. (AR-ApiUser-4) 'api_key' => false ]; + + public array $_supportedMetadata = ['clonable']; /** * Hash (bcrypt) an API Key on save. diff --git a/app/src/Model/Entity/Cou.php b/app/src/Model/Entity/Cou.php index 2fb346906..5c40bea02 100644 --- a/app/src/Model/Entity/Cou.php +++ b/app/src/Model/Entity/Cou.php @@ -39,4 +39,6 @@ class Cou extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable']; } \ No newline at end of file diff --git a/app/src/Model/Entity/ExternalIdentitySource.php b/app/src/Model/Entity/ExternalIdentitySource.php index 80656d6cd..9af03424f 100644 --- a/app/src/Model/Entity/ExternalIdentitySource.php +++ b/app/src/Model/Entity/ExternalIdentitySource.php @@ -39,4 +39,6 @@ class ExternalIdentitySource extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable']; } \ No newline at end of file diff --git a/app/src/Model/Entity/Group.php b/app/src/Model/Entity/Group.php index a8468edae..8c7a30bf6 100644 --- a/app/src/Model/Entity/Group.php +++ b/app/src/Model/Entity/Group.php @@ -42,6 +42,8 @@ class Group extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable']; /** * Determine if this entity record can be deleted. @@ -73,6 +75,7 @@ public function isAllMembers(): bool { */ public function isAutomatic(): bool { + // If this list is updated, CousTable::getCloneSuccessors will need to be updated as well return in_array($this->group_type, [GroupTypeEnum::ActiveMembers, GroupTypeEnum::AllMembers]); } diff --git a/app/src/Model/Entity/IdentifierAssignment.php b/app/src/Model/Entity/IdentifierAssignment.php index a186eca4c..85cd5dde8 100644 --- a/app/src/Model/Entity/IdentifierAssignment.php +++ b/app/src/Model/Entity/IdentifierAssignment.php @@ -39,4 +39,6 @@ class IdentifierAssignment extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable']; } \ No newline at end of file diff --git a/app/src/Model/Entity/Pipeline.php b/app/src/Model/Entity/Pipeline.php index 71831343d..7ea15c4a8 100644 --- a/app/src/Model/Entity/Pipeline.php +++ b/app/src/Model/Entity/Pipeline.php @@ -39,4 +39,6 @@ class Pipeline extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable']; } \ No newline at end of file diff --git a/app/src/Model/Entity/ProvisioningTarget.php b/app/src/Model/Entity/ProvisioningTarget.php index ccd484ad9..f59c0ea66 100644 --- a/app/src/Model/Entity/ProvisioningTarget.php +++ b/app/src/Model/Entity/ProvisioningTarget.php @@ -41,6 +41,8 @@ class ProvisioningTarget extends Entity { 'slug' => false, ]; + public array $_supportedMetadata = ['clonable']; + /** * Determine if this entity is Active. * diff --git a/app/src/Model/Entity/Server.php b/app/src/Model/Entity/Server.php index 9f94981ad..52b1fe9cc 100644 --- a/app/src/Model/Entity/Server.php +++ b/app/src/Model/Entity/Server.php @@ -39,4 +39,6 @@ class Server extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable']; } \ No newline at end of file diff --git a/app/src/Model/Entity/Type.php b/app/src/Model/Entity/Type.php index 77a732059..51d11b46f 100644 --- a/app/src/Model/Entity/Type.php +++ b/app/src/Model/Entity/Type.php @@ -39,4 +39,6 @@ class Type extends Entity { 'id' => false, 'slug' => false, ]; + + public array $_supportedMetadata = ['clonable']; } \ No newline at end of file diff --git a/app/src/Model/Table/CousTable.php b/app/src/Model/Table/CousTable.php index b670ce33f..475e1f32c 100644 --- a/app/src/Model/Table/CousTable.php +++ b/app/src/Model/Table/CousTable.php @@ -35,8 +35,10 @@ use Cake\ORM\RulesChecker; use Cake\ORM\Table; use Cake\ORM\TableRegistry; +use Cake\Utility\Hash; use Cake\Validation\Validator; +use \App\Lib\Enum\GroupTypeEnum; use \App\Lib\Enum\StatusEnum; use \App\Lib\Enum\ProvisioningEligibilityEnum; use \App\Lib\Util\StringUtilities; @@ -163,6 +165,35 @@ public function buildRules(RulesChecker $rules): RulesChecker { return $rules; } + + /** + * Get the set of entities that are to be cloned after $original. + * + * The returned array may include both UUIDs (strings) and PaginatedSqlIterators, + * where the Iterator returns only clonable entities. + * + * @since COmanage Registry v5.2.0 + * @param EntityInterface $original Current entity being cloned + * @return array Array of UUIDs and/or PaginatedSqlIterators + */ + + public function getCloneSuccessors( + \Cake\Datasource\EntityInterface $original + ): array { + // Pull the set of non-automatic COU Groups and return their UUIDs + + $clonableGroups = $this->Groups->find() + ->where([ + 'cou_id' => $original->id, + 'group_type NOT IN' => [ + GroupTypeEnum::ActiveMembers, + GroupTypeEnum::AllMembers + ] + ]) + ->all(); + + return $clonableGroups->extract('uuid')->toArray(); + } /** * Callback after model save. @@ -176,7 +207,11 @@ public function buildRules(RulesChecker $rules): RulesChecker { public function localAfterSave(\Cake\Event\EventInterface $event, \Cake\Datasource\EntityInterface $entity, \ArrayObject $options) { if(isset($options['clone']) && $options['clone']) { - // If we're in the middle of cloning, don't run setup or addDefaults + // If we're in the middle of cloning, don't run setup or addDefaults. + // This is because CloneCommand needs to specially handle the data source + // and UUID syncing, and in edge cases it's possible that addDefaults() is not + // the right behavior because a COU was created before additional default Groups + // were added (though this should be pretty rare). return; } @@ -226,6 +261,34 @@ public function marshalProvisioningData(int $id): array { return $ret; } + + /** + * Check for any dependencies that must be in place before cloning begins. + * + * @since COmanage Registry v5.2.0 + * @param EntityInterface $clone Cloned entity + * @param string $targetDataSource Target DataSource connection name + */ + + public function postClone( + \Cake\Datasource\EntityInterface $clone, + string $targetDataSource='default' + ) { + // We need to call addDefaults on the Target datasource, but only for + // automatic Groups. (Non-automatic Groups are handled by getCloneSuccessors.) + + $TargetGroups = TableUtilities::getTableWithDataSource( + tableName: "Groups", + connectionName: $targetDataSource + ); + + $TargetGroups->addDefaults( + coId: $clone->co_id, + couId: $clone->cou_id, + rename: true, // Allow renaming on updates + autoOnly: true + ); + } /** * Perform initial setup for a COU. diff --git a/app/src/Model/Table/GroupsTable.php b/app/src/Model/Table/GroupsTable.php index 9452f3e05..05f5da4e4 100644 --- a/app/src/Model/Table/GroupsTable.php +++ b/app/src/Model/Table/GroupsTable.php @@ -37,6 +37,7 @@ use Cake\ORM\TableRegistry; use Cake\Validation\Validator; use \App\Lib\Util\PaginatedSqlIterator; +use \App\Lib\Util\TableUtilities; use \App\Lib\Enum\ActionEnum; use \App\Lib\Enum\GroupTypeEnum; use \App\Lib\Enum\ProvisioningEligibilityEnum; @@ -265,19 +266,31 @@ public function initialize(array $config): void { * Add the system groups for a CO or COU. (AR-CO-6, AR-COU-4) * * @since COmanage Registry v5.0.0 - * @param int $coId CO ID - * @param int $couId COU ID - * @param bool $rename If true, rename any existing groups - * @return bool True on success + * @param int $coId CO ID + * @param int $couId COU ID + * @param bool $rename If true, rename any existing groups + * @param bool $autoOnly If true, only process automatic groups + * @param string $dataSource Datasource to use (primarily intended for use with cloning) + * @return bool True on success * @throws InvalidArgumentException * @throws RuntimeException * @throws PersistenceFailedException */ - public function addDefaults(int $coId, ?int $couId=null, bool $rename=false): bool { - // Pull the name of the CO/COU + public function addDefaults( + int $coId, + ?int $couId=null, + bool $rename=false, + bool $autoOnly=false, + string $dataSource='default' + ): bool { + // Pull the name of the CO/COU, making sure to use the correct datasource so + // we get the correct name when cloning - $Cos = TableRegistry::getTableLocator()->get('Cos'); + $Cos = TableUtilities::getTableWithDataSource( + tableName: "Cos", + connectionName: $dataSource + ); try { $co = $Cos->get($coId); @@ -289,7 +302,10 @@ public function addDefaults(int $coId, ?int $couId=null, bool $rename=false): bo $couName = null; if($couId) { - $Cous = TableRegistry::getTableLocator()->get('Cous'); + $Cous = TableUtilities::getTableWithDataSource( + tableName: "Cous", + connectionName: $dataSource + ); try { $cou = $Cous->get($couId); @@ -306,6 +322,7 @@ public function addDefaults(int $coId, ?int $couId=null, bool $rename=false): bo $defaultGroups = [ ':admins' => [ 'group_type' => GroupTypeEnum::Admins, + // Note 'auto' isn't a field anymore, but we use it below before saving 'auto' => false, 'description' => __d('field', 'Groups.desc.admins', [$couName ?: $co->name]), 'open' => false, @@ -352,6 +369,12 @@ public function addDefaults(int $coId, ?int $couId=null, bool $rename=false): bo } foreach($defaultGroups as $suffix => $attrs) { + // $autoOnly is intended to support cloning, which needs to manually manage + // non-automatic Groups + if($autoOnly && !$attrs['auto']) { + continue; + } + // Construct the full group name $gname = "CO" . ($couName ? ":COU:".$couName : "") . $suffix; @@ -361,9 +384,9 @@ public function addDefaults(int $coId, ?int $couId=null, bool $rename=false): bo $grp = $this->find() ->where([ - 'Groups.co_id' => $coId, - 'Groups.group_type' => $attrs['group_type'], - 'Groups.cou_id IS' => $couId ?: null + 'co_id' => $coId, + 'group_type' => $attrs['group_type'], + 'cou_id IS' => $couId ?: null ]) ->first(); @@ -374,7 +397,7 @@ public function addDefaults(int $coId, ?int $couId=null, bool $rename=false): bo $entity->co_id = $coId; $entity->name = $gname; - if(!$this->save($entity)) { + if(!$this->save($entity, options: ['autoOnly' => $autoOnly])) { throw new \RuntimeException(__d('error', 'save', ['GroupsTable::addDefaults'])); } } elseif($rename) { @@ -382,7 +405,7 @@ public function addDefaults(int $coId, ?int $couId=null, bool $rename=false): bo $grp->name = $gname; $grp->description = $attrs['description']; - if(!$this->save($grp)) { + if(!$this->save($grp, options: ['autoOnly' => $autoOnly])) { throw new \RuntimeException(__d('error', 'save', ['GroupsTable::addDefaults'])); } } @@ -524,6 +547,28 @@ public function buildRules(RulesChecker $rules): RulesChecker { return $rules; } + + /** + * Check for any dependencies that must be in place before cloning begins. + * + * @since COmanage Registry v5.2.0 + * @param EntityInterface $original Original entity + * @param string $targetDataSource Target DataSource connection name + */ + + public function checkCloneDependencies( + EntityInterface $original, + string $targetDataSource='default' + ) { + // We don't clone Automatic Groups. Those should be created when the related structure + // (ie: a COU) is created, and then updated automatically as members are cloned. + + if($original->isAutomatic()) { + // This string isn't internationalized because it is intended to render + // in CloneCommand output + throw new \InvalidArgumentException("Group " . $original->id . " is an automatic group, skipping..."); + } + } /** * Create an Owners Group for the requested Group. @@ -612,28 +657,6 @@ public function getAdminGroupId(int $coId): int { return $g->id; } - /** - * Check for any dependencies that must be in place before cloning begins. - * - * @since COmanage Registry v5.2.0 - * @param EntityInterface $original Original entity - * @param string $targetDataSource Target DataSource connection name - */ - - public function checkCloneDependencies( - EntityInterface $original, - string $targetDataSource='default' - ) { - // We don't clone Automatic Groups. Those should be created when the related structure - // (ie: a COU) is created, and then updated automatically as members are cloned. - - if($original->isAutomatic()) { - // This string isn't internationalized because it is intended to render - // in CloneCommand output - throw new \InvalidArgumentException("Group " . $original->id . " is an automatic group, skipping..."); - } - } - /** * Obtain the fully qualified name for the Group, which will include all * parent names, separated by colons. @@ -822,19 +845,26 @@ public function implementedEvents(): array { */ public function localAfterSave(EventInterface $event, EntityInterface $entity, \ArrayObject $options): bool { - if($entity->isNew()) { - $action = ActionEnum::GroupAdded; - $comment = __d('result', 'Groups.added', [$entity->name]); - } elseif($entity->get('deleted')) { - $action = ActionEnum::GroupDeleted; - $comment = __d('result', 'Groups.deleted', [$entity->name]); - } else { - $action = ActionEnum::GroupEdited; - $comment = __d('result', 'Groups.edited', [$entity->name, $this->changesToString($entity)]); + // We don't record history if autoOnly is set because we're in the middle of cloning + // and aside from the datasources not lining up, it's not clear it makes sense to record + // the history in that context + + if((!isset($options['autoOnly']) || !$options['autoOnly']) + && (!isset($options['clone']) || !$options['clone'])) { + if($entity->isNew()) { + $action = ActionEnum::GroupAdded; + $comment = __d('result', 'Groups.added', [$entity->name]); + } elseif($entity->get('deleted')) { + $action = ActionEnum::GroupDeleted; + $comment = __d('result', 'Groups.deleted', [$entity->name]); + } else { + $action = ActionEnum::GroupEdited; + $comment = __d('result', 'Groups.edited', [$entity->name, $this->changesToString($entity)]); + } + + $this->recordHistory($entity, $action, $comment); } - $this->recordHistory($entity, $action, $comment); - if(!$entity->isOwners()) { if($entity->isNew()) { // When a new Group is created, create the owners Group for it. diff --git a/app/templates/ApiUsers/fields.inc b/app/templates/ApiUsers/fields.inc index 5bebf489a..8dfef02eb 100644 --- a/app/templates/ApiUsers/fields.inc +++ b/app/templates/ApiUsers/fields.inc @@ -63,5 +63,3 @@ $fields = [ 'remote_ip', // string 'privileged', // boolean ]; - -$fields = array_merge($fields, include(ROOT . DS . 'templates' . DS . 'Standard/metadata.inc')); \ No newline at end of file diff --git a/app/templates/Cous/fields.inc b/app/templates/Cous/fields.inc index a7aadc109..8a6f24e43 100644 --- a/app/templates/Cous/fields.inc +++ b/app/templates/Cous/fields.inc @@ -35,5 +35,3 @@ if(!empty($parents)) { 'fieldLabel' => __d('field', 'parent_id') ]; } - -$fields = array_merge($fields, include(ROOT . DS . 'templates' . DS . 'Standard/metadata.inc')); \ No newline at end of file diff --git a/app/templates/ExternalIdentitySources/fields.inc b/app/templates/ExternalIdentitySources/fields.inc index 6d2d0a041..d8fab275a 100644 --- a/app/templates/ExternalIdentitySources/fields.inc +++ b/app/templates/ExternalIdentitySources/fields.inc @@ -36,8 +36,6 @@ $fields = [ 'hash_source_record', 'suppress_noop_logs' ]; - -$fields = array_merge($fields, include(ROOT . DS . 'templates' . DS . 'Standard/metadata.inc')); // Top Links $topLinks = [ diff --git a/app/templates/Groups/fields.inc b/app/templates/Groups/fields.inc index f5b575bdc..51fb95aae 100644 --- a/app/templates/Groups/fields.inc +++ b/app/templates/Groups/fields.inc @@ -72,8 +72,6 @@ if($vv_action != 'add') { } } -$fields = array_merge($fields, include(ROOT . DS . 'templates' . DS . 'Standard/metadata.inc')); - // List the MVEAs that may be shown on the mveaCanvas // When this array exists, the mveaCanvas.php element will render $mveas = [ diff --git a/app/templates/IdentifierAssignments/fields.inc b/app/templates/IdentifierAssignments/fields.inc index 8050a9417..662a4b119 100644 --- a/app/templates/IdentifierAssignments/fields.inc +++ b/app/templates/IdentifierAssignments/fields.inc @@ -37,8 +37,6 @@ $fields = [ 'allow_empty', 'ordr' ]; - -$fields = array_merge($fields, include(ROOT . DS . 'templates' . DS . 'Standard/metadata.inc')); ?>