standalone: superset the uf_match table for User entity
authorRich Lott / Artful Robot <code.commits@artfulrobot.uk>
Fri, 29 Sep 2023 15:51:38 +0000 (16:51 +0100)
committerRich Lott / Artful Robot <code.commits@artfulrobot.uk>
Fri, 29 Sep 2023 15:51:38 +0000 (16:51 +0100)
14 files changed:
CRM/Core/BAO/CMSUser.php
ext/standaloneusers/CRM/Standaloneusers/DAO/User.php
ext/standaloneusers/Civi/Api4/Action/User/SendPasswordReset.php
ext/standaloneusers/Civi/Api4/Action/User/WriteTrait.php
ext/standaloneusers/Civi/Standalone/Security.php
ext/standaloneusers/ang/afformEditUserAccount.aff.html
ext/standaloneusers/ang/afsearchAdministerUserAccounts.aff.html
ext/standaloneusers/managed/SavedSearch_Administer_Users.mgd.php
ext/standaloneusers/sql/auto_install.sql
ext/standaloneusers/sql/auto_uninstall.sql
ext/standaloneusers/templates/CRM/Standaloneusers/Page/Login.tpl
ext/standaloneusers/tests/phpunit/Civi/Standalone/SecurityTest.php
ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.entityType.php
ext/standaloneusers/xml/schema/CRM/Standaloneusers/User.xml

index e43a7b499876251b09ed7073819610b9b18708b8..c5f01e496a564e4e41e5587b5538c7936e733962 100644 (file)
@@ -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;
index df4d4a3bce34e9a71e709eda1a90a0c8a0e7377e..92343ccf1da448e7384a9859165365fd002af18b 100644 (file)
@@ -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;
index bb0677b36212e4545ab3909ad1a032d67d2c4a0a..42de24a867db04ec0774e6d4ac40f5585da15a03 100644 (file)
@@ -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]);
     }
   }
+
 }
index 600a09dad8e716e853e13bdeca821fa00f3f2f8d..ec35fe65e0004f115cfe52996589e2b11551d0ee 100644 (file)
@@ -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;
   }
 
 }
index fbba907fa62bcc0f64868099c3ab4c347ef9e1d9..e1e4ef87e510e50a870a62ccd5bb4a27b6f1460e 100644 (file)
@@ -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();
   }
 
index 739d072112de5ef94f5e9484ac36c5ac37bf1313..53e67473d1961f194334bb0fd0ffc4088b7b7627 100644 (file)
@@ -3,7 +3,7 @@
   <fieldset af-fieldset="User1" class="af-container" af-title="User">
     <af-field name="roles" />
     <af-field name="username" />
-    <af-field name="email" />
+    <af-field name="uf_name" />
     <af-field name="is_active" />
     <af-field name="timezone" />
     <af-field name="language" />
index fd9a7e7c9d79d60371223b813a4ed96940b193cc..669ca2e222a9a672234185457435ea604d2e9e9e 100644 (file)
@@ -1,15 +1,15 @@
 <div af-fieldset="">
   <div class="af-markup">
