<?php
#BEGIN_LICENSE
#-------------------------------------------------------------------------
# Module: FrontEndUsers (c) 2008 by Robert Campbell
#         (calguy1000@cmsmadesimple.org)
#  An addon module for CMS Made Simple to allow management of frontend
#  users, and their login process within a CMS Made Simple powered
#  website.
#
#-------------------------------------------------------------------------
# CMS - CMS Made Simple is (c) 2005 by Ted Kulp (wishy@cmsmadesimple.org)
# This projects homepage is: http://www.cmsmadesimple.org
#
#-------------------------------------------------------------------------
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# However, as a special exception to the GPL, this software is distributed
# as an addon module to CMS Made Simple.  You may not use this software
# in any Non GPL version of CMS Made simple, or in any version of CMS
# Made simple that does not indicate clearly and obviously in its admin
# section that the site was built with CMS Made simple.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# Or read it online: http://www.gnu.org/licenses/licenses.html#GPL
#
#-------------------------------------------------------------------------
#END_LICENSE

class MAMSManipulator extends MAMSUserManipulator
{
  private $_cached_propdefns;
  private $_groupinfo_cache;
  private $_multiselect_options;
  private $_useridbyname;
  private $_grouppropmap;
  /**
   * @var |null
   */
  private $_cached_propdefn;
  
  protected function keepalive_cookie()
  {
    return 'ka' . sha1(__FILE__);
  }
  
  /**
   * @internal
   */
  protected function encode_userdata(array $data)
  {
    $str = json_encode($data);
    $str .= '::' . sha1($str . __FILE__);
    
    return base64_encode($str);
  }
  
  private function decode_userdata(string $str)
  {
    $encoded = base64_decode($str);
    
    if(!$encoded) { return []; }
    
    [$json, $sig] = explode('::', $encoded, 2);
    
    if(!$json || !$sig) { return []; }
    
    if(sha1($json . __FILE__) !== $sig) { return []; }
    
    return json_decode($json, TRUE); // TODO harden error handling
  }
  
  private function get_bad_salt()
  {
    // this method should only be used for sites that have been upgraded forever
    // and its use is very bad for security, as it implies a shared salt on user passwords.
    $mod  = $this->GetModule();
    $salt = $mod->GetPreference('pwsalt', '');
    
    if(!$salt)
    {
      audit('', 'MAMS', 'IMPORTANT We are using bad password hashing... and even worse the salt is empty');
    }
    
    return $salt;
  }
  
  private function get_expireusers_lastrun()
  {
    return cms_cache_handler::get_instance()->get('expire_lastrun', __CLASS__);
  }
  
  private function touch_expireusers_lastrun()
  {
    cms_cache_handler::get_instance()->set('expire_lastrun', time(), __CLASS__);
  }
  
  public function add_history($uid, $message)
  {
    if($uid <= 0 || !is_string($message) || $message == '')
    {
      return FALSE;
    }
    
    $db    = $this->GetDb();
    $now   = $db->DBTimeStamp(time());
    $query = 'INSERT INTO ' . CMS_DB_PREFIX .
             "module_mams_history (userid,sessionid,action,refdate,ipaddress) VALUES (?,?,?,$now,?)";
    $ip    = xt_utils::get_real_ip();
    $dbr   = $db->Execute($query, [$uid, session_id(), substr($message, 0, 255), $ip]);
    
    return TRUE;
  }
  
  public function GetEncryptionKey($uid = -1)
  {
    global $CMS_ADMIN_PAGE;
    $gCms = CmsApp::get_instance();
    
    if($CMS_ADMIN_PAGE)
    {
      // an administrator can see encrypted data.
      $res = $this->GetUserInfo($uid);
      
      if(!is_array($res) || $res[0] === FALSE) { return FALSE; }
      
      $key = md5($gCms->config['root_url'] . $uid . $res[1]['createdate'] . $this->get_bad_salt());
      
      return $key;
    }
  
    // frontend request... use logged in id.
    $tuid = $this->LoggedInId();
  
    if($tuid < 1) { return FALSE; }
  
    $res = $this->GetUserInfo($tuid);
  
    if(!is_array($res) || $res[0] === FALSE) { return FALSE; }
  
    $key = md5($gCms->config['root_url'] . $tuid . $res[1]['createdate'] . $this->get_bad_salt());
  
    return $key;
    
  }
  
  public function DecryptUserData($uid, $data)
  {
    $uid = (int)$uid;
    
    if($uid < 1) { return ''; }
    
    $key = $this->GetEncryptionKey($uid);
  
    return trim( xt_encrypt::decrypt( $key, base64_decode($data) ) );
  }
  
  public function CountTempCodeRecords()
  {
    $db       = $this->GetDb();
    $q        = 'SELECT COUNT(*) AS count FROM ' . CMS_DB_PREFIX . 'module_mams_tempcode';
    $dbresult = $db->Execute($q);
    
    if(!$dbresult) { return 0; }
    
    $row = $dbresult->FetchRow();
  
    /** @var array $row */
    return $row['count'];
  }
  
  public function ExpireTempCodes($expirycode)
  {
    $db = $this->GetDb();
    
    try
    {
      $db->BeginTrans();
      $expires = strtotime($expirycode);
      $q       = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_tempcode WHERE created > ?';
      $db->Execute($q, [$expires]);
      $db->CommitTrans();
      
      return TRUE;
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit('', $mod->GetName(), 'Could not expire temp codes ');
      \xt_utils::log_exception($e);
      
      return FALSE;
    }
  }
  
  public function RemoveUserTempCode($uid)
  {
    $db = $this->GetDb();
    
    try
    {
      $uid = (int)$uid;
      
      if($uid < 1) { return FALSE; }
      
      $db->BeginTrans();
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_tempcode WHERE userid = ?';
      $db->Execute($q, [$uid]);
      $db->CommitTrans();
      
      return TRUE;
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit($uid, $mod->GetName(), 'Could not remove temp code');
      \xt_utils::log_exception($e);
      
      return FALSE;
    }
  }
  
  public function GetUserTempCode($uid)
  {
    if(!$uid) { return FALSE; }
    
    $db       = $this->GetDb();
    $q        = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_tempcode WHERE userid = ? ORDER BY created DESC';
    $dbresult = $db->GetRow($q, [$uid]);
    
    if(is_array($dbresult) && count($dbresult)) { return $dbresult['code']; }
  }
  
  public function SetUserTempCode($uid, $code)
  {
    $uid  = (int)$uid;
    $code = trim($code);
    
    if($uid < 1 || !$code) { return FALSE; }
    
    $db = $this->GetDb();
    
    try
    {
      $db->BeginTrans();
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_tempcode WHERE userid = ?';
      $db->Execute($q, [$uid]);
      
      $q = 'INSERT INTO ' . CMS_DB_PREFIX . 'module_mams_tempcode VALUES(?,?,NOW())';
      $db->Execute($q, [$uid, $code]);
      $db->CommitTrans();
      
      return TRUE;
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit($uid, $mod->GetName(), 'Could not set temp code');
      \xt_utils::log_exception($e);
      
      return FALSE;
    }
  }
  
  public function SetPropertyDefn(
    $name, $newname, $prompt, $length, $type, $maxlength = 0, $attribs = '', $force_unique = 0, $encrypt = 0
  )
  {
    $db = $this->GetDb();
    
    if(0 === $maxlength) { $maxlength = $length; }
    
    $q        = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_propdefn
              SET name = ?, prompt = ?, type = ?, length = ?, maxlength = ?, attribs = ?, force_unique = ?, encrypt = ?
              WHERE name = ?';
    $dbresult = $db->Execute(
      $q, [$newname, $prompt, $type, $length, $maxlength, $attribs, $force_unique, $encrypt, $name]
    );
    
    if(!$dbresult) { return FALSE; }
    
    return TRUE;
  }
  
  public function DeletePropertyDefn($name)
  {
    $db = $this->GetDb();
    
    try
    {
      $db->BeginTrans();
      $this->DeleteSelectOptions($name);
      
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_properties WHERE title = ?';
      $db->Execute($q, [$name]);
      
      $query = 'SELECT group_id,sort_key FROM ' . CMS_DB_PREFIX . 'module_mams_grouppropmap WHERE name = ?';
      
      $dbr   = $db->GetArray($query, [$name]);
      
      if(is_array($dbr) && count($dbr))
      {
        $q = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_grouppropmap
                          SET sort_key = sort_key - 1
                          WHERE group_id = ? AND sort_key > ?';
        
        foreach($dbr as $row)
        {
          $db->Execute($q, [$row['group_id'], $row['sort_key']]);
        }
      }
      
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_grouppropmap WHERE name = ?';
      $db->Execute($q, [$name]);
      
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_propdefn WHERE name=?';
      $db->Execute($q, [$name]);
      
      $db->CommitTrans();
      
      return TRUE;
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit('', $mod->GetName(), 'Could not delete property ' . $name);
      \xt_utils::log_exception($e);
      
      return FALSE;
    }
    
  }
  
  public function GetPropertyGroupRelations($title)
  {
    $db = $this->GetDb();
    
    $q        = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_grouppropmap WHERE name = ? ORDER BY sort_key DESC';
    $dbresult = $db->Execute($q, [$title]);
    
    if(!$dbresult) { return [FALSE, $db->ErrorMsg()]; }
    
    $result = [];
    
    while( $row = $dbresult->FetchRow() ) { $result[] = $row; }
    
    return $result;
  }
  
  
  /**
   * Return the unix timestamp of the user's expiry date
   * or false;
   */
  public function GetExpiryDate($uid)
  {
    if(!$uid) { return FALSE; }
    
    $res = mams_user_cache::get_user($uid);
    
    if(is_array($res) && '' !== $res['expires'])
    {
      return $this->GetDb()->UnixTimeStamp($res['expires']);
    }
    
    return FALSE;
  }
  
  public function CanUserLogin($uid)
  {
    $uid = (int)$uid;
    
    if($uid < 1) { return FALSE; }
    
    $res = mams_user_cache::get_user($uid);
    
    if( !is_array($res) ) { return FALSE; }
    
    if($res['disabled']) { return FALSE; }

    if(!$res['password']){ return FALSE; }
    
    $expires = strtotime($res['expires']);
    
    return time() < $expires;
  }
  
