From: colemanw Date: Sat, 9 Dec 2023 16:05:30 +0000 (-0500) Subject: Standalone - Add UserRole table X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=633e1285f32c609f23d7ba3d6f4699d4ee66d901;p=civicrm-core.git Standalone - Add UserRole table Adds bridge table to connect users with roles, which makes joins and complex queries possible. For ease-of-use this re-adds the civicrm_uf_match.roles field as a virtual calculated field. --- diff --git a/ext/standaloneusers/CRM/Standaloneusers/BAO/Role.php b/ext/standaloneusers/CRM/Standaloneusers/BAO/Role.php index e9e3ceef8c..95af217adb 100644 --- a/ext/standaloneusers/CRM/Standaloneusers/BAO/Role.php +++ b/ext/standaloneusers/CRM/Standaloneusers/BAO/Role.php @@ -1,7 +1,10 @@ action === 'delete') { - $users = \Civi\Api4\User::get(FALSE) - ->addSelect('id', 'roles') - ->addWhere('roles', 'CONTAINS', $event->id) - ->execute(); - foreach ($users as $user) { - $roles = array_diff($user['roles'], [$event->id]); - \Civi\Api4\User::update(FALSE) - ->addValue('roles', $roles) - ->addWhere('id', '=', $user['id']) - ->execute(); - } - } - // Reset cache Civi::cache('metadata')->clear(); } diff --git a/ext/standaloneusers/CRM/Standaloneusers/BAO/User.php b/ext/standaloneusers/CRM/Standaloneusers/BAO/User.php index bfbb09a837..d1bbdcb9c4 100644 --- a/ext/standaloneusers/CRM/Standaloneusers/BAO/User.php +++ b/ext/standaloneusers/CRM/Standaloneusers/BAO/User.php @@ -1,27 +1,59 @@ action, ['create', 'edit']) - && empty($event->params['when_updated'])) { + if ( + in_array($event->action, ['create', 'edit'], TRUE) && + empty($event->params['when_updated']) + ) { // Track when_updated. $event->params['when_updated'] = date('YmdHis'); } } + /** + * Event fired after an action is taken on a User record. + * @param \Civi\Core\Event\PostEvent $event + */ + public static function self_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) { + // Handle virtual "roles" field (defined in UserSpecProvider) + // @see \Civi\Api4\Service\Spec\Provider\UserSpecProvider + if ( + in_array($event->action, ['create', 'edit'], TRUE) && + isset($event->params['roles']) && $event->id + ) { + if ($event->params['roles']) { + $newRoles = array_map(function($role_id) { + return ['role_id' => $role_id]; + }, $event->params['roles']); + UserRole::replace(FALSE) + ->addWhere('user_id', '=', $event->id) + ->setRecords($newRoles) + ->execute(); + } + else { + UserRole::delete(FALSE) + ->addWhere('user_id', '=', $event->id) + ->execute(); + } + } + } + public static function updateLastAccessed() { $sess = CRM_Core_Session::singleton(); $ufID = (int) $sess->get('ufID'); diff --git a/ext/standaloneusers/CRM/Standaloneusers/DAO/User.php b/ext/standaloneusers/CRM/Standaloneusers/DAO/User.php index 4cc9d4b608..67331d4a2e 100644 --- a/ext/standaloneusers/CRM/Standaloneusers/DAO/User.php +++ b/ext/standaloneusers/CRM/Standaloneusers/DAO/User.php @@ -6,7 +6,7 @@ * * Generated from standaloneusers/xml/schema/CRM/Standaloneusers/User.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:889ac5b24fb6913d046bd2e52dcb65ea) + * (GenCodeChecksum:29abb6899d43b942155a37ffeddaa10d) */ use CRM_Standaloneusers_ExtensionUtil as E; @@ -109,15 +109,6 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { */ public $hashed_password; - /** - * FK to Role - * - * @var string|null - * (SQL type: varchar(128)) - * Note that values will be retrieved from the database as a string. - */ - public $roles; - /** * @var string|null * (SQL type: timestamp) @@ -377,37 +368,6 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'readonly' => TRUE, 'add' => NULL, ], - 'roles' => [ - 'name' => 'roles', - 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Roles'), - 'description' => E::ts('FK to Role'), - 'maxlength' => 128, - 'size' => CRM_Utils_Type::HUGE, - 'usage' => [ - 'import' => FALSE, - 'export' => FALSE, - 'duplicate_matching' => FALSE, - 'token' => FALSE, - ], - 'where' => 'civicrm_uf_match.roles', - 'table_name' => 'civicrm_uf_match', - 'entity' => 'User', - 'bao' => 'CRM_Standaloneusers_DAO_User', - 'localizable' => 0, - 'serialize' => self::SERIALIZE_SEPARATOR_BOOKEND, - 'html' => [ - 'type' => 'Select', - ], - 'pseudoconstant' => [ - 'table' => 'civicrm_role', - 'keyColumn' => 'id', - 'labelColumn' => 'label', - 'nameColumn' => 'name', - 'condition' => 'name != "everyone"', - ], - 'add' => NULL, - ], 'when_created' => [ 'name' => 'when_created', 'type' => CRM_Utils_Type::T_TIMESTAMP, diff --git a/ext/standaloneusers/CRM/Standaloneusers/DAO/UserRole.php b/ext/standaloneusers/CRM/Standaloneusers/DAO/UserRole.php new file mode 100644 index 0000000000..4544b45637 --- /dev/null +++ b/ext/standaloneusers/CRM/Standaloneusers/DAO/UserRole.php @@ -0,0 +1,245 @@ +__table = 'civicrm_user_role'; + parent::__construct(); + } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('User Roles') : E::ts('User Role'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'user_id', 'civicrm_uf_match', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'role_id', 'civicrm_role', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); + } + return Civi::$statics[__CLASS__]['links']; + } + + /** + * Returns all the column names of this table + * + * @return array + */ + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('ID'), + 'description' => E::ts('Unique UserRole ID'), + 'required' => TRUE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_user_role.id', + 'table_name' => 'civicrm_user_role', + 'entity' => 'UserRole', + 'bao' => 'CRM_Standaloneusers_DAO_UserRole', + 'localizable' => 0, + 'html' => [ + 'type' => 'Number', + ], + 'readonly' => TRUE, + 'add' => NULL, + ], + 'user_id' => [ + 'name' => 'user_id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('User ID'), + 'description' => E::ts('FK to User'), + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_user_role.user_id', + 'table_name' => 'civicrm_user_role', + 'entity' => 'UserRole', + 'bao' => 'CRM_Standaloneusers_DAO_UserRole', + 'localizable' => 0, + 'FKClassName' => 'CRM_Standaloneusers_DAO_User', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("User"), + ], + 'add' => NULL, + ], + 'role_id' => [ + 'name' => 'role_id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('Role ID'), + 'description' => E::ts('FK to Role'), + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_user_role.role_id', + 'table_name' => 'civicrm_user_role', + 'entity' => 'UserRole', + 'bao' => 'CRM_Standaloneusers_DAO_UserRole', + 'localizable' => 0, + 'FKClassName' => 'CRM_Standaloneusers_DAO_Role', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Role"), + ], + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); + } + return Civi::$statics[__CLASS__]['fields']; + } + + /** + * Return a mapping from field-name to the corresponding key (as used in fields()). + * + * @return array + * Array(string $name => string $uniqueName). + */ + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); + } + return Civi::$statics[__CLASS__]['fieldKeys']; + } + + /** + * Returns the names of this table + * + * @return string + */ + public static function getTableName() { + return self::$_tableName; + } + + /** + * Returns if this table needs to be logged + * + * @return bool + */ + public function getLog() { + return self::$_log; + } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'user_role', $prefix, []); + return $r; + } + + /** + * Returns the list of fields that can be exported + * + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'user_role', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; + } + +} diff --git a/ext/standaloneusers/Civi/Api4/Service/Spec/Provider/UserSpecProvider.php b/ext/standaloneusers/Civi/Api4/Service/Spec/Provider/UserSpecProvider.php index 486fa97f87..66bb99fc77 100644 --- a/ext/standaloneusers/Civi/Api4/Service/Spec/Provider/UserSpecProvider.php +++ b/ext/standaloneusers/Civi/Api4/Service/Spec/Provider/UserSpecProvider.php @@ -1,5 +1,4 @@ setTitle(ts('New password')); - $password->setDescription('Provide a new password for this user.'); - $password->setInputType('Password'); - $spec->addFieldSpec($password); + // Write-only `password` field + if (in_array($spec->getAction(), ['create', 'update', 'save'], TRUE)) { + $password = new FieldSpec('password', 'User', 'String'); + $password->setTitle(E::ts('New password')); + $password->setDescription(E::ts('Provide a new password for this user.')); + $password->setInputType('Password'); + $spec->addFieldSpec($password); + } + // Virtual "roles" field is a facade to the FK values in `civicrm_user_role`. + // It makes forms easier to write by acting as if user.roles were a simple field on the User record. + $roles = new FieldSpec('roles', 'User', 'Array'); + $roles->setTitle(E::ts('Roles')); + $roles->setDescription(E::ts('Role ids belonging to this user.')); + $roles->setInputType('Select'); + $roles->setInputAttrs(['multiple' => TRUE]); + $roles->setSerialize(\CRM_Core_DAO::SERIALIZE_COMMA); + $roles->setSuffixes(['id', 'name', 'label']); + $roles->setOptionsCallback([__CLASS__, 'getRolesOptions']); + $roles->setColumnName('id'); + $roles->setSqlRenderer([__CLASS__, 'getRolesSql']); + $spec->addFieldSpec($roles); + } + + public static function getRolesOptions(): array { + $roles = \Civi::cache('metadata')->get('user_roles'); + if (!$roles) { + $select = \CRM_Utils_SQL_Select::from('civicrm_role') + ->select(['id', 'name', 'label']) + ->where('is_active = 1') + ->where('name != "everyone"') + ->orderBy('label') + ->toSQL(); + $roles = \CRM_Core_DAO::executeQuery($select)->fetchAll(); + \Civi::cache('metadata')->set('user_roles', $roles); + } + return $roles; + } + + public static function getRolesSql(array $field, Api4SelectQuery $query): string { + return "(SELECT GROUP_CONCAT(ur.role_id) FROM civicrm_user_role ur WHERE ur.user_id = {$field['sql_name']})"; } /** * @inheritDoc */ public function applies($entity, $action) { - return $entity === 'User' && in_array($action, ['create', 'update', 'save']); + return $entity === 'User'; } } diff --git a/ext/standaloneusers/Civi/Api4/UserRole.php b/ext/standaloneusers/Civi/Api4/UserRole.php new file mode 100644 index 0000000000..75179552c8 --- /dev/null +++ b/ext/standaloneusers/Civi/Api4/UserRole.php @@ -0,0 +1,15 @@ +Hashed, not plaintext password - - roles - varchar - Roles - 128 - FK to Role - - civicrm_role
- id - label - name - name != "everyone" -
- - Select - - SEPARATOR_BOOKEND -
- when_created timestamp diff --git a/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.entityType.php b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.entityType.php new file mode 100644 index 0000000000..3ef64043b2 --- /dev/null +++ b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.entityType.php @@ -0,0 +1,10 @@ + 'UserRole', + 'class' => 'CRM_Standaloneusers_DAO_UserRole', + 'table' => 'civicrm_user_role', + ], +]; diff --git a/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.xml b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.xml new file mode 100644 index 0000000000..14cad17e31 --- /dev/null +++ b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/UserRole.xml @@ -0,0 +1,56 @@ + + + + CRM/Standaloneusers + UserRole + civicrm_user_role + Bridge between users and roles + true + + + id + int unsigned + true + Unique UserRole ID + + Number + + + + id + true + + + + user_id + int unsigned + FK to User + + + EntityRef + + + + user_id +
civicrm_uf_match
+ id + CASCADE + + + + role_id + int unsigned + FK to Role + + + EntityRef + + + + role_id + civicrm_role
+ id + CASCADE +
+ +