-    
-    
+
+
     <div class="help"><p>{{:: 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.') }}</p>
     </div>
-  
-  
+
+
   </div>
   <div class="af-container af-layout-cols" af-title="Filters">
     <af-field name="username" defn="{required: false, input_attrs: {}}" />
-    <af-field name="email" defn="{required: false, input_attrs: {}}" />
+    <af-field name="uf_name" defn="{required: false, input_attrs: {}}" />
     <af-field name="is_active" defn="{label: 'Active'}" />
     <af-field name="roles" defn="{input_attrs: {multiple: true}}" />
   </div>
index d4e0e2ac8a16a5b52fd3709c146fa2b1e7322617..dccaaeb66073590666210fe7b373980643347398 100644 (file)
@@ -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,
index 44ee4c4998b83483fa005a7f8cfd173130d0752a..f00d390197f259e29d25568d308ba534a1c85f95 100644 (file)
@@ -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;
index f754abde63ab098603b029ffb399afb24e12249b..e963db9f2a0cf98d4d527df67b4210e6526cb1af 100644 (file)
@@ -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
index 7cc1073bda6513a8a8531ad0d9ff9c7d55be7131..176546ce673f3985b35be4d80e82825b05c4ef1d 100644 (file)
@@ -255,11 +255,11 @@ a:hover, a:focus {
     <div class="message warning" style="display:none;" id="anonAccessDenied">{ts}You may need to login to access that.{/ts}</div>
     <form>
       <div>
-        <label for="exampleInputEmail1" class="form-label">Username</label>
-        <input type="email" class="form-control" id="usernameInput" aria-describedby="emailHelp">
+        <label for="usernameInput" class="form-label">Username</label>
+        <input type="password" class="form-control" id="usernameInput" >
       </div>
       <div>
-        <label for="exampleInputPassword1" class="form-label">Password</label>
+        <label for="passwordInput" class="form-label">Password</label>
         <input type="password" class="form-control" id="passwordInput">
       </div>
       <div id="error" style="display:none;" class="form-alert">Your username and password do not match</div>
index cb2579ac47ea7a5baef6715a4c20b33766d549f6..af42e0bd09ebd00ecbcf7eaf6e0f2e69b9d535e0 100644 (file)
@@ -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();
index 5ef16e5b9de0aa6bee600b104ed28f4514c2e694..65c36187b91d5fd1ade0d03a99b3073899141268 100644 (file)
@@ -5,6 +5,6 @@ return [
   [
     'name' => 'User',
     'class' => 'CRM_Standaloneusers_DAO_User',
-    'table' => 'civicrm_user',
+    'table' => 'civicrm_uf_match',
   ],
 ];
index 1684bced10fb75df74c8ba98ab5106ddb199d03c..d98063024969d74666a948262095976406c42e3d 100644 (file)
@@ -3,8 +3,8 @@
 <table>
   <base>CRM/Standaloneusers</base>
   <class>User</class>
-  <name>civicrm_user</name>
-  <comment>A standalone user account</comment>
+  <name>civicrm_uf_match</name>
+  <comment>Standalone User Account. In Standalone this is a superset of the original civicrm_uf_match table.</comment>
   <labelField>username</labelField>
   <searchField>username</searchField>
   <descriptionField>email</descriptionField>
 
   <field>
     <name>id</name>
+    <title>UF Match ID</title>
     <type>int unsigned</type>
     <required>true</required>
     <comment>Unique User ID</comment>
     <html>
       <type>Number</type>
     </html>
+    <add>5.67</add>
   </field>
   <primaryKey>
     <name>id</name>
     <autoincrement>true</autoincrement>
   </primaryKey>
-
+  <field>
+    <name>domain_id</name>
+    <title>Domain ID</title>
+    <type>int unsigned</type>
+    <required>true</required>
+    <comment>Which Domain is this match entry for</comment>
+    <pseudoconstant>
+      <table>civicrm_domain</table>
+      <keyColumn>id</keyColumn>
+      <labelColumn>name</labelColumn>
+    </pseudoconstant>
+    <html>
+      <label>Domain</label>
+    </html>
+    <add>3.0</add>
+  </field>
+  <foreignKey>
+    <name>domain_id</name>
+    <table>civicrm_domain</table>
+    <key>id</key>
+    <add>3.0</add>
+  </foreignKey>
+  <field>
+    <name>uf_id</name>
+    <title>CMS ID</title>
+    <type>int unsigned</type>
+    <required>true</required>
+    <default>0</default>
+    <comment>UF ID. Redundant in Standalone. Needs to be identical to id.</comment>
+    <add>1.1</add>
+  </field>
+  <index>
+    <name>I_civicrm_uf_match_uf_id</name>
+    <fieldName>uf_id</fieldName>
+    <add>3.3</add>
+  </index>
+  <field>
+    <name>uf_name</name>
+    <title>CMS Unique Identifier</title>
+    <type>varchar</type>
+    <length>255</length>
+    <comment>Email (e.g. for password resets)</comment>
+    <html>
+      <type>Email</type>
+    </html>
+  </field>
   <field>
     <name>contact_id</name>
+    <title>Contact ID</title>
     <type>int unsigned</type>
-    <comment>FK to Contact - possibly redundant</comment>
+    <comment>FK to Contact ID</comment>
+    <html>
+      <label>Contact</label>
+    </html>
+    <add>1.1</add>
   </field>
   <foreignKey>
     <name>contact_id</name>
     <table>civicrm_contact</table>
     <key>id</key>
-    <onDelete>CASCADE</onDelete>
+    <add>1.1</add>
+    <onDelete>SET NULL</onDelete>
   </foreignKey>
 
   <field>
     <comment>Hashed, not plaintext password</comment>
   </field>
 
-  <field>
-    <name>email</name>
-    <type>varchar</type>
-    <required>true</required>
-    <length>255</length>
-    <comment>Email (e.g. for password resets)</comment>
-    <html>
-      <type>Text</type>
-    </html>
-  </field>
-
   <field>
     <name>roles</name>
     <type>varchar</type>
 
   <field>
     <name>language</name>
-    <type>int unsigned</type>
-    <title>Language</title>
-    <pseudoconstant>
-      <optionGroupName>languages</optionGroupName>
-    </pseudoconstant>
-    <html>
-      <type>Select</type>
-    </html>
-    <comment>The language for the user.</comment>
+    <title>Preferred Language</title>
+    <type>varchar</type>
+    <length>5</length>
+    <comment>UI language preferred by the given user/contact</comment>
+    <add>2.1</add>
   </field>
-
+  <index>
+    <name>UI_uf_name_domain_id</name>
+    <fieldName>uf_name</fieldName>
+    <fieldName>domain_id</fieldName>
+    <unique>true</unique>
+    <add>2.1</add>
+  </index>
+  <index>
+    <name>UI_contact_domain_id</name>
+    <fieldName>contact_id</fieldName>
+    <fieldName>domain_id</fieldName>
+    <unique>true</unique>
+    <add>1.6</add>
+  </index>
 </table>