  public function IsAccountExpired($uid)
  {
    if(!$uid) { return TRUE; } // dunno about this
    
    $expiry = $this->GetExpiryDate($uid);
    
    if(!$expiry) { return TRUE; }
    
    if($expiry < time()) { return TRUE; }
    
    return FALSE;
  }
  
  public function GetUserPropertyRelations($uid)
  {
    $uprops = [];
    $groups = $this->GetMemberGroupsArray($uid);
    
    if(!is_array($groups) || 0 === count($groups)) { return $uprops; }
    
    for($a = 0, $aMax = count($groups); $a < $aMax; $a++)
    {
      $gid    = $groups[$a]['groupid'];
      $relns  = $this->GetGroupPropertyRelations($gid);
      $uprops = \MAMS\RRUtils::array_merge_by_name_required($uprops, $relns);
      usort($uprops, ['xt_array', 'compare_elements_by_sortorder_key']);
    }
    
    return $uprops;
  }
  
  public function GetGroupPropertyRelations($grpid)
  {
    if(!is_array($this->_grouppropmap))
    {
      $db                  = $this->GetDb();
      $q                   = 'SELECT * FROM ' . CMS_DB_PREFIX .
                             'module_mams_grouppropmap ORDER BY group_id,sort_key DESC';
      $this->_grouppropmap = $db->GetArray($q);
      
      if(!$this->_grouppropmap)
      {
        return [FALSE, $db->ErrorMsg()];
      }
    }
    
    $res = [];
    for($i = 0, $iMax = count($this->_grouppropmap); $i < $iMax; $i++)
    {
      $row = $this->_grouppropmap[$i];
      
      if($row['group_id'] < $grpid) { continue; }
      
      if($row['group_id'] > $grpid) { break; }
      
      $res[] = $row;
    }
    
    if(!count($res))
    {
      return [FALSE, 'notfound'];
    }
    
    return $res;
  }
  
  public function AddGroupPropertyRelation($grpid, $propname, $sortkey, $lostun, $val)
  {
    $db = $this->GetDb();
    
    $q        = 'INSERT INTO ' . CMS_DB_PREFIX . 'module_mams_grouppropmap VALUES(?,?,?,?,?)';
    $dbresult = $db->Execute($q, [$propname, $grpid, $sortkey, $val, $lostun]);
    
    if(!$dbresult)
    {
      return [FALSE, $db->ErrorMsg()];
    }
    
    return [TRUE, ''];
  }
  
  public function DeleteAllGroupPropertyRelations($grpid)
  {
    $db = $this->GetDb();
    
    try
    {
      $db->BeginTrans();
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_grouppropmap WHERE group_id = ?';
      $db->Execute($q, [$grpid]);
      $db->CommitTrans();
      
      return [TRUE, ''];
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit('', $mod->GetName(), 'Could not delete all group property relations');
      \xt_utils::log_exception($e);
      
      return [FALSE, $e->getMessage()];
    }
  }
  
  public function DeleteGroupPropertyRelation($grpid, $propname)
  {
    $db = $this->GetDb();
    
    try
    {
      $db->BeginTrans();
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_grouppropmap WHERE name = ? AND group_id = ?';
      $db->Execute($q, [$propname, $grpid]);
      $db->CommitTrans();
      
      return [TRUE];
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit('', $mod->GetName(), 'Could not delete group property relation');
      \xt_utils::log_exception($e);
      
      return [FALSE, $e->getMessage()];
    }
  }
  
  public function AddPropertyDefn(
    $name, $prompt, $type, $length, $maxlength = 0, $attribs = '', $force_unique = 0, $encrypt = 0
  )
  {
    $db = $this->GetDb();
    
    if(0 === $maxlength)
    {
      $maxlength = $length;
    }
    
    $p        = [$name, $prompt, $type, $length, $maxlength, $attribs, $force_unique, $encrypt];
    $q        = 'INSERT INTO ' . CMS_DB_PREFIX . 'module_mams_propdefn
          (name,prompt,type,length,maxlength,attribs,force_unique,encrypt)
          VALUES (?,?,?,?,?,?,?,?)';
    $dbresult = $db->Execute($q, $p);
    
    if(FALSE === $dbresult)
    {
      return [FALSE, $db->sql . '<br/>' . $db->ErrorMsg()];
    }
    
    $db->Insert_ID();
    
    $this->_cached_propdefn = NULL;
    
    return [TRUE, ''];
  }
  
  public function SetPropertyDefnExtra($name, $extra)
  {
    if(is_array($extra))
    {
      $extra = serialize($extra);
    }
    
    $db    = $this->GetDb();
    $query = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_propdefn SET extra = ? WHERE name = ?';
    $db->Execute($query, [$extra, $name]);
  }
  
  public function AddSelectOptions($name, $options)
  {
    $db = $this->GetDb();

    try
    {
      $order_id    = 0;
      $sql         = 'INSERT INTO ' . CMS_DB_PREFIX .
                     'module_mams_dropdowns (order_id, option_name, option_text, control_name) VALUES (?,?,?,?)';
      $db->BeginTrans();
      
      foreach($options as $opttext)
      {
        // if no actual text in the line, make sure it equals '',
        // in order not to add it to the db
        $opttext = trim($opttext);
        
        if('' === $opttext || '__' === $opttext) { continue; }
        
        $optname = trim($opttext);
        
        if( FALSE !== strpos($opttext, '=') )
        {
          $tmp     = explode('=', $opttext, 2);
          $optname = trim($tmp[1]);
          $opttext = trim($tmp[0]);
        }
        
        $order_id++;
        $res = $db->Execute($sql, [$order_id, $optname, $opttext, $name]);
        
        if(!$res)
        {
          throw new \RuntimeException( 'SQL ERROR: ' . $db->ErrorMsg() );
        }
      }
      
      $db->CommitTrans();
      $this->_multiselect_options = NULL;
      
      return [TRUE];
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit('', $mod->GetName(), 'Could not expire temp codes ');
      \xt_utils::log_exception($e);
      
      return [FALSE];
    }
  }
  
  public function DeletePropertyDefns()
  {
    $db = $this->GetDb();
    try
    {
      $defns = $this->GetPropertyDefns();
      foreach($defns as $name => $rec)
      {
        $this->DeletePropertyDefn($name);
      }
      $this->_cached_propdefn = NULL;
      $db->CommitTrans();
      
      return [TRUE];
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit('', $mod->GetName(), 'Could not delete all property definitions');
      \xt_utils::log_exception($e);
      
      return [FALSE, $e->GetMessage()];
    }
  }
  
  public function DeleteSelectOptions($name)
  {
    $db       = $this->GetDb();
    $q        = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_dropdowns WHERE control_name = ?';
    $dbresult = $db->Execute($q, [$name]);
    
    if(FALSE === $dbresult)
    {
      return [FALSE, $db->ErrorMsg()];
    }
    
    $this->_multiselect_options = NULL;
    
    return [TRUE, ''];
  }
  
  public function GenerateRandomUsername($prefix = 'user')
  {
    mt_srand( time() );
    $db  = $this->GetDb();
    $mod = $this->GetModule();
  
    try
    {
      $num = random_int(100, 99999);
    }
    catch(Exception $e)
    {
      $num = rand(100, 99999);
    }
    
    $count  = 0;
    $suffix = '';
    
    if($mod->GetPreference('username_is_email'))
    {
      $suffix = '@sample.com';
    }
    
    while($count < 100)
    {
      // todo, 100 should be configurable?
      $q   = 'SELECT id FROM ' . CMS_DB_PREFIX . 'module_mams_users WHERE username = ?';
      $tmp = $db->GetOne($q, [$prefix . $num . $suffix]);
      
      if(!$tmp)
      {
        return $prefix . $num . $suffix;
      }
  
      try
      {
        $num = random_int(100, 99999);
      }
      catch(Exception $e)
      {
        $num = rand(100, 99999);
      }
      
      ++$count;
    }
    
    return FALSE;
  }
  
  public function ClearPropertyCache()
  {
    $this->_cached_propdefns = NULL;
  }
  
  protected function _cache_propertydefns()
  {
    if( !is_array($this->_cached_propdefns) )
    {
      $db                      = $this->GetDb();
      $this->_cached_propdefns = [];
      
      $query = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_propdefn';
      $data  = $db->GetArray($query);
      
      if(!$data) { return; }
      
      for($i = 0, $iMax = count($data); $i < $iMax; $i++)
      {
        if(!empty($data[$i]['extra']) && NULL !== $data[$i]['extra'])
        {
          $data[$i]['extra'] = @unserialize($data[$i]['extra']);
        }
        
        if(!empty($data[$i]['attribs']))
        {
          $data[$i]['extra'] = @unserialize($data[$i]['attribs']);
        }
        
        $this->_cached_propdefns[$data[$i]['name']] = $data[$i];
      }
    }
  }
  
  public function GetPropertyDefn($name)
  {
    $this->_cache_propertydefns();
    
    if( !is_array($this->_cached_propdefns) ) { return FALSE; }
  
    return $this->_cached_propdefns[$name] ?? FALSE;
  }
  
  public function GetPropertyDefns()
  {
    $this->_cache_propertydefns();
    
    if( !is_array($this->_cached_propdefns) ) { return FALSE; }
    
    return $this->_cached_propdefns;
  }
  
  /**
   * Returns select options as one simple or 2 dimensional array
   *
   * @param String $controlname - name of the dropdown as in the propdefn table
   * @param int    $dim         - dimension of the array
   *                            if $dim == 1, returns a 1 dimensional array text=>name
   *                            if $dim == 2, returns a 2 dimensional array, each item being an
   *                            array with properties 'option_name', 'option_text', 'control_name'.
   *
   * @return array|bool
   */
  public function GetSelectOptions($controlname, $dim = 1)
  {
    if(!$this->_multiselect_options)
    {
      $db = $this->GetDb();
      
      $q   = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_dropdowns ORDER BY order_id';
      $dbr = $db->GetArray($q);
      
      if(is_array($dbr))
      {
        $this->_multiselect_options = $dbr;
      }
    }
    
    if(!count($this->_multiselect_options)) { return FALSE; }
    
    $ret = [];
    
    //for($i = 0, $iMax = count($this->_multiselect_options); $i < $iMax; $i++)
    foreach($this->_multiselect_options as $iValue)
    {
      //$row = $this->_multiselect_options[$i];
      $row = $iValue;
      
      if($row['control_name'] === $controlname)
      {
        if(2 === $dim)
        {
          $ret[] = $row;
        }
        else
        {
          $ret[trim($row['option_text'])] = trim($row['option_name']);
        }
      }
    }
  
    if( !count($ret) ) { return FALSE; }
    
    return $ret;
  }
  
