diff --git a/README.md b/README.md
index 1ec67ec..832cee6 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,10 @@
user_sql
========
+Updates in order to support password verification with Drupal 7 database:
+- added crypt_type 'drupal' for Drupal 7 in templates/admin.php after line 72
+- added processing of crypt_type 'drupal' in lib/user_sql.php after lines 311 and 407
+- added file drupal.php to directory lib
+WD 2018-01-04 reapplied to to new app version on 2018-02-10
**Nextcloud SQL user authentication.**
diff --git a/admin.php b/admin.php
new file mode 100644
index 0000000..5255c13
--- /dev/null
+++ b/admin.php
@@ -0,0 +1,183 @@
+
+
+
+
t('SQL User Backend')); ?>
+
+
+
diff --git a/drupal.php b/drupal.php
new file mode 100644
index 0000000..4c7e0a4
--- /dev/null
+++ b/drupal.php
@@ -0,0 +1,321 @@
+> 6) & 0x3f];
+ if ($i++ >= $count) {
+ break;
+ }
+ if ($i < $count) {
+ $value |= ord($input[$i]) << 16;
+ }
+ $output .= $itoa64[($value >> 12) & 0x3f];
+ if ($i++ >= $count) {
+ break;
+ }
+ $output .= $itoa64[($value >> 18) & 0x3f];
+ } while ($i < $count);
+
+ return $output;
+}
+/**
+ * Returns a string of highly randomized bytes (over the full 8-bit range).
+ *
+ * This function is better than simply calling mt_rand() or any other built-in
+ * PHP function because it can return a long string of bytes (compared to < 4
+ * bytes normally from mt_rand()) and uses the best available pseudo-random
+ * source.
+ *
+ * @param $count
+ * The number of characters (bytes) to return in the string.
+ */
+
+ function _random_bytes($count) {
+ // $random_state does not use static as it stores random bytes.
+ static $random_state, $bytes, $has_openssl;
+
+ $missing_bytes = $count - strlen($bytes);
+
+ if ($missing_bytes > 0) {
+ // PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes()
+ // locking on Windows and rendered it unusable.
+ if (!isset($has_openssl)) {
+ $has_openssl = version_compare(PHP_VERSION, '5.3.4', '>=') && function_exists('openssl_random_pseudo_bytes');
+ }
+
+ // openssl_random_pseudo_bytes() will find entropy in a system-dependent
+ // way.
+ if ($has_openssl) {
+ $bytes .= openssl_random_pseudo_bytes($missing_bytes);
+ }
+
+ // Else, read directly from /dev/urandom, which is available on many *nix
+ // systems and is considered cryptographically secure.
+ elseif ($fh = @fopen('/dev/urandom', 'rb')) {
+ // PHP only performs buffered reads, so in reality it will always read
+ // at least 4096 bytes. Thus, it costs nothing extra to read and store
+ // that much so as to speed any additional invocations.
+ $bytes .= fread($fh, max(4096, $missing_bytes));
+ fclose($fh);
+ }
+
+ // If we couldn't get enough entropy, this simple hash-based PRNG will
+ // generate a good set of pseudo-random bytes on any system.
+ // Note that it may be important that our $random_state is passed
+ // through hash() prior to being rolled into $output, that the two hash()
+ // invocations are different, and that the extra input into the first one -
+ // the microtime() - is prepended rather than appended. This is to avoid
+ // directly leaking $random_state via the $output stream, which could
+ // allow for trivial prediction of further "random" numbers.
+ if (strlen($bytes) < $count) {
+ // Initialize on the first call. The contents of $_SERVER includes a mix of
+ // user-specific and system information that varies a little with each page.
+ if (!isset($random_state)) {
+ $random_state = print_r($_SERVER, TRUE);
+ if (function_exists('getmypid')) {
+ // Further initialize with the somewhat random PHP process ID.
+ $random_state .= getmypid();
+ }
+ $bytes = '';
+ }
+
+ do {
+ $random_state = hash('sha256', microtime() . mt_rand() . $random_state);
+ $bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
+ }
+ while (strlen($bytes) < $count);
+ }
+ }
+ $output = substr($bytes, 0, $count);
+ $bytes = substr($bytes, $count);
+ return $output;
+}
+
+/**
+ * Generates a random base 64-encoded salt prefixed with settings for the hash.
+ *
+ * Proper use of salts may defeat a number of attacks, including:
+ * - The ability to try candidate passwords against multiple hashes at once.
+ * - The ability to use pre-hashed lists of candidate passwords.
+ * - The ability to determine whether two users have the same (or different)
+ * password without actually having to guess one of the passwords.
+ *
+ * @param $count_log2
+ * Integer that determines the number of iterations used in the hashing
+ * process. A larger value is more secure, but takes more time to complete.
+ *
+ * @return
+ * A 12 character string containing the iteration count and a random salt.
+ */
+function _password_generate_salt($count_log2) {
+ $output = '$S$';
+ // Ensure that $count_log2 is within set bounds.
+ $count_log2 = _password_enforce_log2_boundaries($count_log2);
+ // We encode the final log2 iteration count in base 64.
+ $itoa64 = _password_itoa64();
+ $output .= $itoa64[$count_log2];
+ // 6 bytes is the standard salt for a portable phpass hash.
+ $output .= _password_base64_encode(_random_bytes(6), 6);
+ return $output;
+}
+
+/**
+ * Ensures that $count_log2 is within set bounds.
+ *
+ * @param $count_log2
+ * Integer that determines the number of iterations used in the hashing
+ * process. A larger value is more secure, but takes more time to complete.
+ *
+ * @return
+ * Integer within set bounds that is closest to $count_log2.
+ */
+function _password_enforce_log2_boundaries($count_log2) {
+ if ($count_log2 < MIN_HASH_COUNT) {
+ return MIN_HASH_COUNT;
+ }
+ elseif ($count_log2 > MAX_HASH_COUNT) {
+ return MAX_HASH_COUNT;
+ }
+
+ return (int) $count_log2;
+}
+
+/**
+ * Hash a password using a secure stretched hash.
+ *
+ * By using a salt and repeated hashing the password is "stretched". Its
+ * security is increased because it becomes much more computationally costly
+ * for an attacker to try to break the hash by brute-force computation of the
+ * hashes of a large number of plain-text words or strings to find a match.
+ *
+ * @param $algo
+ * The string name of a hashing algorithm usable by hash(), like 'sha256'.
+ * @param $password
+ * Plain-text password up to 512 bytes (128 to 512 UTF-8 characters) to hash.
+ * @param $setting
+ * An existing hash or the output of _password_generate_salt(). Must be
+ * at least 12 characters (the settings and salt).
+ *
+ * @return
+ * A string containing the hashed password (and salt) or FALSE on failure.
+ * The return string will be truncated at DRUPAL_HASH_LENGTH characters max.
+ */
+function _password_crypt($algo, $password, $setting) {
+ // Prevent DoS attacks by refusing to hash large passwords.
+ if (strlen($password) > 512) {
+ return FALSE;
+ }
+ // The first 12 characters of an existing hash are its setting string.
+ $setting = substr($setting, 0, 12);
+
+ if ($setting[0] != '$' || $setting[2] != '$') {
+ return FALSE;
+ }
+ $count_log2 = _password_get_count_log2($setting);
+ // Hashes may be imported from elsewhere, so we allow != DRUPAL_HASH_COUNT
+ if ($count_log2 < MIN_HASH_COUNT || $count_log2 > MAX_HASH_COUNT) {
+ return FALSE;
+ }
+ $salt = substr($setting, 4, 8);
+ // Hashes must have an 8 character salt.
+ if (strlen($salt) != 8) {
+ return FALSE;
+ }
+
+ // Convert the base 2 logarithm into an integer.
+ $count = 1 << $count_log2;
+
+ // We rely on the hash() function being available in PHP 5.2+.
+ $hash = hash($algo, $salt . $password, TRUE);
+ do {
+ $hash = hash($algo, $hash . $password, TRUE);
+ } while (--$count);
+
+ $len = strlen($hash);
+ $output = $setting . _password_base64_encode($hash, $len);
+ // _password_base64_encode() of a 16 byte MD5 will always be 22 characters.
+ // _password_base64_encode() of a 64 byte sha512 will always be 86 characters.
+ $expected = 12 + ceil((8 * $len) / 6);
+ return (strlen($output) == $expected) ? substr($output, 0, HASH_LENGTH) : FALSE;
+}
+
+/**
+ * Parse the log2 iteration count from a stored hash or setting string.
+ */
+function _password_get_count_log2($setting) {
+ $itoa64 = _password_itoa64();
+ return strpos($itoa64, $setting[3]);
+}
+
+/**
+ * Hash a password using a secure hash.
+ *
+ * @param $password
+ * A plain-text password.
+ * @param $count_log2
+ * Optional integer to specify the iteration count. Generally used only during
+ * mass operations where a value less than the default is needed for speed.
+ *
+ * @return
+ * A string containing the hashed password (and a salt), or FALSE on failure.
+ */
+function user_hash_password($password, $count_log2 = 0) {
+ if (empty($count_log2)) {
+ // Use the standard iteration count.
+ $count_log2 = variable_get('password_count_log2', DRUPAL_HASH_COUNT);
+ }
+ return _password_crypt('sha512', $password, _password_generate_salt($count_log2));
+}
+
+/**
+ * Check whether a plain text password matches a stored hashed password.
+ *
+ * @param $password
+ * A plain-text password
+ * @param $hashpass
+ *
+ * @return
+ * TRUE or FALSE.
+ */
+function user_check_password($password, $hashpass) {
+ $stored_hash = $hashpass;
+ $type = substr($stored_hash, 0, 3);
+ switch ($type) {
+ case '$S$':
+ // A normal Drupal 7 password using sha512.
+ $hash = _password_crypt('sha512', $password, $stored_hash);
+ break;
+ case '$H$':
+ // phpBB3 uses "$H$" for the same thing as "$P$".
+ case '$P$':
+ // A phpass password generated using md5. This is an
+ // imported password or from an earlier Drupal version.
+ $hash = _password_crypt('md5', $password, $stored_hash);
+ break;
+ default:
+ return FALSE;
+ }
+ return ($hash && $stored_hash == $hash);
+}
\ No newline at end of file
diff --git a/user_sql.php b/user_sql.php
new file mode 100644
index 0000000..9050101
--- /dev/null
+++ b/user_sql.php
@@ -0,0 +1,993 @@
+
+ *
+ * credits go to Ed W for several SQL injection fixes and caching support
+ * credits go to Frédéric France for providing Joomla support
+ * credits go to Mark Jansenn for providing Joomla 2.5.18+ / 3.2.1+ support
+ * credits go to Dominik Grothaus for providing SSHA256 support and fixing a few bugs
+ * credits go to Sören Eberhardt-Biermann for providing multi-host support
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library. If not, see .
+ *
+ */
+
+namespace OCA\user_sql;
+use OC\User\Backend;
+
+use \OCA\user_sql\lib\Helper;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Notification\IManager as INotificationManager;
+use OCP\Util;
+
+abstract class BackendUtility {
+ protected $access;
+
+ /**
+ * constructor, make sure the subclasses call this one!
+ * @param Access $access an instance of Access for LDAP interaction
+ */
+ public function __construct(Access $access) {
+ $this->access = $access;
+ }
+}
+
+
+class OC_USER_SQL extends BackendUtility implements \OCP\IUserBackend,
+ \OCP\UserInterface
+{
+ protected $cache;
+ protected $settings;
+ protected $helper;
+ protected $session_cache_name;
+ protected $ocConfig;
+
+ /**
+ * The default constructor. It loads the settings for the given domain
+ * and tries to connect to the database.
+ */
+ public function __construct()
+ {
+ $memcache = \OC::$server->getMemCacheFactory();
+ if ( $memcache -> isAvailable())
+ {
+ $this -> cache = $memcache -> create();
+ }
+ $this -> helper = new \OCA\user_sql\lib\Helper();
+ $domain = \OC::$server->getRequest()->getServerHost();
+ $this -> settings = $this -> helper -> loadSettingsForDomain($domain);
+ $this -> ocConfig = \OC::$server->getConfig();
+ $this -> helper -> connectToDb($this -> settings);
+ $this -> session_cache_name = 'USER_SQL_CACHE';
+ return false;
+ }
+
+ /**
+ * Sync the user's E-Mail address with the address stored by ownCloud.
+ * We have three (four) sync modes:
+ * - none: Does nothing
+ * - initial: Do the sync only once from SQL -> ownCloud
+ * - forcesql: The SQL database always wins and sync to ownCloud
+ * - forceoc: ownCloud always wins and syncs to SQL
+ *
+ * @param string $uid The user's ID to sync
+ * @return bool Success or Fail
+ */
+ private function doEmailSync($uid)
+ {
+ Util::writeLog('OC_USER_SQL', "Entering doEmailSync for UID: $uid",
+ Util::DEBUG);
+ if($this -> settings['col_email'] === '')
+ return false;
+
+ if($this -> settings['set_mail_sync_mode'] === 'none')
+ return false;
+
+ $ocUid = $uid;
+ $uid = $this -> doUserDomainMapping($uid);
+
+ $row = $this -> helper -> runQuery('getMail', array('uid' => $uid));
+ if($row === false)
+ {
+ return false;
+ }
+ $newMail = $row[$this -> settings['col_email']];
+
+ $currMail = $this->ocConfig->getUserValue( $ocUid,
+ 'settings',
+ 'email', '');
+
+ switch($this -> settings['set_mail_sync_mode'])
+ {
+ case 'initial':
+ if($currMail === '')
+ $this->ocConfig->setUserValue( $ocUid,
+ 'settings',
+ 'email',
+ $newMail);
+ break;
+ case 'forcesql':
+ //if($currMail !== $newMail)
+ $this->ocConfig->setUserValue( $ocUid,
+ 'settings',
+ 'email',
+ $newMail);
+ break;
+ case 'forceoc':
+ if(($currMail !== '') && ($currMail !== $newMail))
+ {
+ $row = $this -> helper -> runQuery('setMail',
+ array('uid' => $uid,
+ 'currMail' => $currMail)
+ , true);
+
+ if($row === false)
+ {
+ Util::writeLog('OC_USER_SQL',
+ "Could not update E-Mail address in SQL database!",
+ Util::ERROR);
+ }
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * This maps the username to the specified domain name.
+ * It can only append a default domain name.
+ *
+ * @param string $uid The UID to work with
+ * @return string The mapped UID
+ */
+ private function doUserDomainMapping($uid)
+ {
+ $uid = trim($uid);
+
+ if($this -> settings['set_default_domain'] !== '')
+ {
+ Util::writeLog('OC_USER_SQL', "Append default domain: ".
+ $this -> settings['set_default_domain'], Util::DEBUG);
+ if(strpos($uid, '@') === false)
+ {
+ $uid .= "@" . $this -> settings['set_default_domain'];
+ }
+ }
+
+ $uid = strtolower($uid);
+ Util::writeLog('OC_USER_SQL', 'Returning mapped UID: ' . $uid,
+ Util::DEBUG);
+ return $uid;
+ }
+
+ /**
+ * Return the actions implemented by this backend
+ * @param $actions
+ * @return bool
+ */
+ public function implementsActions($actions)
+ {
+ return (bool)((Backend::CHECK_PASSWORD
+ | Backend::GET_DISPLAYNAME
+ | Backend::COUNT_USERS
+ | ($this -> settings['set_allow_pwchange'] === 'true' ?
+ Backend::SET_PASSWORD : 0)
+ | ($this -> settings['set_enable_gethome'] === 'true' ?
+ Backend::GET_HOME : 0)
+ ) & $actions);
+ }
+
+ /**
+ * Checks if this backend has user listing support
+ * @return bool
+ */
+ public function hasUserListings()
+ {
+ return true;
+ }
+
+ /**
+ * Return the user's home directory, if enabled
+ * @param string $uid The user's ID to retrieve
+ * @return mixed The user's home directory or false
+ */
+ public function getHome($uid)
+ {
+ Util::writeLog('OC_USER_SQL', "Entering getHome for UID: $uid",
+ Util::DEBUG);
+
+ if($this -> settings['set_enable_gethome'] !== 'true')
+ return false;
+
+ $uidMapped = $this -> doUserDomainMapping($uid);
+ $home = false;
+
+ switch($this->settings['set_gethome_mode'])
+ {
+ case 'query':
+ Util::writeLog('OC_USER_SQL',
+ "getHome with Query selected, running Query...",
+ Util::DEBUG);
+ $row = $this -> helper -> runQuery('getHome',
+ array('uid' => $uidMapped));
+ if($row === false)
+ {
+ Util::writeLog('OC_USER_SQL',
+ "Got no row, return false",
+ Util::DEBUG);
+ return false;
+ }
+ $home = $row[$this -> settings['col_gethome']];
+ break;
+
+ case 'static':
+ Util::writeLog('OC_USER_SQL',
+ "getHome with static selected",
+ Util::DEBUG);
+ $home = $this -> settings['set_gethome'];
+ $home = str_replace('%ud', $uidMapped, $home);
+ $home = str_replace('%u', $uid, $home);
+ $home = str_replace('%d',
+ $this -> settings['set_default_domain'],
+ $home);
+ break;
+ }
+ Util::writeLog('OC_USER_SQL',
+ "Returning getHome for UID: $uid with Home $home",
+ Util::DEBUG);
+ return $home;
+ }
+
+ /**
+ * Create a new user account using this backend
+ * @return bool always false, as we can't create users
+ */
+ public function createUser()
+ {
+ // Can't create user
+ Util::writeLog('OC_USER_SQL',
+ 'Not possible to create local users from web'.
+ ' frontend using SQL user backend', Util::ERROR);
+ return false;
+ }
+
+ /**
+ * Delete a user account using this backend
+ * @param string $uid The user's ID to delete
+ * @return bool always false, as we can't delete users
+ */
+ public function deleteUser($uid)
+ {
+ // Can't delete user
+ Util::writeLog('OC_USER_SQL', 'Not possible to delete local users'.
+ ' from web frontend using SQL user backend', Util::ERROR);
+ return false;
+ }
+
+ /**
+ * Set (change) a user password
+ * This can be enabled/disabled in the settings (set_allow_pwchange)
+ *
+ * @param string $uid The user ID
+ * @param string $password The user's new password
+ * @return bool The return status
+ */
+ public function setPassword($uid, $password)
+ {
+ // Update the user's password - this might affect other services, that
+ // use the same database, as well
+ Util::writeLog('OC_USER_SQL', "Entering setPassword for UID: $uid",
+ Util::DEBUG);
+
+ if($this -> settings['set_allow_pwchange'] !== 'true')
+ return false;
+
+ $uid = $this -> doUserDomainMapping($uid);
+
+ $row = $this -> helper -> runQuery('getPass', array('uid' => $uid));
+ if($row === false)
+ {
+ return false;
+ }
+ $old_password = $row[$this -> settings['col_password']];
+
+ // Added and disabled updating passwords for Drupal 7 WD 2018-01-04
+ if($this -> settings['set_crypt_type'] === 'drupal')
+ {
+ return false;
+ }
+ elseif($this -> settings['set_crypt_type'] === 'joomla2')
+ {
+ if(!class_exists('\PasswordHash'))
+ require_once('PasswordHash.php');
+ $hasher = new \PasswordHash(10, true);
+ $enc_password = $hasher -> HashPassword($password);
+ }
+ // Redmine stores the salt separatedly, this doesn't play nice with
+ // the way we check passwords
+ elseif($this -> settings['set_crypt_type'] === 'redmine')
+ {
+ $salt = $this -> helper -> runQuery('getRedmineSalt',
+ array('uid' => $uid));
+ if(!$salt)
+ return false;
+ $enc_password = sha1($salt['salt'].sha1($password));
+
+ }
+ elseif($this -> settings['set_crypt_type'] === 'sha1')
+ {
+ $enc_password = sha1($password);
+ }
+ elseif($this -> settings['set_crypt_type'] === 'system')
+ {
+ $prefix=substr($old_password,0,2);
+ if ($prefix==="$2")
+ {
+ $enc_password = $this->pw_hash($password);
+ }
+ else
+ {
+ if (($prefix==="$1") or ($prefix[0] != "$")) //old md5 or DES
+ {
+ //Update encryption algorithm
+ $prefix="$6"; //change to sha512
+ }
+
+ $newsalt=$this->create_systemsalt();
+ $enc_password=crypt($password,$prefix ."$" . $newsalt);
+ }
+
+ }
+ elseif($this -> settings['set_crypt_type'] === 'password_hash')
+ {
+ $enc_password = $this->pw_hash($password);
+ }
+ else
+ {
+ $enc_password = $this -> pacrypt($password, $old_password);
+ }
+ $res = $this -> helper -> runQuery('setPass',
+ array('uid' => $uid, 'enc_password' => $enc_password),
+ true);
+ if($res === false)
+ {
+ Util::writeLog('OC_USER_SQL', "Could not update password!",
+ Util::ERROR);
+ return false;
+ }
+ Util::writeLog('OC_USER_SQL',
+ "Updated password successfully, return true",
+ Util::DEBUG);
+ return true;
+ }
+
+ /**
+ * Check if the password is correct
+ * @param string $uid The username
+ * @param string $password The password
+ * @return bool true/false
+ *
+ * Check if the password is correct without logging in the user
+ */
+ public function checkPassword($uid, $password)
+ {
+ Util::writeLog('OC_USER_SQL',
+ "Entering checkPassword() for UID: $uid",
+ Util::DEBUG);
+
+ $uid = $this -> doUserDomainMapping($uid);
+
+ $row = $this -> helper -> runQuery('getPass', array('uid' => $uid));
+ if($row === false)
+ {
+ Util::writeLog('OC_USER_SQL', "Got no row, return false", Util::DEBUG);
+ return false;
+ }
+ $db_pass = $row[$this -> settings['col_password']];
+
+ Util::writeLog('OC_USER_SQL', "Encrypting and checking password",
+ Util::DEBUG);
+ // Added handling for Drupal 7 passwords WD 2018-01-04
+ if($this -> settings['set_crypt_type'] === 'drupal')
+ {
+ if(!function_exists('user_check_password'))
+ require_once('drupal.php');
+ $ret = user_check_password($password, $db_pass);
+ }
+ // Joomla 2.5.18 switched to phPass, which doesn't play nice with the
+ // way we check passwords
+ elseif($this -> settings['set_crypt_type'] === 'joomla2')
+ {
+ if(!class_exists('\PasswordHash'))
+ require_once('PasswordHash.php');
+ $hasher = new \PasswordHash(10, true);
+ $ret = $hasher -> CheckPassword($password, $db_pass);
+ }
+ elseif($this -> settings['set_crypt_type'] === 'password_hash')
+ {
+ $ret = password_verify($password,$db_pass);
+ }
+ // Redmine stores the salt separatedly, this doesn't play nice with the
+ // way we check passwords
+ elseif($this -> settings['set_crypt_type'] === 'redmine')
+ {
+ $salt = $this -> helper -> runQuery('getRedmineSalt',
+ array('uid' => $uid));
+ if(!$salt)
+ return false;
+ $ret = sha1($salt['salt'].sha1($password)) === $db_pass;
+ }
+
+ elseif($this -> settings['set_crypt_type'] == 'sha1')
+ {
+ $ret = $this->hash_equals(sha1($password) , $db_pass);
+ } else
+
+ {
+ // $ret = $this -> pacrypt($password, $db_pass) === $db_pass;
+ $ret = $this->hash_equals($this -> pacrypt($password, $db_pass),
+ $db_pass);
+ }
+ if($ret)
+ {
+ Util::writeLog('OC_USER_SQL',
+ "Passwords matching, return true",
+ Util::DEBUG);
+ if($this -> settings['set_strip_domain'] === 'true')
+ {
+ $uid = explode("@", $uid);
+ $uid = $uid[0];
+ }
+ return $uid;
+ } else
+ {
+ Util::writeLog('OC_USER_SQL',
+ "Passwords do not match, return false",
+ Util::DEBUG);
+ return false;
+ }
+ }
+
+ /**
+ * Count the number of users
+ * @return int The user count
+ */
+ public function countUsers()
+ {
+ Util::writeLog('OC_USER_SQL', "Entering countUsers()",
+ Util::DEBUG);
+
+ $search = "%".$this -> doUserDomainMapping("");
+ $userCount = $this -> helper -> runQuery('countUsers',
+ array('search' => $search));
+ if($userCount === false)
+ {
+ $userCount = 0;
+ }
+ else {
+ $userCount = reset($userCount);
+ }
+
+ Util::writeLog('OC_USER_SQL', "Return usercount: ".$userCount,
+ Util::DEBUG);
+ return $userCount;
+ }
+
+ /**
+ * Get a list of all users
+ * @param string $search The search term (can be empty)
+ * @param int $limit The search limit (can be null)
+ * @param int $offset The search offset (can be null)
+ * @return array with all uids
+ */
+ public function getUsers($search = '', $limit = null, $offset = null)
+ {
+ Util::writeLog('OC_USER_SQL',
+ "Entering getUsers() with Search: $search, ".
+ "Limit: $limit, Offset: $offset", Util::DEBUG);
+ $users = array();
+
+ if($search !== '')
+ {
+ $search = "%".$this -> doUserDomainMapping($search."%")."%";
+ }
+ else
+ {
+ $search = "%".$this -> doUserDomainMapping("")."%";
+ }
+
+ $rows = $this -> helper -> runQuery('getUsers',
+ array('search' => $search),
+ false,
+ true,
+ array('limit' => $limit,
+ 'offset' => $offset));
+ if($rows === false)
+ return array();
+
+ foreach($rows as $row)
+ {
+ $uid = $row[$this -> settings['col_username']];
+ if($this -> settings['set_strip_domain'] === 'true')
+ {
+ $uid = explode("@", $uid);
+ $uid = $uid[0];
+ }
+ $users[] = strtolower($uid);
+ }
+ Util::writeLog('OC_USER_SQL', "Return list of results",
+ Util::DEBUG);
+ return $users;
+ }
+
+ /**
+ * Check if a user exists
+ * @param string $uid the username
+ * @return boolean
+ */
+ public function userExists($uid)
+ {
+
+ $cacheKey = 'sql_user_exists_' . $uid;
+ $cacheVal = $this -> getCache ($cacheKey);
+ Util::writeLog('OC_USER_SQL',
+ "userExists() for UID: $uid cacheVal: $cacheVal",
+ Util::DEBUG);
+ if(!is_null($cacheVal))
+ return (bool)$cacheVal;
+
+ Util::writeLog('OC_USER_SQL',
+ "Entering userExists() for UID: $uid",
+ Util::DEBUG);
+
+ // Only if the domain is removed for internal user handling,
+ // we should add the domain back when checking existance
+ if($this -> settings['set_strip_domain'] === 'true')
+ {
+ $uid = $this -> doUserDomainMapping($uid);
+ }
+
+ $exists = (bool)$this -> helper -> runQuery('userExists',
+ array('uid' => $uid));;
+ $this -> setCache ($cacheKey, $exists, 60);
+
+ if(!$exists)
+ {
+ Util::writeLog('OC_USER_SQL',
+ "Empty row, user does not exists, return false",
+ Util::DEBUG);
+ return false;
+ } else
+ {
+ Util::writeLog('OC_USER_SQL', "User exists, return true",
+ Util::DEBUG);
+ return true;
+ }
+
+ }
+
+ /**
+ * Get the display name of the user
+ * @param string $uid The user ID
+ * @return mixed The user's display name or FALSE
+ */
+ public function getDisplayName($uid)
+ {
+ Util::writeLog('OC_USER_SQL',
+ "Entering getDisplayName() for UID: $uid",
+ Util::DEBUG);
+
+ $this -> doEmailSync($uid);
+ $uid = $this -> doUserDomainMapping($uid);
+
+ if(!$this -> userExists($uid))
+ {
+ return false;
+ }
+
+ $row = $this -> helper -> runQuery('getDisplayName',
+ array('uid' => $uid));
+
+ if(!$row)
+ {
+ Util::writeLog('OC_USER_SQL',
+ "Empty row, user has no display name or ".
+ "does not exist, return false",
+ Util::DEBUG);
+ return false;
+ } else
+ {
+ Util::writeLog('OC_USER_SQL',
+ "User exists, return true",
+ Util::DEBUG);
+ $displayName = $row[$this -> settings['col_displayname']];
+ return $displayName; ;
+ }
+ return false;
+ }
+
+ public function getDisplayNames($search = '', $limit = null, $offset = null)
+ {
+ $uids = $this -> getUsers($search, $limit, $offset);
+ $displayNames = array();
+ foreach($uids as $uid)
+ {
+ $displayNames[$uid] = $this -> getDisplayName($uid);
+ }
+ return $displayNames;
+ }
+
+ /**
+ * Returns the backend name
+ * @return string
+ */
+ public function getBackendName()
+ {
+ return 'SQL';
+ }
+
+ /**
+ * The following functions were directly taken from PostfixAdmin and just
+ * slightly modified
+ * to suit our needs.
+ * Encrypt a password,using the apparopriate hashing mechanism as defined in
+ * config.inc.php ($this->crypt_type).
+ * When wanting to compare one pw to another, it's necessary to provide the
+ * salt used - hence
+ * the second parameter ($pw_db), which is the existing hash from the DB.
+ *
+ * @param string $pw cleartext password
+ * @param string $pw_db encrypted password from database
+ * @return string encrypted password.
+ */
+ private function pacrypt($pw, $pw_db = "")
+ {
+ Util::writeLog('OC_USER_SQL', "Entering private pacrypt()",
+ Util::DEBUG);
+ $pw = stripslashes($pw);
+ $password = "";
+ $salt = "";
+
+ if($this -> settings['set_crypt_type'] === 'md5crypt')
+ {
+ $split_salt = preg_split('/\$/', $pw_db);
+ if(isset($split_salt[2]))
+ {
+ $salt = $split_salt[2];
+ }
+ $password = $this -> md5crypt($pw, $salt);
+ } elseif($this -> settings['set_crypt_type'] === 'md5')
+ {
+ $password = md5($pw);
+ } elseif($this -> settings['set_crypt_type'] === 'system')
+ {
+ // We never generate salts, as user creation is not allowed here
+ $password = crypt($pw, $pw_db);
+ } elseif($this -> settings['set_crypt_type'] === 'cleartext')
+ {
+ $password = $pw;
+ }
+
+// See
+// https://sourceforge.net/tracker/?func=detail&atid=937966&aid=1793352&group_id=191583
+// this is apparently useful for pam_mysql etc.
+ elseif($this -> settings['set_crypt_type'] === 'mysql_encrypt')
+ {
+ if($pw_db !== "")
+ {
+ $salt = substr($pw_db, 0, 2);
+
+ $row = $this -> helper -> runQuery('mysqlEncryptSalt',
+ array('pw' => $pw, 'salt' => $salt));
+ } else
+ {
+ $row = $this -> helper -> runQuery('mysqlEncrypt',
+ array('pw' => $pw));
+ }
+
+ if($row === false)
+ {
+ return false;
+ }
+ $password = $row[0];
+ } elseif($this -> settings['set_crypt_type'] === 'mysql_password')
+ {
+ $row = $this -> helper -> runQuery('mysqlPassword',
+ array('pw' => $pw));
+
+ if($row === false)
+ {
+ return false;
+ }
+ $password = $row[0];
+ }
+
+ // The following is by Frédéric France
+ elseif($this -> settings['set_crypt_type'] === 'joomla')
+ {
+ $split_salt = preg_split('/:/', $pw_db);
+ if(isset($split_salt[1]))
+ {
+ $salt = $split_salt[1];
+ }
+ $password = ($salt) ? md5($pw . $salt) : md5($pw);
+ $password .= ':' . $salt;
+ }
+
+ elseif($this-> settings['set_crypt_type'] === 'ssha256')
+ {
+ $salted_password = base64_decode(
+ preg_replace('/{SSHA256}/i','',$pw_db));
+ $salt = substr($salted_password,-(strlen($salted_password)-32));
+ $password = $this->ssha256($pw,$salt);
+ } else
+ {
+ Util::writeLog('OC_USER_SQL',
+ "unknown/invalid crypt_type settings: ".
+ $this->settings['set_crypt_type'],
+ Util::ERROR);
+ die('unknown/invalid Encryption type setting: ' .
+ $this -> settings['set_crypt_type']);
+ }
+ Util::writeLog('OC_USER_SQL', "pacrypt() done, return",
+ Util::DEBUG);
+ return $password;
+ }
+
+ /**
+ * md5crypt
+ * Creates MD5 encrypted password
+ * @param string $pw The password to encrypt
+ * @param string $salt The salt to use
+ * @param string $magic ?
+ * @return string The encrypted password
+ */
+
+ private function md5crypt($pw, $salt = "", $magic = "")
+ {
+ $MAGIC = "$1$";
+
+ if($magic === "")
+ $magic = $MAGIC;
+ if($salt === "")
+ $salt = $this -> create_md5salt();
+ $slist = explode("$", $salt);
+ if($slist[0] === "1")
+ $salt = $slist[1];
+
+ $salt = substr($salt, 0, 8);
+ $ctx = $pw . $magic . $salt;
+ $final = $this -> pahex2bin(md5($pw . $salt . $pw));
+
+ for($i = strlen($pw); $i > 0; $i -= 16)
+ {
+ if($i > 16)
+ {
+ $ctx .= substr($final, 0, 16);
+ } else
+ {
+ $ctx .= substr($final, 0, $i);
+ }
+ }
+ $i = strlen($pw);
+
+ while($i > 0)
+ {
+ if($i & 1)
+ $ctx .= chr(0);
+ else
+ $ctx .= $pw[0];
+ $i = $i>>1;
+ }
+ $final = $this -> pahex2bin(md5($ctx));
+
+ for($i = 0; $i < 1000; $i++)
+ {
+ $ctx1 = "";
+ if($i & 1)
+ {
+ $ctx1 .= $pw;
+ } else
+ {
+ $ctx1 .= substr($final, 0, 16);
+ }
+ if($i % 3)
+ $ctx1 .= $salt;
+ if($i % 7)
+ $ctx1 .= $pw;
+ if($i & 1)
+ {
+ $ctx1 .= substr($final, 0, 16);
+ } else
+ {
+ $ctx1 .= $pw;
+ }
+ $final = $this -> pahex2bin(md5($ctx1));
+ }
+ $passwd = "";
+ $passwd .= $this -> to64(((ord($final[0])<<16) |
+ (ord($final[6])<<8) | (ord($final[12]))), 4);
+ $passwd .= $this -> to64(((ord($final[1])<<16) |
+ (ord($final[7])<<8) | (ord($final[13]))), 4);
+ $passwd .= $this -> to64(((ord($final[2])<<16) |
+ (ord($final[8])<<8) | (ord($final[14]))), 4);
+ $passwd .= $this -> to64(((ord($final[3])<<16) |
+ (ord($final[9])<<8) | (ord($final[15]))), 4);
+ $passwd .= $this -> to64(((ord($final[4])<<16) |
+ (ord($final[10])<<8) | (ord($final[5]))), 4);
+ $passwd .= $this -> to64(ord($final[11]), 2);
+ return "$magic$salt\$$passwd";
+ }
+
+ /**
+ * Create a new salte
+ * @return string The salt
+ */
+ private function create_md5salt()
+ {
+ srand((double) microtime() * 1000000);
+ $salt = substr(md5(rand(0, 9999999)), 0, 8);
+ return $salt;
+ }
+
+ /**
+ * Encrypt using SSHA256 algorithm
+ * @param string $pw The password
+ * @param string $salt The salt to use
+ * @return string The hashed password, prefixed by {SSHA256}
+ */
+ private function ssha256($pw, $salt)
+ {
+ return '{SSHA256}'.base64_encode(hash('sha256',$pw.$salt,true).$salt);
+ }
+
+ /**
+ * PostfixAdmin's hex2bin function
+ * @param string $str The string to convert
+ * @return string The converted string
+ */
+ private function pahex2bin($str)
+ {
+ if(function_exists('hex2bin'))
+ {
+ return hex2bin($str);
+ } else
+ {
+ $len = strlen($str);
+ $nstr = "";
+ for($i = 0; $i < $len; $i += 2)
+ {
+ $num = sscanf(substr($str, $i, 2), "%x");
+ $nstr .= chr($num[0]);
+ }
+ return $nstr;
+ }
+ }
+
+ /**
+ * Convert to 64?
+ */
+ private function to64($v, $n)
+ {
+ $ITOA64 =
+ "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ $ret = "";
+ while(($n - 1) >= 0)
+ {
+ $n--;
+ $ret .= $ITOA64[$v & 0x3f];
+ $v = $v>>6;
+ }
+ return $ret;
+ }
+
+ /**
+ * Store a value in memcache or the session, if no memcache is available
+ * @param string $key The key
+ * @param mixed $value The value to store
+ * @param int $ttl (optional) defaults to 3600 seconds.
+ */
+ private function setCache($key, $value, $ttl=3600)
+ {
+ if ($this -> cache === NULL)
+ {
+ $_SESSION[$this -> session_cache_name][$key] = array(
+ 'value' => $value,
+ 'time' => time(),
+ 'ttl' => $ttl,
+ );
+ } else
+ {
+ $this -> cache -> set($key,$value,$ttl);
+ }
+ }
+
+ /**
+ * Fetch a value from memcache or session, if memcache is not available.
+ * Returns NULL if there's no value stored or the value expired.
+ * @param string $key
+ * @return mixed|NULL
+ */
+ private function getCache($key)
+ {
+ $retVal = NULL;
+ if ($this -> cache === NULL)
+ {
+ if (isset($_SESSION[$this -> session_cache_name],
+ $_SESSION[$this -> session_cache_name][$key]))
+ {
+ $value = $_SESSION[$this -> session_cache_name][$key];
+ if (time() < $value['time'] + $value['ttl'])
+ {
+ $retVal = $value['value'];
+ }
+ }
+ } else
+ {
+ $retVal = $this -> cache -> get ($key);
+ }
+ return $retVal;
+ }
+
+ private function create_systemsalt($length=20)
+ {
+ $fp = fopen('/dev/urandom', 'r');
+ $randomString = fread($fp, $length);
+ fclose($fp);
+ $salt = base64_encode($randomString);
+ return $salt;
+ }
+
+ private function pw_hash($password)
+ {
+ $options = [
+ 'cost' => 10,
+ ];
+ return password_hash($password, PASSWORD_BCRYPT, $options);
+
+ }
+
+ function hash_equals( $a, $b ) {
+ $a_length = strlen( $a );
+
+ if ( $a_length !== strlen( $b ) ) {
+ return false;
+ }
+ $result = 0;
+
+ // Do not attempt to "optimize" this.
+ for ( $i = 0; $i < $a_length; $i++ ) {
+ $result |= ord( $a[ $i ] ) ^ ord( $b[ $i ] );
+ }
+
+ //Hide the length of the string
+ $additional_length=200-($a_length % 200);
+ $tmp=0;
+ $c="abCD";
+ for ( $i = 0; $i < $additional_length; $i++ ) {
+ $tmp |= ord( $c[ 0 ] ) ^ ord( $c[ 0 ] );
+ }
+
+ return $result === 0;
+}
+
+}
+