From fe0924af66a3e79d6a6d82858f43acb240800533 Mon Sep 17 00:00:00 2001 From: Rich Lott / Artful Robot Date: Fri, 29 Sep 2023 16:51:38 +0100 Subject: [PATCH] standalone: superset the uf_match table for User entity --- CRM/Core/BAO/CMSUser.php | 9 +- .../CRM/Standaloneusers/DAO/User.php | 250 ++++++++++++------ .../Api4/Action/User/SendPasswordReset.php | 23 +- .../Civi/Api4/Action/User/WriteTrait.php | 14 +- .../Civi/Standalone/Security.php | 19 +- .../ang/afformEditUserAccount.aff.html | 2 +- .../afsearchAdministerUserAccounts.aff.html | 10 +- .../SavedSearch_Administer_Users.mgd.php | 4 +- ext/standaloneusers/sql/auto_install.sql | 22 +- ext/standaloneusers/sql/auto_uninstall.sql | 2 +- .../CRM/Standaloneusers/Page/Login.tpl | 6 +- .../phpunit/Civi/Standalone/SecurityTest.php | 24 +- .../CRM/Standaloneusers/User.entityType.php | 2 +- .../xml/schema/CRM/Standaloneusers/User.xml | 103 ++++++-- 14 files changed, 335 insertions(+), 155 deletions(-) diff --git a/CRM/Core/BAO/CMSUser.php b/CRM/Core/BAO/CMSUser.php index e43a7b4998..c5f01e496a 100644 --- a/CRM/Core/BAO/CMSUser.php +++ b/CRM/Core/BAO/CMSUser.php @@ -40,9 +40,12 @@ class CRM_Core_BAO_CMSUser { $ufID = $config->userSystem->createUser($params, $mailParam); - //if contact doesn't already exist create UF Match - if ($ufID !== FALSE && - isset($params['contactID']) + // Create UF Match if we have contactID unless we're Standalone + // since in Standalone uf_match is the same table as User. + if ( + CIVICRM_UF !== 'Standalone' + && $ufID !== FALSE + && isset($params['contactID']) ) { // create the UF Match record $ufmatch['uf_id'] = $ufID; diff --git a/ext/standaloneusers/CRM/Standaloneusers/DAO/User.php b/ext/standaloneusers/CRM/Standaloneusers/DAO/User.php index df4d4a3bce..92343ccf1d 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:12f08a726067ec0e3cc7f45411d9526a) + * (GenCodeChecksum:5769c2469482e66ebeec050ea3e82a97) */ use CRM_Standaloneusers_ExtensionUtil as E; @@ -22,7 +22,7 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { * * @var string */ - public static $_tableName = 'civicrm_user'; + public static $_tableName = 'civicrm_uf_match'; /** * Field to show when displaying a record. @@ -58,7 +58,34 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { public $id; /** - * FK to Contact - possibly redundant + * Which Domain is this match entry for + * + * @var int|string + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $domain_id; + + /** + * UF ID. Redundant in Standalone. Needs to be identical to id. + * + * @var int|string + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. + */ + public $uf_id; + + /** + * Email (e.g. for password resets) + * + * @var string|null + * (SQL type: varchar(255)) + * Note that values will be retrieved from the database as a string. + */ + public $uf_name; + + /** + * FK to Contact ID * * @var int|string|null * (SQL type: int unsigned) @@ -82,15 +109,6 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { */ public $hashed_password; - /** - * Email (e.g. for password resets) - * - * @var string - * (SQL type: varchar(255)) - * Note that values will be retrieved from the database as a string. - */ - public $email; - /** * FK to Role * @@ -138,10 +156,10 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { public $timezone; /** - * The language for the user. + * UI language preferred by the given user/contact * - * @var int|string|null - * (SQL type: int unsigned) + * @var string|null + * (SQL type: varchar(5)) * Note that values will be retrieved from the database as a string. */ public $language; @@ -150,7 +168,7 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { * Class constructor. */ public function __construct() { - $this->__table = 'civicrm_user'; + $this->__table = 'civicrm_uf_match'; parent::__construct(); } @@ -173,6 +191,7 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 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(), 'domain_id', 'civicrm_domain', 'id'); Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'contact_id', 'civicrm_contact', 'id'); CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); } @@ -190,7 +209,7 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('ID'), + 'title' => E::ts('UF Match ID'), 'description' => E::ts('Unique User ID'), 'required' => TRUE, 'usage' => [ @@ -199,8 +218,8 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.id', - 'table_name' => 'civicrm_user', + 'where' => 'civicrm_uf_match.id', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, @@ -208,26 +227,100 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'type' => 'Number', ], 'readonly' => TRUE, + 'add' => '5.67', + ], + 'domain_id' => [ + 'name' => 'domain_id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('Domain ID'), + 'description' => E::ts('Which Domain is this match entry for'), + 'required' => TRUE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_uf_match.domain_id', + 'table_name' => 'civicrm_uf_match', + 'entity' => 'User', + 'bao' => 'CRM_Standaloneusers_DAO_User', + 'localizable' => 0, + 'FKClassName' => 'CRM_Core_DAO_Domain', + 'html' => [ + 'label' => E::ts("Domain"), + ], + 'pseudoconstant' => [ + 'table' => 'civicrm_domain', + 'keyColumn' => 'id', + 'labelColumn' => 'name', + ], + 'add' => '3.0', + ], + 'uf_id' => [ + 'name' => 'uf_id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('CMS ID'), + 'description' => E::ts('UF ID. Redundant in Standalone. Needs to be identical to id.'), + 'required' => TRUE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_uf_match.uf_id', + 'default' => '0', + 'table_name' => 'civicrm_uf_match', + 'entity' => 'User', + 'bao' => 'CRM_Standaloneusers_DAO_User', + 'localizable' => 0, + 'add' => '1.1', + ], + 'uf_name' => [ + 'name' => 'uf_name', + 'type' => CRM_Utils_Type::T_STRING, + 'title' => E::ts('CMS Unique Identifier'), + 'description' => E::ts('Email (e.g. for password resets)'), + 'maxlength' => 255, + 'size' => CRM_Utils_Type::HUGE, + 'usage' => [ + 'import' => FALSE, + 'export' => FALSE, + 'duplicate_matching' => FALSE, + 'token' => FALSE, + ], + 'where' => 'civicrm_uf_match.uf_name', + 'table_name' => 'civicrm_uf_match', + 'entity' => 'User', + 'bao' => 'CRM_Standaloneusers_DAO_User', + 'localizable' => 0, + 'html' => [ + 'type' => 'Email', + ], 'add' => NULL, ], 'contact_id' => [ 'name' => 'contact_id', 'type' => CRM_Utils_Type::T_INT, 'title' => E::ts('Contact ID'), - 'description' => E::ts('FK to Contact - possibly redundant'), + 'description' => E::ts('FK to Contact ID'), 'usage' => [ 'import' => FALSE, 'export' => FALSE, 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.contact_id', - 'table_name' => 'civicrm_user', + 'where' => 'civicrm_uf_match.contact_id', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, 'FKClassName' => 'CRM_Contact_DAO_Contact', - 'add' => NULL, + 'html' => [ + 'label' => E::ts("Contact"), + ], + 'add' => '1.1', ], 'username' => [ 'name' => 'username', @@ -242,8 +335,8 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.username', - 'table_name' => 'civicrm_user', + 'where' => 'civicrm_uf_match.username', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, @@ -266,38 +359,14 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.hashed_password', - 'table_name' => 'civicrm_user', + 'where' => 'civicrm_uf_match.hashed_password', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, 'readonly' => TRUE, 'add' => NULL, ], - 'email' => [ - 'name' => 'email', - 'type' => CRM_Utils_Type::T_STRING, - 'title' => E::ts('Email'), - 'description' => E::ts('Email (e.g. for password resets)'), - 'required' => TRUE, - 'maxlength' => 255, - 'size' => CRM_Utils_Type::HUGE, - 'usage' => [ - 'import' => FALSE, - 'export' => FALSE, - 'duplicate_matching' => FALSE, - 'token' => FALSE, - ], - 'where' => 'civicrm_user.email', - 'table_name' => 'civicrm_user', - 'entity' => 'User', - 'bao' => 'CRM_Standaloneusers_DAO_User', - 'localizable' => 0, - 'html' => [ - 'type' => 'Text', - ], - 'add' => NULL, - ], 'roles' => [ 'name' => 'roles', 'type' => CRM_Utils_Type::T_STRING, @@ -311,8 +380,8 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.roles', - 'table_name' => 'civicrm_user', + 'where' => 'civicrm_uf_match.roles', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, @@ -339,9 +408,9 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.when_created', + 'where' => 'civicrm_uf_match.when_created', 'default' => 'CURRENT_TIMESTAMP', - 'table_name' => 'civicrm_user', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, @@ -358,8 +427,8 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.when_last_accessed', - 'table_name' => 'civicrm_user', + 'where' => 'civicrm_uf_match.when_last_accessed', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, @@ -376,8 +445,8 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.when_updated', - 'table_name' => 'civicrm_user', + 'where' => 'civicrm_uf_match.when_updated', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, @@ -394,9 +463,9 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.is_active', + 'where' => 'civicrm_uf_match.is_active', 'default' => '1', - 'table_name' => 'civicrm_user', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, @@ -419,8 +488,8 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.timezone', - 'table_name' => 'civicrm_user', + 'where' => 'civicrm_uf_match.timezone', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, @@ -431,28 +500,23 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { ], 'language' => [ 'name' => 'language', - 'type' => CRM_Utils_Type::T_INT, - 'title' => E::ts('Language'), - 'description' => E::ts('The language for the user.'), + 'type' => CRM_Utils_Type::T_STRING, + 'title' => E::ts('Preferred Language'), + 'description' => E::ts('UI language preferred by the given user/contact'), + 'maxlength' => 5, + 'size' => CRM_Utils_Type::SIX, 'usage' => [ 'import' => FALSE, 'export' => FALSE, 'duplicate_matching' => FALSE, 'token' => FALSE, ], - 'where' => 'civicrm_user.language', - 'table_name' => 'civicrm_user', + 'where' => 'civicrm_uf_match.language', + 'table_name' => 'civicrm_uf_match', 'entity' => 'User', 'bao' => 'CRM_Standaloneusers_DAO_User', 'localizable' => 0, - 'html' => [ - 'type' => 'Select', - ], - 'pseudoconstant' => [ - 'optionGroupName' => 'languages', - 'optionEditPath' => 'civicrm/admin/options/languages', - ], - 'add' => NULL, + 'add' => '2.1', ], ]; CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); @@ -499,7 +563,7 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { * @return array */ public static function &import($prefix = FALSE) { - $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'user', $prefix, []); + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'uf_match', $prefix, []); return $r; } @@ -511,7 +575,7 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { * @return array */ public static function &export($prefix = FALSE) { - $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'user', $prefix, []); + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'uf_match', $prefix, []); return $r; } @@ -524,6 +588,14 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { */ public static function indices($localize = TRUE) { $indices = [ + 'I_civicrm_uf_match_uf_id' => [ + 'name' => 'I_civicrm_uf_match_uf_id', + 'field' => [ + 0 => 'uf_id', + ], + 'localizable' => FALSE, + 'sig' => 'civicrm_uf_match::0::uf_id', + ], 'UI_username' => [ 'name' => 'UI_username', 'field' => [ @@ -531,7 +603,27 @@ class CRM_Standaloneusers_DAO_User extends CRM_Core_DAO { ], 'localizable' => FALSE, 'unique' => TRUE, - 'sig' => 'civicrm_user::1::username', + 'sig' => 'civicrm_uf_match::1::username', + ], + 'UI_uf_name_domain_id' => [ + 'name' => 'UI_uf_name_domain_id', + 'field' => [ + 0 => 'uf_name', + 1 => 'domain_id', + ], + 'localizable' => FALSE, + 'unique' => TRUE, + 'sig' => 'civicrm_uf_match::1::uf_name::domain_id', + ], + 'UI_contact_domain_id' => [ + 'name' => 'UI_contact_domain_id', + 'field' => [ + 0 => 'contact_id', + 1 => 'domain_id', + ], + 'localizable' => FALSE, + 'unique' => TRUE, + 'sig' => 'civicrm_uf_match::1::contact_id::domain_id', ], ]; return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; diff --git a/ext/standaloneusers/Civi/Api4/Action/User/SendPasswordReset.php b/ext/standaloneusers/Civi/Api4/Action/User/SendPasswordReset.php index bb0677b362..42de24a867 100644 --- a/ext/standaloneusers/Civi/Api4/Action/User/SendPasswordReset.php +++ b/ext/standaloneusers/Civi/Api4/Action/User/SendPasswordReset.php @@ -30,11 +30,11 @@ class SendPasswordReset extends AbstractAction { } $user = User::get(FALSE) - ->addSelect('id', 'email', 'username') + ->addSelect('id', 'uf_name', 'username') ->addWhere('is_active', '=', TRUE) ->setLimit(1) ->addWhere( - filter_var($identifier, FILTER_VALIDATE_EMAIL) ? 'email' : 'username', + filter_var($identifier, FILTER_VALIDATE_EMAIL) ? 'uf_name' : 'username', '=', $identifier) ->execute() @@ -73,21 +73,21 @@ class SendPasswordReset extends AbstractAction { ->setSelect(['id']) ->addWhere('workflow_name', '=', 'password_reset') ->addWhere('is_default', '=', TRUE) - ->addWhere('is_active', '=', TRUE) + ->addWhere('is_active', '=', TRUE) ->execute()->first()['id']; if (!$tplID) { // Some sites may deliberately disable this, but it's unusual, so leave a notice in the log. Civi::log()->notice("There is no active, default password_reset message template, which has prevented emailing a reset to {username}", ['username' => $user['username']]); return; } - if (!filter_var($user['email'] ?? '', FILTER_VALIDATE_EMAIL)) { + if (!filter_var($user['uf_name'] ?? '', FILTER_VALIDATE_EMAIL)) { Civi::log()->warning("User $user[id] has an invalid email. Failed to send password reset."); return; } // Generate a once-use token that expires in 1 hour. // We'll store this on the User record, that way invalidating any previous token that may have been generated. - $expires = time() + 60*60; + $expires = time() + 60 * 60; $token = dechex($expires) . substr(preg_replace('@[/+=]+@', '', base64_encode(random_bytes(64))), 0, 32); User::update(FALSE) @@ -101,12 +101,12 @@ class SendPasswordReset extends AbstractAction { $resetUrlHtml = htmlspecialchars($resetUrlPlaintext); // The template_params are used in the template like {$resetUrlHtml} and {$resetUrlHtml} $params = [ - 'id' => $tplID, - 'template_params' => compact('resetUrlPlaintext', 'resetUrlHtml'), - 'from' => "\"$domainFromName\" <$domainFromEmail>", - 'to_email' => $user['email'], - 'disable_smarty' => 1, - ]; + 'id' => $tplID, + 'template_params' => compact('resetUrlPlaintext', 'resetUrlHtml'), + 'from' => "\"$domainFromName\" <$domainFromEmail>", + 'to_email' => $user['uf_name'], + 'disable_smarty' => 1, + ]; try { civicrm_api3('MessageTemplate', 'send', $params); @@ -118,4 +118,5 @@ class SendPasswordReset extends AbstractAction { $params + ['userID' => $user['id'], 'exception' => $e]); } } + } diff --git a/ext/standaloneusers/Civi/Api4/Action/User/WriteTrait.php b/ext/standaloneusers/Civi/Api4/Action/User/WriteTrait.php index 600a09dad8..ec35fe65e0 100644 --- a/ext/standaloneusers/Civi/Api4/Action/User/WriteTrait.php +++ b/ext/standaloneusers/Civi/Api4/Action/User/WriteTrait.php @@ -148,7 +148,19 @@ trait WriteTrait { unset($item['password']); } } - return parent::write($items); + unset($item); + + // Call parent to do the main saving. + $saved = parent::write($items); + + // Enforce uf_id === id + foreach ($saved as $bao) { + if ($bao->uf_id !== $bao->id) { + $bao->uf_id = $bao->id; + $bao->save(); + } + } + return $saved; } } diff --git a/ext/standaloneusers/Civi/Standalone/Security.php b/ext/standaloneusers/Civi/Standalone/Security.php index fbba907fa6..e1e4ef87e5 100644 --- a/ext/standaloneusers/Civi/Standalone/Security.php +++ b/ext/standaloneusers/Civi/Standalone/Security.php @@ -3,6 +3,7 @@ namespace Civi\Standalone; use CRM_Core_Session; use Civi; +use Civi\Api4\User; /** * This is a single home for security related functions for Civi Standalone. @@ -130,27 +131,29 @@ class Security { * - 'cms_name' * - 'cms_pass' plaintext password * - 'notify' boolean - * @param string $mailParam + * @param string $emailParam * Name of the $param which contains the email address. * * @return int|bool * uid if user was created, false otherwise */ - public function createUser(&$params, $mailParam) { + public function createUser(&$params, $emailParam) { try { - $mail = $params[$mailParam]; - $userID = \Civi\Api4\User::create(FALSE) + $email = $params[$emailParam]; + $userID = User::create(FALSE) ->addValue('username', $params['cms_name']) - ->addValue('email', $mail) + ->addValue('uf_name', $email) ->addValue('password', $params['cms_pass']) + ->addValue('contact_id', $params['contact_id'] ?? NULL) + // ->addValue('uf_id', 0) // does not work without this. ->execute()->single()['id']; } catch (\Exception $e) { - \Civi::log()->warning("Failed to create user '$mail': " . $e->getMessage()); + \Civi::log()->warning("Failed to create user '$email': " . $e->getMessage()); return FALSE; } - // @todo This is what Drupal does, but it's unclear why. + // @todo This next line is what Drupal does, but it's unclear why. // I think it assumes we want to be logged in as this contact, and as there's no uf match, it's not in civi. // But I'm not sure if we are always becomming this user; I'm not sure waht calls this function. // CRM_Core_Config::singleton()->inCiviCRM = FALSE; @@ -166,7 +169,7 @@ class Security { public function updateCMSName($ufID, $email) { \Civi\Api4\User::update(FALSE) ->addWhere('id', '=', $ufID) - ->addValue('email', $email) + ->addValue('uf_name', $email) ->execute(); } diff --git a/ext/standaloneusers/ang/afformEditUserAccount.aff.html b/ext/standaloneusers/ang/afformEditUserAccount.aff.html index 739d072112..53e67473d1 100644 --- a/ext/standaloneusers/ang/afformEditUserAccount.aff.html +++ b/ext/standaloneusers/ang/afformEditUserAccount.aff.html @@ -3,7 +3,7 @@
- + diff --git a/ext/standaloneusers/ang/afsearchAdministerUserAccounts.aff.html b/ext/standaloneusers/ang/afsearchAdministerUserAccounts.aff.html index fd9a7e7c9d..669ca2e222 100644 --- a/ext/standaloneusers/ang/afsearchAdministerUserAccounts.aff.html +++ b/ext/standaloneusers/ang/afsearchAdministerUserAccounts.aff.html @@ -1,15 +1,15 @@
- - + +

{{:: ts('User accounts allow people to access CiviCRM. What they can access is determined by which roles the users have, and what permissions are granted to those roles.') }}

- - + +
- +
diff --git a/ext/standaloneusers/managed/SavedSearch_Administer_Users.mgd.php b/ext/standaloneusers/managed/SavedSearch_Administer_Users.mgd.php index d4e0e2ac8a..dccaaeb660 100644 --- a/ext/standaloneusers/managed/SavedSearch_Administer_Users.mgd.php +++ b/ext/standaloneusers/managed/SavedSearch_Administer_Users.mgd.php @@ -21,7 +21,7 @@ return [ 'select' => [ 'id', 'username', - 'email', + 'uf_name', 'is_active', 'when_created', 'when_last_accessed', @@ -82,7 +82,7 @@ return [ ], [ 'type' => 'field', - 'key' => 'email', + 'key' => 'uf_name', 'dataType' => 'String', 'label' => E::ts('Email'), 'sortable' => TRUE, diff --git a/ext/standaloneusers/sql/auto_install.sql b/ext/standaloneusers/sql/auto_install.sql index 44ee4c4998..f00d390197 100644 --- a/ext/standaloneusers/sql/auto_install.sql +++ b/ext/standaloneusers/sql/auto_install.sql @@ -17,7 +17,7 @@ SET FOREIGN_KEY_CHECKS=0; -DROP TABLE IF EXISTS `civicrm_user`; +DROP TABLE IF EXISTS `civicrm_uf_match`; DROP TABLE IF EXISTS `civicrm_role`; SET FOREIGN_KEY_CHECKS=1; @@ -46,26 +46,32 @@ ENGINE=InnoDB; -- /******************************************************* -- * --- * civicrm_user +-- * civicrm_uf_match -- * --- * A standalone user account +-- * Standalone User Account. In Standalone this is a superset of the original civicrm_uf_match table. -- * -- *******************************************************/ -CREATE TABLE `civicrm_user` ( +CREATE TABLE `civicrm_uf_match` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique User ID', - `contact_id` int unsigned COMMENT 'FK to Contact - possibly redundant', + `domain_id` int unsigned NOT NULL COMMENT 'Which Domain is this match entry for', + `uf_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'UF ID. Redundant in Standalone. Needs to be identical to id.', + `uf_name` varchar(255) COMMENT 'Email (e.g. for password resets)', + `contact_id` int unsigned COMMENT 'FK to Contact ID', `username` varchar(60) NOT NULL, `hashed_password` varchar(128) NOT NULL COMMENT 'Hashed, not plaintext password', - `email` varchar(255) NOT NULL COMMENT 'Email (e.g. for password resets)', `roles` varchar(128) COMMENT 'FK to Role', `when_created` timestamp DEFAULT CURRENT_TIMESTAMP, `when_last_accessed` timestamp NULL, `when_updated` timestamp NULL, `is_active` tinyint NOT NULL DEFAULT 1, `timezone` varchar(32) NULL COMMENT 'User\'s timezone', - `language` int unsigned COMMENT 'The language for the user.', + `language` varchar(5) COMMENT 'UI language preferred by the given user/contact', PRIMARY KEY (`id`), + INDEX `I_civicrm_uf_match_uf_id`(uf_id), UNIQUE INDEX `UI_username`(username), - CONSTRAINT FK_civicrm_user_contact_id FOREIGN KEY (`contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE + UNIQUE INDEX `UI_uf_name_domain_id`(uf_name, domain_id), + UNIQUE INDEX `UI_contact_domain_id`(contact_id, domain_id), + CONSTRAINT FK_civicrm_uf_match_domain_id FOREIGN KEY (`domain_id`) REFERENCES `civicrm_domain`(`id`), + CONSTRAINT FK_civicrm_uf_match_contact_id FOREIGN KEY (`contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE SET NULL ) ENGINE=InnoDB; diff --git a/ext/standaloneusers/sql/auto_uninstall.sql b/ext/standaloneusers/sql/auto_uninstall.sql index f754abde63..e963db9f2a 100644 --- a/ext/standaloneusers/sql/auto_uninstall.sql +++ b/ext/standaloneusers/sql/auto_uninstall.sql @@ -15,7 +15,7 @@ SET FOREIGN_KEY_CHECKS=0; -DROP TABLE IF EXISTS `civicrm_user`; +DROP TABLE IF EXISTS `civicrm_uf_match`; DROP TABLE IF EXISTS `civicrm_role`; SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/ext/standaloneusers/templates/CRM/Standaloneusers/Page/Login.tpl b/ext/standaloneusers/templates/CRM/Standaloneusers/Page/Login.tpl index 7cc1073bda..176546ce67 100644 --- a/ext/standaloneusers/templates/CRM/Standaloneusers/Page/Login.tpl +++ b/ext/standaloneusers/templates/CRM/Standaloneusers/Page/Login.tpl @@ -255,11 +255,11 @@ a:hover, a:focus {
- - + +
- +
diff --git a/ext/standaloneusers/tests/phpunit/Civi/Standalone/SecurityTest.php b/ext/standaloneusers/tests/phpunit/Civi/Standalone/SecurityTest.php index cb2579ac47..af42e0bd09 100644 --- a/ext/standaloneusers/tests/phpunit/Civi/Standalone/SecurityTest.php +++ b/ext/standaloneusers/tests/phpunit/Civi/Standalone/SecurityTest.php @@ -80,7 +80,7 @@ class SecurityTest extends \PHPUnit\Framework\TestCase implements EndToEndInterf ->execute()->single(); $this->assertEquals('user_one', $user['username']); - $this->assertEquals('user_one@example.org', $user['email']); + $this->assertEquals('user_one@example.org', $user['uf_name']); $this->assertStringStartsWith('$', $user['hashed_password']); $this->assertTrue($security->checkPassword('secret1', $user['hashed_password'])); @@ -89,6 +89,8 @@ class SecurityTest extends \PHPUnit\Framework\TestCase implements EndToEndInterf public function testPerms() { [$contactID, $userID, $security] = $this->createFixtureContactAndUser(); + $ufID = \CRM_Core_BAO_UFMatch::getUFId($contactID); + $this->assertEquals($userID, $ufID); // Create a custom role $roleID = \Civi\Api4\Role::create(FALSE) @@ -122,6 +124,7 @@ class SecurityTest extends \PHPUnit\Framework\TestCase implements EndToEndInterf } protected function switchToOurUFClasses() { + return; if (!empty($this->originalUFPermission)) { throw new \RuntimeException("are you calling switchToOurUFClasses twice?"); } @@ -132,6 +135,7 @@ class SecurityTest extends \PHPUnit\Framework\TestCase implements EndToEndInterf } protected function switchBackFromOurUFClasses($justInCase = FALSE) { + return; if (!$justInCase && empty($this->originalUFPermission)) { throw new \RuntimeException("are you calling switchBackFromOurUFClasses() twice?"); } @@ -140,21 +144,29 @@ class SecurityTest extends \PHPUnit\Framework\TestCase implements EndToEndInterf $this->originalUFPermission = $this->originalUF = NULL; } + public function dump(string $s = '') { + $d = \CRM_Core_DAO::executeQuery("SELECT * FROM civicrm_uf_match;"); + print "\ndump---------- $s\n"; + foreach ($d->fetchAll() as $row) { + print json_encode($row, JSON_UNESCAPED_SLASHES) . "\n"; + } + print "--------------\n"; + } + /** * @return Array[int, int, \Civi\Standalone\Security] */ public function createFixtureContactAndUser(): array { - $contactID = \Civi\Api4\Contact::create(FALSE) ->setValues([ 'contact_type' => 'Individual', 'display_name' => 'Admin McDemo', ])->execute()->first()['id']; - $params = ['cms_name' => 'user_one', 'cms_pass' => 'secret1', 'notify' => FALSE, 'contactID' => $contactID, 'email' => 'user_one@example.org']; - $this->switchToOurUFClasses(); + $params = ['cms_name' => 'user_one', 'cms_pass' => 'secret1', 'notify' => FALSE, 'contact_id' => $contactID, 'email' => 'user_one@example.org']; + // $this->switchToOurUFClasses(); $userID = \CRM_Core_BAO_CMSUser::create($params, 'email'); - $this->switchBackFromOurUFClasses(); + // $this->switchBackFromOurUFClasses(); $this->assertGreaterThan(0, $userID); $this->contactID = $contactID; $this->userID = $userID; @@ -204,7 +216,7 @@ class SecurityTest extends \PHPUnit\Framework\TestCase implements EndToEndInterf 'password' => 'shhh', 'contact_id' => $stafferContactID, 'roles:name' => ['staff'], - 'email' => 'testuser1@example.org', + 'uf_name' => 'testuser1@example.org', ]) ->execute()->first()['id']; $user = User::get(FALSE)->addWhere('id', '=', $userID)->execute()->first(); diff --git a/ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.entityType.php b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.entityType.php index 5ef16e5b9d..65c36187b9 100644 --- a/ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.entityType.php +++ b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.entityType.php @@ -5,6 +5,6 @@ return [ [ 'name' => 'User', 'class' => 'CRM_Standaloneusers_DAO_User', - 'table' => 'civicrm_user', + 'table' => 'civicrm_uf_match', ], ]; diff --git a/ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.xml b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.xml index 1684bced10..d980630249 100644 --- a/ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.xml +++ b/ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.xml @@ -3,8 +3,8 @@ CRM/StandaloneusersUser - civicrm_user - A standalone user account + civicrm_uf_match + Standalone User Account. In Standalone this is a superset of the original civicrm_uf_match table.usernameusernameemail @@ -16,28 +16,81 @@ id + UF Match ID int unsigned true Unique User ID Number + 5.67 id true - + + domain_id + Domain ID + int unsigned + true + Which Domain is this match entry for + +
civicrm_domain
+ id + name + + + + + 3.0 + + + domain_id + civicrm_domain
+ id + 3.0 +
+ + uf_id + CMS ID + int unsigned + true + 0 + UF ID. Redundant in Standalone. Needs to be identical to id. + 1.1 + + + I_civicrm_uf_match_uf_id + uf_id + 3.3 + + + uf_name + CMS Unique Identifier + varchar + 255 + Email (e.g. for password resets) + + Email + + contact_id + Contact ID int unsigned - FK to Contact - possibly redundant + FK to Contact ID + + + + 1.1 contact_id civicrm_contact
id - CASCADE + 1.1 + SET NULL
@@ -65,17 +118,6 @@ Hashed, not plaintext password - - email - varchar - true - 255 - Email (e.g. for password resets) - - Text - - - roles varchar @@ -136,15 +178,24 @@ language - int unsigned - Language - - languages - - - Select - - The language for the user. + Preferred Language + varchar + 5 + UI language preferred by the given user/contact + 2.1 - + + UI_uf_name_domain_id + uf_name + domain_id + true + 2.1 + + + UI_contact_domain_id + contact_id + domain_id + true + 1.6 + -- 2.25.1