  /**
   * Log the user into the site.
   * This checks the user password, and preferences about duplicate logins and other things then marks the user
   * as logged in with the appropriate database change.  It does not send events.
   *
   * @param string $username
   * @param string $password     The plaintext password
   * @param string $groups       A comma separated list of group names that the user must be a member of.
   * @param string $ignored
   * @param bool   $force_logout Optionally force the user to logout before logging him in again.
   *
   * @return array|int[]|void[]
   */
  public function Login($username, $password, $groups = '', $ignored = FALSE, $force_logout = FALSE)
  {
    
    $db   = $this->GetDb();
    $mod  = $this->GetModule();
    
    if(!$this->CheckPassword($username, $password, $groups))
    {
      $uid = $this->GetUserID($username);
      
      if(!$uid) { $uid = -1; }
      
      $this->add_history($uid, 'fail');
      
      return [FALSE, $mod->Lang('error_loginfailed')];
    }
    
    $uid = $this->GetUserID($username);
    
    if(!$uid)
    {
      return [FALSE, $mod->Lang('error_usernotfound')];
    }
    
    if($force_logout)
    {
      $this->LogoutUser($uid);
    }
    
    if(!$this->CanUserLogin($uid))
    {
      return [FALSE, $mod->Lang('error_accountexpired')];
    }
    
    if(0 === $mod->GetPreference('allow_repeated_logins'))
    {
      // make sure this user isn't already logged in
      $q        = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_loggedin WHERE USERID = ?';
      $dbresult = $db->Execute($q, [$uid]);
      
      if($dbresult && $dbresult->RecordCount())
      {
        return [FALSE, $mod->Lang('error_norepeatedlogins')];
      }
    }
    
    // actually does the logging in.
    $this->_set_saved_logindetails(['uid' => $uid, 'time' => time()], TRUE);
    
    // and add history info
    audit($uid, $mod->GetName(), "$username logged in");
    $this->add_history($uid, 'login');
    
    // send the event.
    $ip = \xt_utils::get_real_ip();
    \CMSMS\Hookmanager::do_hook('MAMS::OnLogin', ['id' => $uid, 'username' => $username, 'ip' => $ip]);
    
    return [$uid];
  }
  
  /**
   * userid api method
   *
   * @param $uid
   * @param $gid
   *
   * @return bool
   */
  public function AssignUserToGroup($uid, $gid)
  {
    $uid = (int)$uid;
    $gid = (int)$gid;
    
    if($uid < 1 || $gid < 1) { return FALSE; }
    
    // validate the user id
    if(!$this->UserExistsByID($uid)) { return FALSE; }
    
    // validate the group id
    if(!$this->GroupExistsByID($gid)) { return FALSE; }
    
    $db = $this->GetDb();
    // make sure it already doesn't exist
    $q   = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_belongs WHERE userid = ? AND groupid = ?';
    $tmp = $db->GetRow($q, [$uid, $gid]);
    
    if($tmp) { return TRUE; }
    
    // add the record to the table to make this
    // user a member of this group
    $q        = 'INSERT INTO ' . CMS_DB_PREFIX . 'module_mams_belongs (userid, groupid) VALUES (?,?)';
    $dbresult = $db->Execute($q, [$uid, $gid]);
    
    return (FALSE !== $dbresult);
  }
  
  /**
   * userid api method
   *
   * @param $password
   *
   * @return bool
   */
  public function IsValidPassword($password)
  {
    // a password is valid, if it's length is
    // within certain ranges
    $module        = $this->GetModule();
    $minlen        = $module->GetPreference('min_passwordlength', 6);
    $maxlen        = $module->GetPreference('max_passwordlength', 20);
    $enhancedpw    = (int)$module->GetPreference('enhanced_password', 0);
    $requiredchars = $module->GetPreference('password_requiredchars');
    $len           = strlen($password);
    
    if($len < $minlen || $len > $maxlen) { return FALSE; }
    
    if($enhancedpw)
    {
      if(!preg_match('#[A-Z]#', $password)) { return FALSE; }
      
      if(!preg_match('#[0-9]#', $password)) { return FALSE; }
      
      //  $tmp = strpbrk($password, '!f'); ???? JM TODO why was this here at all
      
      if($requiredchars && FALSE === strpbrk($password, $requiredchars))  { return FALSE; }
    }
    
    return TRUE;
  }
  
  /**
   * userid api method
   *
   * @param $id
   *
   * @return array|bool
   */
  public function DeleteUserFull($id)
  {
    $db       = $this->getDb();
    $in_trans = NULL;
    
    try
    {
      $id = (int)$id;
      
      if($id < 1)
      {
        return [FALSE, 'Invalid UID'];
      }
      
      $res = $this->GetUserInfo($id);
      
      if(!is_array($res) || FALSE === $res[0])
      {
        return [FALSE, 'Invalid UID'];
      }
      
      $username = $res[1]['username'];
      
      // send the event... before we actually delete stuff.
      $parms             = [];
      $parms['id']       = $id;
      $parms['username'] = $username;
      CMSMS\HookManager::do_hook('MAMS::OnDeleteUser', $parms);
      
      // log the user out
      $this->LogoutUser($id);
      
      $db->BeginTrans();
      $in_trans = TRUE;
      
      // delete user properties
      $this->DeleteAllUserPropertiesFull($id);
      
      // delete user from groups
      $ret = $this->RemoveUserFromGroup($id, '');
      
      if(FALSE === $ret[0])
      {
        throw new \RuntimeException('Problem removing user from group');
      }
      
      // delete the user history
      $q        = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_history WHERE userid = ?';
      $dbresult = $db->Execute($q, [$id]);
      
      // delete user record
      $q        = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_users WHERE id = ?';
      $dbresult = $db->Execute($q, [$id]);
      
      // and delete anything from the tempcodes table too
      $q        = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_tempcode WHERE userid = ?';
      $dbresult = $db->Execute($q, [$id]);
      
      $this->_useridbyname = [];
      mams_user_cache::del_user($id);
      $db->CommitTrans();
      
      $parms             = [];
      $parms['id']       = $id;
      $parms['username'] = $username;
      CMSMS\HookManager::do_hook('MAMS::AfterDeleteUser', $parms);
      
      return [TRUE, ''];
    }
    catch(\Exception $e)
    {
      if($in_trans)
      {
        $db->RollbackTrans();
      }
      
      $mod = $this->GetModule();
      audit($id, $mod->GetName(), 'Problem deleting user: ' . $e->GetMessage());
      \xt_utils::log_exception($e);
      
      return [FALSE, $e->getMessage()];
    }
  }
  
  /**
   * @param bool $with_count
   */
  private function _get_groupinfo($with_count = FALSE)
  {
    if(!is_array($this->_groupinfo_cache))
    {
      $auth_consumer = mams_utils::get_auth_consumer();
      
      if($auth_consumer->has_capability($auth_consumer::CAPABILITY_LISTGROUPS))
      {
        $tmp                    = $auth_consumer->get_group_list(TRUE);
        $this->_groupinfo_cache = [];
        
        if( !count($tmp) ) { return; }
        
        $out = [];
        foreach($tmp as $gid => $rec)
        {
          $tmp2 = ['id' => $gid, 'groupdesc' => ''];
          
          if(is_string($rec) && !is_array($rec))
          {
            $tmp2['groupname'] = $rec;
          }
          else if(isset($rec['name']))
          {
            $tmp2['groupname'] = $rec['name'];
          }
          else if(isset($rec['groupname']))
          {
            $tmp2['groupname'] = $rec['groupname'];
          }
          else
          {
            continue;
          }
          
          if(is_array($rec))
          {
            if(isset($rec['desc']))
            {
              $tmp2['groupdesc'] = $rec['desc'];
            }
            else if(isset($rec['description']))
            {
              $tmp2['groupdesc'] = $rec['description'];
            }
            else if(isset($rec['groupdesc']))
            {
              $tmp2['groupdesc'] = $rec['groupdesc'];
            }
            
            if(isset($rec['count']))
            {
              $tmp2['count'] = $rec['count'];
            }
          }
          
          $out[$gid] = $tmp2;
        }
        $this->_groupinfo_cache = $out;
        
        return;
      }
      
      // the consumer does not provide group info.... so we gotta list em.
      $db    = $this->GetDb();
      $query = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_groups';
      if($with_count)
      {
        $query = 'SELECT g.*,count(b.userid) AS count FROM ' . CMS_DB_PREFIX . 'module_mams_groups g
                  LEFT JOIN ' . CMS_DB_PREFIX . 'module_mams_belongs b
                  ON g.id = b.groupid GROUP BY g.id, g.groupname, g.groupdesc';
      }
      $dbr = $db->GetArray($query);
      
      if(is_array($dbr))
      {
        $this->_groupinfo_cache = xt_array::to_hash($dbr, 'id');
      }
    }
  }
  
  /**
   * userid api method
   * @param $gid
   * returns an array ???
   * @return bool|mixed
   */
  public function GetGroupInfo($gid)
  {
    $this->_get_groupinfo();
    
    if(is_array($this->_groupinfo_cache) && isset($this->_groupinfo_cache[$gid]))
    {
      return $this->_groupinfo_cache[$gid];
    }
  }
  
  /**
   * Return an array of user information
   *
   * @param array An array of integer user ids
   * @param boolean flag indicating whether to return property information
   *
   * @return array of user info.  or null.
   * @throws \CmsException
   * @deprecated
   */
  public function GetBulkUserInfo($uids, $deep = TRUE)
  {
    mams_user_cache::load_users($uids, $deep);
    
    $out = [];
    
    foreach($uids as $one)
    {
      $one = (int)$one;
      
      if($one < 1) { continue; }
      
      $t = mams_user_cache::get_user_noload($one);
      
      if(is_array($t)) { $out[] = $t; }
    }
    
    return $out;
  }
  
  
  /**
   * // userid api method
   * //returns an array
   * // second element of array may be an array ???
   *
   * @param      $uid
   * @param bool $deep
   *
   * @return array
   */
  public function GetUserInfo($uid, $deep = FALSE)
  {
    if(!$uid) { return [FALSE, 'Could not find user ' . $uid ]; }
    
    $row = mams_user_cache::get_user($uid, $deep);
    
    if(!is_array($row)) { return [FALSE, 'Could not find user info for user' . $uid ]; }
    
    return [TRUE, $row];
  }
  /**
   * // userid api method
   * // returns an array
   * // second element of array may be an array
   * @param $username
   *
   * @return array
   */

  public function GetUserInfoByName($username)
  {
    if(!$username) { return [FALSE, 'Invalid username ' . $username]; }
    
    $uid = $this->GetUserID($username);
    
    return $this->GetUserInfo($uid);
  }
  
  /**
   * @param      $propname
   * @param null $propvalue
   * @note cannot work on encrypted properties.
   *
   * @return array
   */
  protected function GetUserInfoByProperty($propname, $propvalue = NULL)
  {
    $module = $this->GetModule();
    
    /** @var array $defns */
    $defns = $this->GetPropertyDefns();
    
    if(!is_array($defns))
    {
      return [FALSE, $module->Lang('error_dberror')];
    }
    
    if(!isset($defns[$propname]))
    {
      return [FALSE, $module->Lang('error_dberror')];
    }
    
    if($defns[$propname]['encrypt'])
    {
      return [FALSE, $module->Lang('error_dberror')];
    }
    
    $db    = $this->GetDb();
    $parms = [$propname];
    $query = 'SELECT userid FROM ' . CMS_DB_PREFIX . 'module_mams_properties up WHERE up.title = ?';
    
    if(NULL !== $propvalue)
    {
      $query   .= ' AND data = ?';
      $parms[] = $propvalue;
    }
    
    $uid = $db->GetOne($query, $parms);
    
    if(!$uid)
    {
      return [FALSE, $module->Lang('error_usernotfound')];
    }
    
    return $this->GetUserInfo($uid);
  }
  
  /**
   * @param        $uid
   * @param string $action
   * @param int    $count
   *
   * @return array
   */
  public function GetUserHistory($uid, $action = '', $count = -1)
  {
    $db    = $this->GetDb();
    $parms = [$uid];
    $query = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_history WHERE userid = ?';
    
    if(!empty($action))
    {
      $query   .= ' AND action = ?';
      $parms[] = $action;
    }
    $query .= ' ORDER BY refdate DESC';
    
    $results = [];
    
    if($count <= 0)
    {
      $results = $db->GetArray($query, $parms);
    }
    else
    {
      $dbr = $db->SelectLimit($query, (int)$count);
      
      while($dbr && ($row = $dbr->FetchRow()))
      {
        $results[] = $row;
      }
      
      if(1 === count($results))
      {
        $tmp     = $results[0];
        $results = $tmp;
      }
    }
    
    return $results;
  }
  
  /**
   * @param string $not_active_since
   *
   * @return array
   */
  public function GetLoggedInUsers($not_active_since = '')
  {
    $db = $this->GetDb();
    
    $q      = 'SELECT userid FROM ' . CMS_DB_PREFIX . 'module_mams_loggedin';
    $qparms = [];
    
    if($not_active_since)
    {
      $q        .= ' WHERE lastused < ?';
      $qparms[] = $not_active_since;
    }
  
    return $db->GetCol($q, $qparms);
  }
  
  /**
   * userid api method
   * returns an int or false
   *
   * @param $groupid
   *
   * @return bool|int
   */
  public function CountUsersInGroup($groupid)
  {
    $db = $this->GetDb();
    
    $parms = [];
    
    if($groupid == '' || $groupid < 0)
    {
      $q = 'SELECT count(id) as num FROM ' . CMS_DB_PREFIX . 'module_mams_users WHERE coalesce(disabled,0) = 0';
    }
    else
    {
      $q       = 'SELECT count(id) as num FROM ' . CMS_DB_PREFIX . 'module_mams_users,' .
                 CMS_DB_PREFIX . 'module_mams_belongs WHERE id=userid AND groupid = ? AND coalesce(disabled,0) = 0';
      $parms[] = $groupid;
    }
    
    $dbresult = $db->Execute($q, $parms);
    
    if(!$dbresult) { return FALSE; }
  
    /** @var array $row */
    $row = $dbresult->FetchRow();
    
    return $row['num'];
  }
  
  /**
   * TODO Replace this code with a user query
   *
   * @param $gid
   *
   * @return array|bool
   * @throws \CmsException
   */
  public function GetFullUsersInGroup($gid)
  {
    $db    = $this->GetDb();
    $query = 'SELECT userid FROM ' . CMS_DB_PREFIX . 'module_mams_belongs WHERE groupid = ?';
    $tmp   = $db->GetCol($query, [$gid]);
    
    if(!$tmp) { return FALSE; }
    
    mams_user_cache::load_users($tmp, TRUE);
    $out = [];
    
    foreach($tmp as $one)
    {
      $one = (int)$one;
      
      if($one < 1) { continue; }
      
      $rec = mams_user_cache::get_user_noload($one, TRUE);
      
      if(is_array($rec) && isset($rec['fprops']))
      {
        $tmp2 = [];
        
        foreach($rec['fprops'] as $one_prop)
        {
          $tmp2[$one_prop['title']] = $one_prop['data'];
        }
        
        $rec['props'] = $tmp2;
      }
      
      $out[] = $rec;
    }
    
    if( count($out) ) { return $out; }
    
    return FALSE;
  }
  

  /**
   * userid api method
   * @param $gid
   *
   * @return bool
   */
  public function GroupExistsByID($gid)
  {
    $data = $this->GetGroupInfo($gid);
    
    return (FALSE !== $data);
  }

  /**
   * userid api method
   *
   * @param $name
   *
   * @return bool
   */
  public function GroupExistsByName($name)
  {
    $gid = $this->GetGroupID($name);
    
    return (FALSE !== $gid);
  }
  
  /**
   * @return bool|string
   */
  function LoggedInEmail()
  {
    $userid = $this->LoggedInId();
    
    return $this->GetEmail($userid);
  }
  
  /**
   * @param $uid
   *
   * @return bool|string
   */
  public function GetEmail($uid)
  {
    $module = $this->GetModule();
    $db     = $this->GetDb();
    
    if($module->GetPreference('username_is_email'))
    {
      $tmp = mams_user_cache::get_user($uid);
      
      if( !is_array($tmp) ) { return FALSE; }
      
      $result = $tmp['username'];
    }
    else
    {
      // get the users first email address.
      $q      = 'SELECT data FROM ' . CMS_DB_PREFIX . 'module_mams_propdefn,' .
                CMS_DB_PREFIX . 'module_mams_properties WHERE name=title AND type=2 AND userid = ?';
      $result = $db->GetOne($q, [$uid]);
    }
    
    return $result;
  }
  
  
  /**
   * @param      $email
   * @param int  $uid
   * @param bool $check_existing
   * @deprecated use \MAMS\utils::is_valid_email_address
   * @see       \MAMS\utils::is_valid_email_address
   *
   * @return array
   */
  public function IsValidEmailAddress($email, $uid = -1, $check_existing = FALSE)
  {
    return MAMS\utils::is_valid_email_address($email, $uid, $check_existing);
  }
  
  /**
   * @return string
   */
  public function GetUsernamePrompt()
  {
    $consumer = MAMS\utils::get_auth_consumer();
    
    return $consumer->get_username_prompt();
  }
  
  
  /**
   * todo: move this to utils.
   * @param      $username
   * @param bool $check_email
   * @param int  $uid
   *
   * @return bool
   */
  public function IsValidUsername($username, $check_email = TRUE, $uid = -1)
  {
    $consumer = MAMS\utils::get_auth_consumer();
    
    return $consumer->validate_username($username, $check_email, $uid);
  }
  
  /**
   * userid api method
   *
   * @param $uid
   * @param $gid
   *
   * @return array|bool[]
   */
  public function RemoveUserFromGroup($uid, $gid)
  {
    $uid = (int)$uid;
    $gid = (int)$gid;
    $db  = \cms_utils::get_db();
    
    try
    {
      if($uid < 1) { return [FALSE, 'Invalid UID']; }
      
      $db->BeginTrans();
      $parms = [$uid];
      $q     = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_belongs WHERE userid = ?';
      
      if($gid > 0)
      {
        $q       .= ' AND groupid = ?';
        $parms[] = $gid;
      }
      $db->Execute($q, $parms);
      $db->CommitTrans();
      
      return [TRUE, 'User removed from group'];
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit($uid, $mod->GetName(), 'Could not remove user from group ' . $gid);
      \xt_utils::log_exception($e);
      
      return [FALSE, $e->getMessage()];
    }
  }
  
  /**
   * userid api method
   *
   * @param      $id
   * @param      $name
   * @param null $desc
   *
   * @return array
   */
  public function SetGroup($id, $name, $desc = NULL)
  {
    $id   = (int)$id;
    $name = trim($name);
    $desc = trim($desc);
    $mod = $this->GetModule();
    $this->_groupinfo_cache = NULL;
    
    if($id < 1 || !$name)
    {
      return [FALSE, $mod->Lang('error_insufficientparams')];
    }
  
    $db  = $this->GetDb();
    $eid = $this->GetGroupID($name);
    
    if(FALSE !== $eid && $id !== $eid)
    {
      return [FALSE, $mod->Lang('error_groupname_exists')];
    }
    
    $q        = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_groups SET groupname = ?, groupdesc = ? WHERE id = ?';
    $dbresult = $db->Execute($q, [$name, $desc, $id]);
    
    if(!$dbresult)
    {
      return [FALSE, $db->ErrorMsg()];
    }
    
    $this->_groupinfo_cache = NULL;
    
    return [TRUE, ''];
  }
  
  /**
   * @param $uid
   * @param $plain_password
   *
   * @return false|string|null
   */
  public function EncryptPassword($uid, $plain_password)
  {
    /* should be protected */
    $use_usersalt = $this->use_usersalt();
    
    if(!$use_usersalt)
    {
      // this is really, really bad.. but kept for old installs.
      audit('', 'MAMS', 'User is using unsafe password encryption');
      
      return md5( $plain_password . $this->get_bad_salt() );
    }
  
    if(1 === $use_usersalt)
    {
      $db   = $this->GetDb();
      $salt = $db->GetOne('SELECT salt FROM ' . CMS_DB_PREFIX . 'module_mams_users WHERE id = ?', [$uid]);
      
      if(!$salt)
      {
        throw new \LogicException(
          'Using user salted passwords, but no salt set for this user (or the user could not be found)'
        );
      }
      
      return sha1($plain_password . $salt);
    }
  
    // use_usersalt == 2
    return password_hash($plain_password, PASSWORD_BCRYPT);
  }
  
  /**
   * @param $uid
   * @param $password
   *
   * @return array
   */
  public function SetUserPassword($uid, $password)
  {
    $uid      = (int)$uid;
    $password = trim((string)$password);
    $mod      = $this->GetModule();
    
    if($uid < 1 || !$password)
    {
      return [FALSE, $mod->Lang('error_invalidparams')];
    }
    
    if(!$uid)
    {
      return [FALSE, $mod->Lang('error_invalidparams')];
    }
    
    $db = $this->GetDb();
    
    // going forward, we use no salt column.
    $pw       = $this->EncryptPassword($uid, $password);
    $q        = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_users SET salt = NULL, password = ? WHERE id = ?';
    $dbresult = $db->Execute($q, [$pw, $uid]);
    
    if(!$dbresult)
    {
      return [FALSE, $db->ErrorMsg()];
    }
    
    mams_user_cache::del_user($uid);
    
    return [TRUE, ''];
  }
  
  /**
   * @param      $uid
   * @param bool $flag
   */
  public function SetUserDisabled($uid, $flag = TRUE)
  {
    $uid  = (int)$uid;
    $flag = (bool)$flag;
    
    if($uid < 1)
    {
      throw new \LogicException('Invalid uid passed to ' . __METHOD__);
    }
    
    $db    = $this->GetDb();
    $query = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_users SET disabled = ? WHERE id = ?';
    $dbr   = $db->Execute($query, [$flag, $uid]);
    
    if(!$dbr)
    {
      throw new \LogicException('problem setting uid ' . $uid . ' to disabled');
    }
    
    if($flag)
    {
      $this->LogoutUser($uid, 'user disabled');
    }
    else
    {
      $this->add_history($uid, 'user enabled');
    }
  }
  
  /**
   * @param      $uid
   * @param bool $flag
   */
  public function ForcePasswordChange($uid, $flag = TRUE)
  {
    $uid  = (int)$uid;
    $flag = (bool)$flag;
    
    if($uid < 1)
    {
      throw new \LogicException('Invalid uid passed to ' . __METHOD__);
    }
    
    $db    = $this->GetDb();
    $query = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_users SET force_newpw = ? WHERE id = ?';
    $dbr   = $db->Execute($query, [$flag, $uid]);
    
    if(!$dbr)
    {
      throw new \LogicException('problem setting force_newpw flag for user ' . $uid);
    }
    
    if($flag)
    {
      $this->add_history($uid, 'forcepwreset');
    }
  }
  
  /**
   * @param      $uid
   * @param bool $flag
   */
  public function ForceChangeSettings($uid, $flag = TRUE)
  {
    $uid  = (int)$uid;
    $flag = (bool)$flag;
    
    if($uid < 1)
    {
      throw new \LogicException('Invalid uid passed to ' . __METHOD__);
    }
    
    $db    = $this->GetDb();
    $query = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_users SET force_chsettings = ? WHERE id = ?';
    $dbr   = $db->Execute($query, [$flag, $uid]);
    
    if(!$dbr)
    {
      throw new \LogicException('problem setting force_chsettings flag for user ' . $uid);
    }
    
    if($flag)
    {
      $this->add_history($uid, 'forcechsettings');
    }
  }
  
  /**
   * @param      $uid
   * @param bool $flag
   */
  public function ForceVerify($uid, $flag = TRUE)
  {
    $uid  = (int)$uid;
    $flag = (bool)$flag;
    
    if($uid < 1)
    {
      throw new \LogicException('Invalid uid passed to ' . __METHOD__);
    }
    
    $db    = $this->GetDb();
    $query = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_users SET must_validate = ? WHERE id = ?';
    $dbr   = $db->Execute($query, [$flag, $uid]);
    //if( !$dbr ) throw new \LogicException('problem setting force verify flag for user '.$uid); TODO revisit this line
    
    if($flag)
    {
      $this->add_history($uid, 'init_verify');
    }
  }
  

  /**
   * userid api method
   *
   * @param      $uid
   * @param      $username
   * @param      $password
   * @param bool $expires
   *
   * @return array
   */
  public function SetUser($uid, $username, $password, $expires = FALSE)
  {
    if(!$uid) { return [FALSE, '']; }
    $db     = $this->GetDb();
    $module = $this->GetModule();
    
    // make sure that this user exists
    $ret = $this->GetUserInfo($uid);
    
    if($ret[0] == FALSE)
    {
      return [FALSE, $module->Lang('error_usernotfound')];
    }
    
    // make sure that this username is not taken by some other id
    $nuid = $this->GetUserID($username);
    
    if(FALSE !== $nuid && $nuid !== $uid)
    {
      return [FALSE, $module->Lang('error_usernametaken', $uid)];
    }
    
    $dbresult = '';
    $parms    = [];
    $q        = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_users SET username = ?';
    $parms[]  = $username;
    
    if(FALSE !== $expires)
    {
      $q       .= ', expires = ?';
      $parms[] = trim($db->DBTimeStamp($expires), "'");
    }
    
    $q        .= ' WHERE id = ?';
    $parms[]  = $uid;
    $dbresult = $db->Execute($q, $parms);
    
    if(FALSE === $dbresult)
    {
      return [FALSE, $db->ErrorMsg()];
    }
    
    if($password)
    {
      $res = $this->SetUserPassword($uid, $password);
      
      if(!$res[0])
      {
        return $res;
      }
    }
    
    $this->_useridbyname = [];
    mams_user_cache::del_user($uid);
    
    // Changed to pass $uid back so it matches AddUser()
    return [TRUE, $uid];
  }
  
  /**
   * Set the user group memberships
   * does not alter any user properties.
   * does not validate group ids, but does validate uid
   *
   * @param int userid
   * @param array array of integer group ids
   *
   * @return array (status,msg)
   */
  public function SetUserGroups($uid, $grpids)
  {
    if(!$uid) {  return [FALSE, '']; }
    
    $db = $this->GetDb();
    
    // first make sure this user exists
    $ret = $this->GetUserInfo($uid);
    
    if(FALSE === $ret[0])
    {
      return [FALSE, 'User does not exist'];
    }
    
    // then remove all his current assignments
    $q        = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_belongs WHERE userid = ?';
    $dbresult = $db->Execute($q, [$uid]);
    
    if(!$dbresult)
    {
      return [FALSE, $db->ErrorMsg()];
    }
    
    if(is_array($grpids) && count($grpids))
    {
      // and add all of them in
      $q = 'INSERT INTO ' . CMS_DB_PREFIX . 'module_mams_belongs VALUES (?,?)';
      foreach($grpids as $grpid)
      {
        $dbresult = $db->Execute($q, [$uid, $grpid]);
        
        if(!$dbresult)
        {
          return [FALSE, $db->ErrorMsg()];
        }
      }
    }
    
    return [TRUE, ''];
  }
  
  /**
   * @param $uid
   * @param $gid
   *
   * @return bool
   */
  public function AddUserToGroup($uid, $gid)
  {
    return $this->AssignUserToGroup($uid, $gid);
  }
  
  /**
   * userid api method
   *
   * @param $uid
   * @param $props
   *
   * @return bool
   */
  public function SetUserProperties($uid, $props)
  {
    if(!$uid) { return FALSE; }
    
    // Delete all the user properties
    // and set new ones
    $this->DeleteUserPropertyFull('', $uid, TRUE);
    mams_user_cache::del_user($uid);
    
    if( is_array($props) && count($props) )
    {
      if(xt_array::is_hash($props))
      {
        foreach($props as $key => $val)
        {
          if( FALSE === ( $r = $this->SetUserPropertyFull($key, $val, $uid) ) )
          {
            return FALSE;
          }
        }
      }
      else
      {
        foreach($props as $prop)
        {
          [$key, $val] = explode('=', $prop, 2);
          
          if( FALSE === ( $r = $this->SetUserPropertyFull($key, $val, $uid) ) )
          {
            return FALSE;
          }
        }
      }
    }
    
    return TRUE;
  }
  
  /**
   * userid api method
   * @param $uid
   *
   * @return bool
   */
  public function UserExistsByID($uid)
  {
    if(!$uid) { return FALSE; }
    
    $data = $this->GetUserInfo($uid);
    
    return (FALSE !== $data[0]);
  }
  
  //
  // returns an array or false
  /**
   * userid api method
   *
   * @param $uid
   *
   * @return array|bool
   */
  public function GetUserProperties($uid)
  {
    if(!$uid) { return FALSE; }
    
    $uinfo = mams_user_cache::get_user($uid, TRUE);
    
    if(!is_array($uinfo) || !isset($uinfo['fprops'])) { return FALSE; }
    
    return $uinfo['fprops'];
  }
  

  /**
   * userid api method
   * returns an array of records or false
   *
   * @param $userid
   *
   * @return array|bool
   */
  public function GetMemberGroupsArray($userid)
  {
    $auth_consumer = mams_utils::get_auth_consumer();
    
    if($auth_consumer->has_capability($auth_consumer::CAPABILITY_GROUPMEMBERSHIP))
    {
      $out = $auth_consumer->get_group_membership($userid);
      
      if(!is_array($out) || 0 === count($out)) { return FALSE; }
      
      $res = [];
      
      foreach($out as $one)
      {
        $res[] = ['userid' => $userid, 'groupid' => $one];
      }
      
      return $res;
    }
    
    // shouldn't be used anymore... but just in case.
    //$db       = $this->GetDb();
    $q        = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_belongs WHERE userid=?';
    $dbresult = $this->GetDb()->Execute($q, [$userid]);
    
    if($dbresult && $dbresult->RecordCount())
    {
      $result = [];
      
      while($row = $dbresult->FetchRow()) { $result[] = $row; }
      
      return $result;
    }
  
    return FALSE;
  }
  
  //
  // end of rc functions
  //
  
  /**
   * userid api method
   *
   * @param      $title
   * @param bool $defaultvalue
   *
   * @return bool|mixed
   */
  public function GetUserProperty($title, $defaultvalue = FALSE)
  {
    $userid = $this->LoggedInId();
    
    if(FALSE === $userid) { return FALSE; }
    
    return $this->GetUserPropertyFull($title, $userid, $defaultvalue);
  }
  
  
  /**
   * userid api method
   *
   * @param      $title
   * @param      $userid
   * @param bool $defaultvalue
   *
   * @return bool|mixed
   */
  public function GetUserPropertyFull($title, $userid, $defaultvalue = FALSE)
  {
    if(FALSE === $userid) { return FALSE; }
    
    $uinfo = mams_user_cache::get_user($userid, TRUE);
    
    $defn = $this->GetPropertyDefn($title);
    
    if(!$defn) { return FALSE; }
    
    if(isset($uinfo['fprops']))
    {
      foreach($uinfo['fprops'] as $oneprop)
      {
        if($oneprop['title'] === $title)
        {
          return $oneprop['data'];
        }
      }
    }
    
    return $defaultvalue;
  }
  
  /**
   * userid api method
   *
   * @param $uid
   * @param $title
   * @param $data
   *
   * @return bool
   */
  public function IsUserPropertyValueUnique($uid, $title, $data)
  {
    $db  = $this->GetDb();
  
    if($uid > 0)
    {
      $q   = 'SELECT id FROM ' . CMS_DB_PREFIX . 'module_mams_properties
            WHERE title = ? AND userid != ? AND data = ?';
      $dbr = $db->GetOne($q, [$title, $uid, $data]);
    }
    else
    {
      $q   = 'SELECT id FROM ' . CMS_DB_PREFIX . 'module_mams_properties
            WHERE title = ? AND data = ?';
      $dbr = $db->GetOne($q, [$title, $data]);
    }
    
    if($dbr) { return FALSE; }
    
    return TRUE;
  }
  
  
  /**
   * userid api method
   *
   * @param $title
   * @param $data
   *
   * @return bool
   */
  public function SetUserProperty($title, $data)
  {
    $userid = $this->LoggedInId();
    
    if(FALSE === $userid) { return FALSE; }
    
    return $this->SetUserPropertyFull($title, $data, $userid);
  }
  
  
  /**
   * userid api method
   *
   * @param $title
   * @param $data
   * @param $userid
   *
   * @return bool
   */
  public function SetUserPropertyFull($title, $data, $userid)
  {
    if(FALSE === $userid){ return FALSE; }

    $defn = $this->GetPropertyDefn($title);
    
    if(!$defn){ return FALSE; }
    
    if($defn['encrypt'])
    {
      // gotta encrypt.
      $key  = $this->GetEncryptionKey($userid);
      $data = base64_encode(xt_encrypt::encrypt($key, $data));
    }
    
    if($defn['force_unique'] && !$this->IsUserPropertyValueUnique($userid, $title, $data))
    {
      return FALSE;
    }
    
    $db = $this->GetDb();
    $q  = 'SELECT * FROM ' . CMS_DB_PREFIX . 'module_mams_properties WHERE title=? AND userid=?';
    $p  = [$title, $userid];
    $r  = $db->Execute($q, $p);
    
    if(!$r || (0 === $r->NumRows()))
    {
      $q = 'INSERT INTO ' . CMS_DB_PREFIX . 'module_mams_properties (userid,title,data) VALUES (?,?,?)';
      $p = [$userid, $title, $data];
      $r = $db->Execute($q, $p);
    }
    else
    {
      /** @var array $row */
      $row = $r->FetchRow();
      $q   = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_properties SET data=? WHERE id=?';
      $p   = [$data, (int)$row['id']];
      $r   = $db->Execute($q, $p);
    }
    
    mams_user_cache::del_user($userid);
    
    return (FALSE !== $r);
  }
  
  
  /**
   * delete all occurrences of the userproperty by name
   *
   * @param $title
   *
   * @return bool
   */
  public function DeleteUserPropertyByName($title)
  {
    $db     = $this->GetDB();
    $q      = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_properties WHERE title=?';
    $p      = [$title];
    $result = $db->Execute($q, $p);
    
    mams_user_cache::clear_all();
    
    return (FALSE !== $result);
  }

  
  /**
   * userid api method
   *
   * @param      $title
   * @param bool $all
   *
   * @return bool
   */
  public function DeleteUserProperty($title, $all = FALSE)
  {
    $userid = $this->LoggedInId();
    
    if(FALSE === $userid) { return FALSE; }
    
    return $this->DeleteUserPropertyFull($title, $userid, $all);
  }
  
  /**
   * userid api method
   *
   * @param      $title
   * @param      $userid
   * @param bool $all
   *
   * @return bool
   */
  public function DeleteUserPropertyFull($title, $userid, $all = FALSE)
  {
    $db = $this->GetDb();
    $q  = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_properties WHERE userid=?';
    if(!$all)
    {
      $q .= ' AND title=?';
    }
  
    if($all)
    {
      $p = [$userid];
    }
    else
    {
      $p = [$userid, $title];
    }
    
    $result = $db->Execute($q, $p);
    
    mams_user_cache::del_user($userid);
    
    return (FALSE !== $result);
  }
  
  /**
   * userid api method
   *
   * @return bool
   */
  public function DeleteAllUserProperties()
  {
    return $this->DeleteUserProperty('', TRUE);
  }

  /**
   * userid api method
   *
   * @param $userid
   *
   * @return bool
   */
  public function DeleteAllUserPropertiesFull($userid)
  {
    return $this->DeleteUserPropertyFull('', $userid, TRUE);
  }
  
  /**
   * @return int
   */
  protected function use_usersalt()
  {
    $mod = $this->GetModule();
    
    return (int)$mod->GetPreference('use_usersalt', 2);
  }
  
  /**
   * @param        $username
   * @param        $password
   * @param string $groups
   *
   * @return int|mixed
   */
  public function CheckPassword($username, $password, $groups = '')
  {
    $tmp = $this->GetUserInfoByName($username);
    
    if(!is_array($tmp) || !isset($tmp[0]) || !$tmp[0] || !is_array($tmp[1]))
    {
      return FALSE;
    }
    
    $uinfo = $tmp[1];
    
    if(!isset($uinfo['id']) && !isset($uinfo['password']))
    {
      return FALSE;
    }
    
    if($groups)
    {
      $ginfo = $this->GetMemberGroupsArray((int)$uinfo['id']);
      
      if(!$ginfo) { return FALSE; } // user not a member of any groups
      
      $member_gids = array_map(
        static function($row) {
          return (int)$row['groupid'];
        }, $ginfo
      );
      
      unset($ginfo);
      $groups = explode(',', $groups);
      
      // convert these group names to gids.
      $gids      = [];
      $allgroups = $this->GetGroupList();
      
      if(!$allgroups) { return 0; } // no groups defined
      
      $allgroups = array_flip($allgroups);
      
      foreach($groups as $one)
      {
        $one = trim($one);
        
        if(!$one) { continue; }
        
        $gid = array_search($one, $allgroups); // TODO see if 3rd parameter can/should be used JM
        
        if(FALSE === $gid) { continue; } // group does not exist
        
        $gids[] = $gid;
      }
      
      unset($allgroups, $groups);
      $gids = array_unique($gids);
      
      if(empty($gids)) { return 0; } // no groups specified actually exist.
      
      $tmp = array_intersect($gids, $member_gids);
      
      if(empty($tmp)) { return 0; }
    }
    
    // now verify the hash
    if(32 === strlen($uinfo['password']))
    {
      // this is the old, not really good hashing.
      // kept for backwards compatibility.
      if(!empty($uinfo['salt']))
      {
        $tmpb = md5(trim($password) . $uinfo['salt']);
        
        audit($uinfo['id'], 'MAMS', 'User has a salt, but an md5 encoded password');
        
        if($uinfo['password'] === $tmpb) { return $uinfo['id']; }
      }
      
      $tmpa = md5(trim($password) . $this->get_bad_salt());
      
      if($uinfo['password'] === $tmpa) { return $uinfo['id']; }
    }
    else if($uinfo['salt'])
    {
      // user has a salt... so it is using old sha1 hashing
      $tmp = sha1($password . $uinfo['salt']);
      
      if($uinfo['password'] === $tmp) { return $uinfo['id']; }
    }
    
    // usersalt == 2, new password hashes.
    return password_verify($password, $uinfo['password']) ? $uinfo['id'] : 0;
  }
  
  /**
   * userid api method
   *
   * @return string
   */
  public function LoggedInName()
  {
    $userid = $this->LoggedInId();
    
    if($userid) { return $this->GetUserName($userid); }
  
    return '';
  }
  

  /**
   * userid api method
   * this is for frontend code
   * @param $uid
   * @param $message
   *
   * @return void
   */
  public function Logout($uid = '', $message = 'logout')
  {
    if($uid != '')
    {
      $this->LogoutUser($uid, $message);
      return;
    }
    
    $uid = (int)$this->LoggedInId();
    if($uid < 1) { return; }
    
    // logout the current user (clears session and cookies).
    $this->_clear_logindetails($uid);
    
    // and add history info
    $this->add_history($uid, $message);
    
    // send the event.
    $module = $this->GetModule();
    \CMSMS\HookManager::do_hook('MAMS::OnLogout', ['id' => $uid]);
  }
  
  
  /**
   * this deletes the entry from the loggedin table, and adds a history entry.
   * it also sets some cache data used to force logout the user
   * designed to programmatically log out a user from the admin panel of the module.
   * so that the next time a user does anything on this site, and as long as cache is not cleared
   * the next time he does a request on the website, his cookies and session data will be cleared
   * another option here would be to use a flag in the database that indicates the user should be logged out.
   *
   * @param $uid
   * @param $eventmsg
   *
   * @return void
   */
  // userid api method
  public function LogoutUser($uid, $eventmsg = 'logout')
  {
 
    cms_cache_handler::get_instance()->set('mams_logout_' . $uid, 1, __CLASS__);
    
    if(!$uid) { return; }
    
    $db     = $this->GetDb();
    $q      = "DELETE FROM " . CMS_DB_PREFIX . "module_mams_loggedin WHERE userid=?";
    $p      = [$uid];
    $result = $db->Execute($q, $p);
    
    $this->add_history($uid, $eventmsg);
  }
  
  /**
   * @return void
   */
  public function ExpireUsers()
  {
    static $_hasrun;
    
    if($_hasrun) { return; }
    
    $_hasrun = TRUE;
    
    // expires users who have not been 'touched' within the last N seconds.
    $module          = $this->GetModule();
    $expire_interval = (int)$module->GetPreference('expireusers_interval', 180);
    $expire_lastrun  = $this->get_expireusers_lastrun();
    
    if(time() - $expire_lastrun >= $expire_interval)
    {
      $expirytime = $module->GetPreference('user_session_expires');
      $db         = $this->GetDb();
      
      // todo: begin transaction
      $q        = "SELECT * FROM " . CMS_DB_PREFIX . "module_mams_loggedin WHERE lastused < ?";
      $p        = [time() - $expirytime];
      $dbresult = $db->Execute($q, $p);
      
      while($dbresult && ($row = $dbresult->FetchRow()))
      {
        $this->add_history($row['userid'], 'expire');
        $key = $this->keepalive_cookie();
        
        if(isset($_SESSION[$key]) && '' != $_SESSION[$key])
        {
          // this logs the current user out.
          $data = $this->decode_userdata($_SESSION[$key]);
          
          if(isset($data['uid']) && $data['uid'] == $row['userid'])
          {
            $this->_clear_logindetails($row['userid'], TRUE);
          }
        }
        
        $this->NotifyExpiredUser($row['userid']);
      }
      
      $q      = "DELETE FROM " . CMS_DB_PREFIX . "module_mams_loggedin WHERE lastused<?";
      $result = $db->Execute($q, $p);
      
      // todo: commit transaction
      
      $this->touch_expireusers_lastrun();
    }
  }
  
  // userid api method
  
  /**
   * @return false|int|mixed
   */
  public function LoggedInId()
  {
    // if the user is authenticated using the auth module
    $module        = $this->GetModule();
    $auth_consumer = mams_utils::get_auth_consumer();
    
    if(!$auth_consumer->has_capability($auth_consumer::CAPABILITY_LOGIN))
    {
      // it's the built-in stuff.
      return $this->_std_LoggedInId();
    }
    
    // either the consumer doesn't provide absolute login (CAPABILITY_LOGIN)
    // OR the user is not authenticated via the old stuff.
    if($auth_consumer->is_authenticated())
    {
      // authenticated via some external mechanism... still need to tie this to an
      // internal user record for properties and groups and cruft.
      
      // search for a userid based on a property
      $prop = $auth_consumer->get_connecting_property_name();
      $val  = $auth_consumer->get_unique_identifier();
      
      if(!$val) { return FALSE; }
      
      $uinfo   = '';
      $useprop = FALSE;
      
      if('' == $prop || mams_auth_consumer::PROPERTY_USERNAME == $prop)
      {
        // get user by name
        $uinfo = $this->GetUserInfoByName($auth_consumer->get_username($val));
      }
      else if($prop == mams_auth_consumer::PROPERTY_UID)
      {
        // see if the uid exists.
        $uinfo = $this->GetUserInfo($val);
      }
      else
      {
        // it's a property of some type.
        $uinfo   = $this->GetUserInfoByProperty($prop, $val);
        $useprop = TRUE;
      }
      
      #                      || <- at this point $uinfo is an array (JM)
      //if(!is_array($uinfo) || (is_array($uinfo) && FALSE == $uinfo[0]) ) ## maybe the next line works better?
      if(!is_array($uinfo) || !$uinfo[0])
      {
        // user authenticated but not found, do we need to create one?
        if($module->GetPreference('auto_create_unknown'))
        {
          // we're going to create a new user.
          $username = '';
          
          if($auth_consumer->has_capability($auth_consumer::CAPABILITY_USERNAME))
          {
            $username = $auth_consumer->get_username();
          }
          
          if(!$username)
          {
            $username = $val;
            if(
              $module->GetPreference('use_randomusername') &&
              $prop != mams_auth_consumer::PROPERTY_USERNAME &&
              $prop != mams_auth_consumer::PROPERTY_UID &&
              '' != $prop
            )
            {
              $username = $module->GenerateRandomUsername();
            }
          }
          
          $tmp     = $module->GetPreference('expireage_months', 520);
          $expires = strtotime(sprintf('+%d months', $tmp));
          
          $ret = $this->AddUser(
            $username,
            mams_utils::generate_random_printable_string(),
            $expires, TRUE, TRUE
          );
          
          if(!$ret[0])
          {
            $module->Audit('', $module->GetName(), $ret[1]);
            
            return FALSE;
          }
          
          $uid = $ret[1];
          
          // set his groups.
          $dflt_groups = $this->GetDfltGroups();
          
          if(is_array($dflt_groups) && count($dflt_groups))
          {
            # was $res but smelled like an error
            $ret = $this->SetUserGroups($uid, $dflt_groups);
          }
          
          // now set a property.
          if($useprop)
          {
            $ret = $this->SetUserPropertyFull($prop, $val, $uid);
            
            if(!$ret)
            {
              // should remove the user...
              $module->Audit('', $module->GetName(), $module->Lang('error_problemsettinginfo'));
              
              return FALSE;
            }
          }
          
          $module->Audit($uid, $module->GetName(), $module->Lang('audit_user_created'));
          
          return $uid;
        }
      }
      else
      {
        return $uinfo[1]['id'];
      }
    }
    
    return FALSE;
  }
  
  /**
   * NOT FOR EXTERNAL USE
   *
   * @internal
   */
  public function _std_LoggedInId()
  {
    $sessionid = session_id();
    if(!$sessionid)
    {
      return FALSE;
    }
    
    // if we are expired this will log us out. calls _clear_logindetails
    $this->ExpireUsers();
    
    // get the login details from session, cookies or from the database
    $data = $this->_get_saved_logindetails();
    if(!$data || !isset($data['uid']))
    {
      return FALSE;
    }
    $uid = (int)$data['uid'];
    
    // if we are force logged out
    if(cms_cache_handler::get_instance()->exists('mams_logout_' . $uid, __CLASS__))
    {
      cms_cache_handler::get_instance()->erase('mams_logout_' . $uid, __CLASS__);
      // clears cookies and session data
      $this->_clear_logindetails($uid);
      
      return FALSE;
    }
    
    // actually does the logging in to the database, and sets cookies and session data.
    $this->_set_saved_logindetails($data);
    
    return $uid;
  }
  
  // userid api method
  public function LoggedIn()
  {
    if(!$this->LoggedInId())
    {
      return FALSE;
    }
    
    return TRUE;
  }
  
  /**
   * Determine if the user id is a member of the group(s) specified.
   *
   * @param integer userid
   * @param mixed integer (positive) group id, or an array of positive integer group ids.
   *
   * @return boolean
   */
  public function MemberOfGroup($userid, $groupid)
  {
    if($userid < 1) { return FALSE; }
    
    // cleanup the input groupid list.
    if(!is_array($groupid))
    {
      $groupid = [(int)$groupid];
    }
    
    $groupid = array_values($groupid);
    
    foreach($groupid as $i => $iValue)
    {
      if($iValue < 1) { unset($groupid[$i]); }
    }
    
    if(!is_array($groupid) || 0 === count($groupid)) { return FALSE; }
    
    $tmp = $this->GetMemberGroupsArray($userid);
    
    if(!is_array($tmp) || 0 === count($tmp)) { return FALSE; }
    
    $membership = xt_array::extract_field($tmp, 'groupid');
    $tmp        = array_intersect($membership, $groupid);
    
    if(count($tmp)) { return TRUE; }
    
    return FALSE;
  }
  
  /**
   * userid api method
   *
   * @param $userid
   *
   * @return string
   */
  public function GetUserName($userid)
  {
    $row = mams_user_cache::get_user($userid);
    
    if(!$row) { return ''; }
    
    return $row['username'];
  }
  
  
  /**
   * userid api method
   *
   * @param $username
   *
   * @return int
   */
  public function GetUserID($username)
  {
    if( !is_array($this->_useridbyname) )
    {
      $this->_useridbyname = [];
    }
    
    if(!isset($this->_useridbyname[$username]))
    {
      $db  = $this->GetDb();
      $q   = 'SELECT id FROM ' . CMS_DB_PREFIX . 'module_mams_users WHERE username = ?';
      $uid = (int)$db->GetOne($q, [$username]);
      
      if($uid < 1) { return -1; }
      
      $this->_useridbyname[$username] = $uid;
      
      return $uid;
    }
    
    return (int)$this->_useridbyname[$username];
  }
  
  /**
   * userid api method
   *
   * @param $name
   * @param $description
   *
   * @return array
   */
  public function AddGroup($name, $description)
  {
    $db = $this->GetDb();
    
    // see if it exists already or not (by name)
    $q        = "SELECT * FROM " . CMS_DB_PREFIX . "module_mams_groups WHERE groupname = ?";
    $dbresult = $db->Execute($q, [$name]);
    if(!$dbresult)
    {
      return [FALSE, $db->ErrorMsg()];
    }
    
    $row = $dbresult->FetchRow();
    
    if($row)
    {
      $module = $this->GetModule();
      
      return [FALSE, $module->Lang('error_groupname_exists')];
    }
    
    $q        = "INSERT INTO " . CMS_DB_PREFIX . "module_mams_groups (groupname,groupdesc) VALUES (?,?)";
    $dbresult = $db->Execute($q, [$name, $description]);
    
    if(!$dbresult)
    {
      return [FALSE, $db->ErrorMsg()];
    }
    
    $grpid = $db->Insert_ID();
    unset($this->_groupinfo_cache);
    
    return [TRUE, $grpid];
  }
  
  /**
   * userid api method
   *
   * @param      $name
   * @param      $password
   * @param      $expires
   * @param bool $nonstd
   * @param null $createdate
   *
   * @return array
   */
  public function AddUser($name, $password, $expires, $nonstd = FALSE, $createdate = NULL)
  {
    $db = $this->GetDb();
    
    // see if it exists already or not (by name)
    $uid = $this->GetUserID($name);
    
    if($uid > 0)
    {
      $module = $this->GetModule();
      
      return [FALSE, $module->Lang('error_username_exists')];
    }
    
    // generate the salt.
    $salt = NULL;
    
    if(1 === $this->use_usersalt())
    {
      $salt = sha1(time() . $name . rand() . $expires . __FILE__);
    }
    
    // insert the record
    $createdate = (int)$createdate;
    
    if($createdate < 1) { $createdate = time(); }
    
    $q        = 'INSERT INTO ' . CMS_DB_PREFIX .
                'module_mams_users (username,password,createdate,expires,nonstd,salt) VALUES (?,?,?,?,?,?)';
    $dbresult = $db->Execute(
                              $q,
                              [
                                $name, ' ',
                                trim($db->DbTimeStamp($createdate), "'"),
                                trim($db->DbTimeStamp($expires), "'"),
                                (bool)$nonstd, $salt
                              ]
                            );
    
    if(!$dbresult)
    {
      return [FALSE, $db->ErrorMsg()];
    }
    
    $uid = $db->Insert_ID();
    
    // set the password
    $res = $this->SetUserPassword($uid, $password);
    
    if(!$res[0]) { return $res; }
    
    $this->_useridbyname = [];
    
    return [TRUE, $uid];
  }
  
  /**
   * userid api method
   *
   * @param $gid
   *
   * @return string
   */
  public function GetGroupName($gid)
  {
    $this->_get_groupinfo();
    
    if(is_array($this->_groupinfo_cache) && isset($this->_groupinfo_cache[$gid]))
    {
      return (string)$this->_groupinfo_cache[$gid]['groupname'];
    }
    
    return '';
  }
  
  
  /**
   * userid api method
   *
   * @param $gid
   *
   * @return string
   */
  public function GetGroupDesc($gid)
  {
    $this->_get_groupinfo();
    
    if(is_array($this->_groupinfo_cache) && isset($this->_groupinfo_cache[$gid]))
    {
      return (string) $this->_groupinfo_cache[$gid]['groupdesc'];
    }
    
    return '';
  }

  /**
   * userid api method
   *
   * @param $id
   *
   * @return array
   */
  public function DeleteGroupFull($id)
  {
    $db = $this->GetDb();
    try
    {
      $db->BeginTrans();
      
      // delete all property relations from this group
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_grouppropmap WHERE group_id = ?';
      $db->Execute($q, [$id]);
      
      // delete all indication that anybody is a member
      // of this group
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_belongs WHERE groupid = ?';
      $db->Execute($q, [$id]);
      
      // and then delete the group
      $q = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_groups WHERE id = ?';
      $db->Execute($q, [$id]);
      
      $db->CommitTrans();
      $this->_groupinfo_cache = NULL;
      
      return [TRUE, ''];
    }
    catch(\Exception $e)
    {
      $db->RollbackTrans();
      $mod = $this->GetModule();
      audit($id, $mod->GetName(), 'Problem Deleting Group: ' . $e->getMessage());
      \xt_utils::log_exception($e);
      
      return [FALSE, $e->getMessage()];
    }
  }

  /**
   * userid api method
   *
   * @return array
   */
  public function GetGroupList()
  {
    $this->_get_groupinfo();
    $result = [];
    
    if(is_array($this->_groupinfo_cache))
    {
      foreach($this->_groupinfo_cache as $gid => $info)
      {
        $result[$info['groupname']] = (int)$gid;
      }
    }
    
    return $result;
  }
  
  // userid api method
  /**
   * userid api method
   *
   * @param bool $with_count
   *
   * @return array
   */
  public function GetGroupListFull($with_count = FALSE)
  {
    $this->_get_groupinfo($with_count);
    $result = [];
    
    if(is_array($this->_groupinfo_cache))
    {
      $result = $this->_groupinfo_cache;
    }
    
    return $result;
  }
  
  /**
   * old userid api method
   *
   * @param $groupname
   *
   * @return int
   */
  public function GetGroupID($groupname)
  {
    $this->_get_groupinfo();
    
    if(is_array($this->_groupinfo_cache))
    {
      foreach($this->_groupinfo_cache as $gid => $info)
      {
        if($info['groupname'] === $groupname) { return (int)$gid; }
      }
    }
    
    return -1;
  }
  
  /**
   * Return a list of groups that the user is a member of
   *
   * @param integer The user identifier
   *
   * @return string A comma delimited string containing member group names.  or 'none'
   */
  function GetMemberGroups($userid)
  {
    $auth_consumer = mams_utils::get_auth_consumer();
    
    if(!$auth_consumer->has_capability($auth_consumer::CAPABILITY_GROUPMEMBERSHIP))
    {
      return 'none';
    }
    
    $out = $auth_consumer->get_group_membership($userid);
    
    if(!is_array($out) || 0 === count($out)) { return 'none'; }
    
    $info = $this->_get_groupinfo();
    $tmp  = [];
    
    foreach($out as $one)
    {
      if(isset($info[$one]))
      {
        $tmp[] = $info[$one]['groupname'];
      }
    }
    
    // this may be already false at this point so the following line may not be needed (JM)
    // if(0 === count($out)) { return 'none'; }
    
    return implode(',', $tmp);
  }
  
  /**
   * old userid api method
   *
   * @param $id
   *
   * @return bool
   */
  public function DeleteUser($id)
  {
    /** @var array $res */
    $res = $this->DeleteUserFull($id);
    if($res[0]) { return TRUE; }
    
    return FALSE;
  }
  
  /**
   * @ignore
   * @return array|int[]|mixed|void
   */
  protected function GetDfltGroups()
  {
    $consumer = mams_utils::get_auth_consumer();
    
    if($consumer->has_capability($consumer::CAPABILITY_DEFAULTGROUPS))
    {
      return $consumer->get_default_groups();
    }
    
    $mams       = $this->GetModule();
    $dflt_group = $mams->GetPreference('default_group');
    
    return [(int)$dflt_group];
  }
  
  /**
   * @ignore
   * @param int $uid
   */
  public function SetPermanentCookie(int $uid)
  {
    $mams = $this->GetModule();
    $name = $mams->GetPreference('cookiename');
    
    if(!$name) { return; }
    
    $data = ['uid' => $uid, 'time' => time()];
    $data = $this->encode_userdata($data);
    \cms_cookies::set($name, $data, time() + 60 * 24 * 3600);
  }
  
  /**
   * @return array
   */
  private function _get_saved_logindetails()
  {
    $key       = $this->keepalive_cookie();
    $sessionid = session_id();
    
    if(!$sessionid) { return []; }
    
    $data   = [];
    $module = $this->GetModule();
    
    if(!empty($_SESSION[$key]))
    {
      $data = (array)$this->decode_userdata($_SESSION[$key]);
    }
  
    // see if we can get the data from a session cookie
    if(!$data && $module->GetPreference('cookie_keepalive', 0) && !empty(\cms_cookies::exists($key)))
    {
      $data = (array)$this->decode_userdata(\cms_cookies::get($key));
    }
    
    if(!$data)
    {
      // see if we can get the data from a permanent cookie.
      if('' !== ($cn = $module->GetPreference('cookiename')))
      {
        // this is remember me functionality. as opposed to a session cookie.
        $tmp = \cms_cookies::get($cn);
        
        if($tmp)
        {
          $data = (array)$this->decode_userdata($tmp);
        }
      }
    }
    
    if($data) { return $data; }
    
    // read logged in data from database for this session.
    $db   = $this->GetDb();
    $sql  = 'SELECT userid AS uid,lastused AS time FROM ' . CMS_DB_PREFIX . 'module_mams_loggedin WHERE sessionid = ?';
    $data = $db->GetRow($sql, [$sessionid]);
    
    if(is_array($data) && count($data)) { return $data; }
    
    return [];
  }
  
  /**
   * @param array $data
   * @param bool  $force
   *
   * @return void
   */
  private function _set_saved_logindetails(array $data, bool $force = FALSE)
  {
    if(!isset($data['uid']) || !isset($data['time']))
    {
      throw new \LogicException('Invalid data passed to ' . __METHOD__);
    }
    
    if(!isset($data['sessionid']))
    {
      $data['sessionid'] = session_id();
    }
    
    //$uid  = (int)$data['uid'];
    $time = (int)$data['time'];
    
    static $_done = FALSE;
    
    if($_done) { return; }
  
    $_done          = TRUE;
    $key            = $this->keepalive_cookie();
    $encoded        = $this->encode_userdata($data);
    $_SESSION[$key] = $encoded;
    
    $module = $this->GetModule();
    
    if($module->GetPreference('cookie_keepalive', 0))
    {
      // this, if enabled will essentially keep the user logged in, IF he comes in once every, say, 24hrs
      \cms_cookies::set($key, $encoded, time() + 24 * 3600); // expiry to 24 hours.
    }
    
    // note: we never touch the permacookie.
    
    // here, we touch the user login data... but infrequently.   must be more frequent than the expireusers interval though.
    if(isset($_SESSION[__METHOD__]))
    {
      $time = max($_SESSION[__METHOD__], $time);
    }
    
    $expire_interval = (int)$module->GetPreference('expireusers_interval', 180) - 1;
    
    if($force || $time < time() - max(0, $expire_interval))
    {
      // touch the loggedin table.
      $db    = $this->GetDb();
      $query = 'UPDATE ' . CMS_DB_PREFIX . 'module_mams_loggedin SET lastused = ? WHERE userid = ? AND sessionid = ?';
      $dbr   = $db->Execute($query, [$data['time'], $data['uid'], $data['sessionid']]);
      
      if( !$dbr || 0 === $db->Affected_Rows() )
      {
        $query = 'INSERT INTO ' . CMS_DB_PREFIX . 'module_mams_loggedin (sessionid,lastused,userid) VALUES (?,?,?)';
        $dbr   = $db->Execute($query, [$data['sessionid'], $data['time'], $data['uid']]);
      }
      
      $_SESSION[__METHOD__] = time();
    }
    
    return;
  }
  
  /**
   * @param      $uid
   * @param bool $session_only
   */
  private function _clear_logindetails($uid, $session_only = FALSE)
  {
    // we nuke some cookies, and nuke some session variables.
    // and delete the loggedin entry from the database
    $key       = $this->keepalive_cookie();
    $sessionid = session_id();
    
    if(!$sessionid) { return; }
    
    foreach($_SESSION as $k => $v)
    {
      if(startswith($k, $key)) { unset($_SESSION[$k]); }
    }
    
    if(TRUE === (bool)$session_only) { return; }
    
    \cms_cookies::erase($key);
    $mams = $this->GetModule();
    
    if('' !== ($cn = $mams->GetPreference('cookiename')))
    {
      \cms_cookies::erase($cn);
    }
    
    $query = 'DELETE FROM ' . CMS_DB_PREFIX . 'module_mams_loggedin WHERE userid = ?';
    $db    = $this->GetDb();
    $db->Execute($query, [$uid]);
  }
  
} // class
?>