diff options
-rw-r--r-- | .hg_archival.txt | 4 | ||||
-rw-r--r-- | .hgignore | 2 | ||||
-rw-r--r-- | .hgtags | 14 | ||||
-rw-r--r-- | PasswordHash.php | 253 | ||||
-rw-r--r-- | README.md | 19 | ||||
-rw-r--r-- | ajax/settings.php | 260 | ||||
-rw-r--r-- | appinfo/app.php | 31 | ||||
-rw-r--r-- | appinfo/info.xml | 20 | ||||
-rw-r--r-- | appinfo/routes.php | 8 | ||||
-rw-r--r-- | appinfo/update.php | 71 | ||||
-rw-r--r-- | css/settings.css | 24 | ||||
-rw-r--r-- | js/settings.js | 314 | ||||
-rw-r--r-- | lib/helper.php | 348 | ||||
-rw-r--r-- | settings.php | 52 | ||||
-rw-r--r-- | templates/settings.php | 168 | ||||
-rw-r--r-- | user_sql.php | 782 |
16 files changed, 2370 insertions, 0 deletions
diff --git a/.hg_archival.txt b/.hg_archival.txt new file mode 100644 index 0000000..8255758 --- /dev/null +++ b/.hg_archival.txt @@ -0,0 +1,4 @@ +repo: 3f045a88dd7014290a9bf60cbd740c8548dba967 +node: 77b8809852f4a8599200c9a538f94319ca5a4ff4 +branch: default +tag: v.2.3.1 diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..4f0682f --- /dev/null +++ b/.hgignore @@ -0,0 +1,2 @@ +.project +.settings @@ -0,0 +1,14 @@ +8401b998f40be1e93d05b5c4db61ba98932c3de3 v0.2 +c74b035788ebbcade6bfae22963632f4c5f49540 v0.3 +5ee6481afb63f32dcfeaf93a72c07c4bc2a1a993 v0.4 +5ee6481afb63f32dcfeaf93a72c07c4bc2a1a993 v0.4 +f9eb8e6e4b4f2bcb521554bc513cc94d9ccf6fbd v0.4 +df3dc7358b79abdd7075ba78b58fff1da7fa3fde to +df3dc7358b79abdd7075ba78b58fff1da7fa3fde 0.5 +df3dc7358b79abdd7075ba78b58fff1da7fa3fde to +0000000000000000000000000000000000000000 to +df3dc7358b79abdd7075ba78b58fff1da7fa3fde 0.5 +0000000000000000000000000000000000000000 0.5 +845b75d72ec18f3fbf2a74f1a1ec3cf54ca4a304 v0.7 +599b774a4b6ac81b44971ac1a319fc41c04e3589 v2.3 +1fedd27cb5d8c69a998c8278463c8e8abd1f371a v2.3.0 diff --git a/PasswordHash.php b/PasswordHash.php new file mode 100644 index 0000000..474b2e3 --- /dev/null +++ b/PasswordHash.php @@ -0,0 +1,253 @@ +<?php +# +# Portable PHP password hashing framework. +# +# Version 0.3 / genuine. +# +# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in +# the public domain. Revised in subsequent years, still public domain. +# +# There's absolutely no warranty. +# +# The homepage URL for this framework is: +# +# http://www.openwall.com/phpass/ +# +# Please be sure to update the Version line if you edit this file in any way. +# It is suggested that you leave the main version number intact, but indicate +# your project name (after the slash) and add your own revision information. +# +# Please do not change the "private" password hashing method implemented in +# here, thereby making your hashes incompatible. However, if you must, please +# change the hash type identifier (the "$P$") to something different. +# +# Obviously, since this code is in the public domain, the above are not +# requirements (there can be none), but merely suggestions. +# +class PasswordHash { + var $itoa64; + var $iteration_count_log2; + var $portable_hashes; + var $random_state; + + function PasswordHash($iteration_count_log2, $portable_hashes) + { + $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) + $iteration_count_log2 = 8; + $this->iteration_count_log2 = $iteration_count_log2; + + $this->portable_hashes = $portable_hashes; + + $this->random_state = microtime(); + if (function_exists('getmypid')) + $this->random_state .= getmypid(); + } + + function get_random_bytes($count) + { + $output = ''; + if (is_readable('/dev/urandom') && + ($fh = @fopen('/dev/urandom', 'rb'))) { + $output = fread($fh, $count); + fclose($fh); + } + + if (strlen($output) < $count) { + $output = ''; + for ($i = 0; $i < $count; $i += 16) { + $this->random_state = + md5(microtime() . $this->random_state); + $output .= + pack('H*', md5($this->random_state)); + } + $output = substr($output, 0, $count); + } + + return $output; + } + + function encode64($input, $count) + { + $output = ''; + $i = 0; + do { + $value = ord($input[$i++]); + $output .= $this->itoa64[$value & 0x3f]; + if ($i < $count) + $value |= ord($input[$i]) << 8; + $output .= $this->itoa64[($value >> 6) & 0x3f]; + if ($i++ >= $count) + break; + if ($i < $count) + $value |= ord($input[$i]) << 16; + $output .= $this->itoa64[($value >> 12) & 0x3f]; + if ($i++ >= $count) + break; + $output .= $this->itoa64[($value >> 18) & 0x3f]; + } while ($i < $count); + + return $output; + } + + function gensalt_private($input) + { + $output = '$P$'; + $output .= $this->itoa64[min($this->iteration_count_log2 + + ((PHP_VERSION >= '5') ? 5 : 3), 30)]; + $output .= $this->encode64($input, 6); + + return $output; + } + + function crypt_private($password, $setting) + { + $output = '*0'; + if (substr($setting, 0, 2) === $output) + $output = '*1'; + + $id = substr($setting, 0, 3); + # We use "$P$", phpBB3 uses "$H$" for the same thing + if ($id !== '$P$' && $id !== '$H$') + return $output; + + $count_log2 = strpos($this->itoa64, $setting[3]); + if ($count_log2 < 7 || $count_log2 > 30) + return $output; + + $count = 1 << $count_log2; + + $salt = substr($setting, 4, 8); + if (strlen($salt) !== 8) + return $output; + + # We're kind of forced to use MD5 here since it's the only + # cryptographic primitive available in all versions of PHP + # currently in use. To implement our own low-level crypto + # in PHP would result in much worse performance and + # consequently in lower iteration counts and hashes that are + # quicker to crack (by non-PHP code). + if (PHP_VERSION >= '5') { + $hash = md5($salt . $password, TRUE); + do { + $hash = md5($hash . $password, TRUE); + } while (--$count); + } else { + $hash = pack('H*', md5($salt . $password)); + do { + $hash = pack('H*', md5($hash . $password)); + } while (--$count); + } + + $output = substr($setting, 0, 12); + $output .= $this->encode64($hash, 16); + + return $output; + } + + function gensalt_extended($input) + { + $count_log2 = min($this->iteration_count_log2 + 8, 24); + # This should be odd to not reveal weak DES keys, and the + # maximum valid value is (2**24 - 1) which is odd anyway. + $count = (1 << $count_log2) - 1; + + $output = '_'; + $output .= $this->itoa64[$count & 0x3f]; + $output .= $this->itoa64[($count >> 6) & 0x3f]; + $output .= $this->itoa64[($count >> 12) & 0x3f]; + $output .= $this->itoa64[($count >> 18) & 0x3f]; + + $output .= $this->encode64($input, 3); + + return $output; + } + + function gensalt_blowfish($input) + { + # This one needs to use a different order of characters and a + # different encoding scheme from the one in encode64() above. + # We care because the last character in our encoded string will + # only represent 2 bits. While two known implementations of + # bcrypt will happily accept and correct a salt string which + # has the 4 unused bits set to non-zero, we do not want to take + # chances and we also do not want to waste an additional byte + # of entropy. + $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $output = '$2a$'; + $output .= chr(ord('0') + $this->iteration_count_log2 / 10); + $output .= chr(ord('0') + $this->iteration_count_log2 % 10); + $output .= '$'; + + $i = 0; + do { + $c1 = ord($input[$i++]); + $output .= $itoa64[$c1 >> 2]; + $c1 = ($c1 & 0x03) << 4; + if ($i >= 16) { + $output .= $itoa64[$c1]; + break; + } + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 4; + $output .= $itoa64[$c1]; + $c1 = ($c2 & 0x0f) << 2; + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 6; + $output .= $itoa64[$c1]; + $output .= $itoa64[$c2 & 0x3f]; + } while (1); + + return $output; + } + + function HashPassword($password) + { + $random = ''; + + if (CRYPT_BLOWFISH === 1 && !$this->portable_hashes) { + $random = $this->get_random_bytes(16); + $hash = + crypt($password, $this->gensalt_blowfish($random)); + if (strlen($hash) === 60) + return $hash; + } + + if (CRYPT_EXT_DES === 1 && !$this->portable_hashes) { + if (strlen($random) < 3) + $random = $this->get_random_bytes(3); + $hash = + crypt($password, $this->gensalt_extended($random)); + if (strlen($hash) === 20) + return $hash; + } + + if (strlen($random) < 6) + $random = $this->get_random_bytes(6); + $hash = + $this->crypt_private($password, + $this->gensalt_private($random)); + if (strlen($hash) === 34) + return $hash; + + # Returning '*' on error is safe here, but would _not_ be safe + # in a crypt(3)-like function used _both_ for generating new + # hashes and for validating passwords against existing hashes. + return '*'; + } + + function CheckPassword($password, $stored_hash) + { + $hash = $this->crypt_private($password, $stored_hash); + if ($hash[0] === '*') + $hash = crypt($password, $stored_hash); + + return $hash === $stored_hash; + } +} + +?> diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6cf065 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +user_sql +======== + +Owncloud SQL authentification + +This is plugin is heavily based on user_imap, user_pwauth, user_ldap and user_redmine! + +Enable it in your Admin -> Apps section and configure your server's details. +Currently, it supports most of postfixadmin's encryption options, except dovecot and saslauthd. +It was tested and developed for a postfixadmin database. + +Password changing is disabled by default, but can be enabled in the Admin area. +Caution: user_sql does not recreate password salts, which imposes a security risk. +Password salts should be newly generated whenever the password changes. + +Credits + + * Johan Hendriks provided his user_postfixadmin + * Ed Wildgoose for fixing possible SQL injection vulnerability diff --git a/ajax/settings.php b/ajax/settings.php new file mode 100644 index 0000000..28ecce3 --- /dev/null +++ b/ajax/settings.php @@ -0,0 +1,260 @@ +<?php + +/** + * ownCloud - user_sql + * + * @author Andreas Böhler and contributors + * @copyright 2012-2015 Andreas Böhler <dev (at) aboehler (dot) at> + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + + /** + * This is the AJAX portion of the settings page. + * + * It can: + * - Verify the connection settings + * - Load autocomplete values for tables + * - Load autocomplete values for columns + * - Save settings for a given domain + * - Load settings for a given domain + * + * It always returns JSON encoded responses + */ + +namespace OCA\user_sql; + +// Init owncloud + +// Check if we are a user +\OCP\User::checkAdminUser(); +\OCP\JSON::checkAppEnabled('user_sql'); + +// CSRF checks +\OCP\JSON::callCheck(); + + +$helper = new \OCA\user_sql\lib\Helper; + +$l = \OC::$server->getL10N('user_sql'); + +$params = $helper -> getParameterArray(); +$response = new \OCP\AppFramework\Http\JSONResponse(); + +// Check if the request is for us +if(isset($_POST['appname']) && ($_POST['appname'] === 'user_sql') && isset($_POST['function']) && isset($_POST['domain'])) +{ + $domain = $_POST['domain']; + switch($_POST['function']) + { + // Save the settings for the given domain to the database + case 'saveSettings': + $parameters = array('host' => $_POST['sql_hostname'], + 'password' => $_POST['sql_password'], + 'user' => $_POST['sql_username'], + 'dbname' => $_POST['sql_database'], + 'tablePrefix' => '' + ); + + // Check if the table exists + if(!$helper->verifyTable($parameters, $_POST['sql_driver'], $_POST['sql_table'])) + { + $response->setData(array('status' => 'error', + 'data' => array('message' => $l -> t('The selected SQL table '.$_POST['sql_table'].' does not exist!')))); + break; + } + + // Retrieve all column settings + $columns = array(); + foreach($params as $param) + { + if(strpos($param, 'col_') === 0) + { + if(isset($_POST[$param]) && $_POST[$param] !== '') + $columns[] = $_POST[$param]; + } + } + + // Check if the columns exist + $status = $helper->verifyColumns($parameters, $_POST['sql_driver'], $_POST['sql_table'], $columns); + if($status !== true) + { + $response->setData(array('status' => 'error', + 'data' => array('message' => $l -> t('The selected SQL column(s) do(es) not exist: '.$status)))); + break; + } + + // If we reach this point, all settings have been verified + foreach($params as $param) + { + // Special handling for checkbox fields + if(isset($_POST[$param])) + { + if($param === 'set_strip_domain') + { + \OC::$server->getConfig()->setAppValue('user_sql', 'set_strip_domain_'.$domain, 'true'); + } + elseif($param === 'set_allow_pwchange') + { + \OC::$server->getConfig()->setAppValue('user_sql', 'set_allow_pwchange_'.$domain, 'true'); + } + elseif($param === 'set_active_invert') + { + \OC::$server->getConfig()->setAppValue('user_sql', 'set_active_invert_'.$domain, 'true'); + } + elseif($param === 'set_enable_gethome') + { + \OC::$server->getConfig()->setAppValue('user_sql', 'set_enable_gethome_'.$domain, 'true'); + } + else + { + \OC::$server->getConfig()->setAppValue('user_sql', $param.'_'.$domain, $_POST[$param]); + } + } else + { + if($param === 'set_strip_domain') + { + \OC::$server->getConfig()->setAppValue('user_sql', 'set_strip_domain_'.$domain, 'false'); + } + elseif($param === 'set_allow_pwchange') + { + \OC::$server->getConfig()->setAppValue('user_sql', 'set_allow_pwchange_'.$domain, 'false'); + } + elseif($param === 'set_active_invert') + { + \OC::$server->getConfig()->setAppValue('user_sql', 'set_active_invert_'.$domain, 'false'); + } + elseif($param === 'set_enable_gethome') + { + \OC::$server->getConfig()->setAppValue('user_sql', 'set_enable_gethome_'.$domain, 'false'); + } + } + } + $response->setData(array('status' => 'success', + 'data' => array('message' => $l -> t('Application settings successfully stored.')))); + break; + + // Load the settings for a given domain + case 'loadSettingsForDomain': + $retArr = array(); + foreach($params as $param) + { + $retArr[$param] = \OC::$server->getConfig()->getAppValue('user_sql', $param.'_'.$domain, ''); + } + $response->setData(array('status' => 'success', + 'settings' => $retArr)); + break; + + // Try to verify the database connection settings + case 'verifySettings': + $cm = new \OC\DB\ConnectionFactory(); + + if(!isset($_POST['sql_driver'])) + { + $response->setData(array('status' => 'error', + 'data' => array('message' => $l -> t('Error connecting to database: No driver specified.')))); + break; + } + + if(($_POST['sql_hostname'] === '') || ($_POST['sql_database'] === '')) + { + $response->setData(array('status' => 'error', + 'data' => array('message' => $l -> t('Error connecting to database: You must specify at least host and database')))); + break; + } + + $parameters = array('host' => $_POST['sql_hostname'], + 'password' => $_POST['sql_password'], + 'user' => $_POST['sql_username'], + 'dbname' => $_POST['sql_database'], + 'tablePrefix' => '' + ); + + try { + $conn = $cm -> getConnection($_POST['sql_driver'], $parameters); + $response->setData(array('status' => 'success', + 'data' => array('message' => $l -> t('Successfully connected to database')))); + } + catch(\Exception $e) + { + $response->setData(array('status' => 'error', + 'data' => array('message' => $l -> t('Error connecting to database: ').$e->getMessage()))); + } + break; + + // Get the autocompletion values for a column + case 'getColumnAutocomplete': + + + $parameters = array('host' => $_POST['sql_hostname'], + 'password' => $_POST['sql_password'], + 'user' => $_POST['sql_username'], + 'dbname' => $_POST['sql_database'], + 'tablePrefix' => '' + ); + + if($helper->verifyTable($parameters, $_POST['sql_driver'], $_POST['sql_table'])) + $columns = $helper->getColumns($parameters, $_POST['sql_driver'], $_POST['sql_table']); + else + $columns = array(); + + $search = $_POST['request']; + $ret = array(); + + foreach($columns as $name) + { + if(($search === '') || ($search === 'search') || (strpos($name, $search) === 0)) + { + $ret[] = array('label' => $name, + 'value' => $name); + } + } + $response -> setData($ret); + break; + + // Get the autocompletion values for a table + case 'getTableAutocomplete': + $parameters = array('host' => $_POST['sql_hostname'], + 'password' => $_POST['sql_password'], + 'user' => $_POST['sql_username'], + 'dbname' => $_POST['sql_database'], + 'tablePrefix' => '' + ); + + $tables = $helper->getTables($parameters, $_POST['sql_driver']); + + $search = $_POST['request']; + $ret = array(); + foreach($tables as $name) + { + if(($search === '') || ($search === 'search') || (strpos($name, $search) === 0)) + { + $ret[] = array('label' => $name, + 'value' => $name); + } + } + $response -> setData($ret); + break; + } + +} else +{ + // If the request was not for us, set an error message + $response->setData(array('status' => 'error', + 'data' => array('message' => $l -> t('Not submitted for us.')))); +} + +// Return the JSON array +echo $response->render(); diff --git a/appinfo/app.php b/appinfo/app.php new file mode 100644 index 0000000..b21c7ea --- /dev/null +++ b/appinfo/app.php @@ -0,0 +1,31 @@ +<?php + +/** +* ownCloud - user_sql +* +* @author Andreas Böhler +* @copyright 2012-2015 Andreas Böhler <dev (at) aboehler (dot) at> +* +* 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 <http://www.gnu.org/licenses/>. +* +*/ + +require_once('apps/user_sql/user_sql.php'); +\OCP\App::registerAdmin('user_sql','settings'); + +$backend = new \OCA\user_sql\OC_USER_SQL; + + +// register user backend +\OC_User::useBackend($backend); diff --git a/appinfo/info.xml b/appinfo/info.xml new file mode 100644 index 0000000..fd74bdd --- /dev/null +++ b/appinfo/info.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<info> + <id>user_sql</id> + <name>SQL user backend</name> + <summary>Authenticate Users by SQL</summary> + <description>Authenticate Users by SQL</description> + <version>2.3.1</version> + <licence>agpl</licence> + <author>Andreas Boehler <dev (at) aboehler +(dot) at ></author> + <namespace>user_sql</namespace> + <types> + <authentication/> + </types> + <category>auth</category> + <dependencies> + <owncloud min-version="8.1" max-version="9.1"/> + <nextcloud min-version="9" max-version="10"/> + </dependencies> +</info> diff --git a/appinfo/routes.php b/appinfo/routes.php new file mode 100644 index 0000000..afb4fea --- /dev/null +++ b/appinfo/routes.php @@ -0,0 +1,8 @@ +<?php +/** +* Copyright (c) 2015, Andreas Böhler <dev@aboehler.at> +* This file is licensed under the Affero General Public License version 3 or later. +* See the COPYING-README file. +*/ +/** @var $this \OCP\Route\IRouter */ +$this->create('user_sql_ajax_settings', 'ajax/settings.php')->actionInclude('user_sql/ajax/settings.php'); diff --git a/appinfo/update.php b/appinfo/update.php new file mode 100644 index 0000000..6312a16 --- /dev/null +++ b/appinfo/update.php @@ -0,0 +1,71 @@ +<?php + +/** + * ownCloud - user_sql + * + * @author Andreas Böhler and contributors + * @copyright 2012-2015 Andreas Böhler <dev (at) aboehler (dot) at> + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + +$installedVersion = \OC::$server->getConfig()->getAppValue('user_sql', 'installed_version'); + +$params = array('sql_host' => 'sql_hostname', + 'sql_user' => 'sql_username', + 'sql_database' => 'sql_database', + 'sql_password' => 'sql_password', + 'sql_table' => 'sql_table', + 'sql_column_username' => 'col_username', + 'sql_column_password' => 'col_password', + 'sql_type' => 'sql_driver', + 'sql_column_active' => 'col_active', + 'strip_domain' => 'set_strip_domain', + 'default_domain' => 'set_default_domain', + 'crypt_type' => 'set_crypt_type', + 'sql_column_displayname' => 'col_displayname', + 'allow_password_change' => 'set_allow_pwchange', + 'sql_column_active_invert' => 'set_active_invert', + 'sql_column_email' => 'col_email', + 'mail_sync_mode' => 'set_mail_sync_mode' + ); + +$delParams = array('domain_settings', + 'map_array', + 'domain_array' + ); + +if(version_compare($installedVersion, '1.99', '<')) +{ + foreach($params as $oldPar => $newPar) + { + $val = \OC::$server->getConfig()->getAppValue('user_sql', $oldPar); + if(($oldPar === 'strip_domain') || ($oldPar === 'allow_password_change') || ($oldPar === 'sql_column_active_invert')) + { + if($val) + $val = 'true'; + else + $val = 'false'; + } + if($val) + \OC::$server->getConfig()->setAppValue('user_sql', $newPar.'_default', $val); + \OC::$server->getConfig()->deleteAppValue('user_sql', $oldPar); + } + + foreach($delParams as $param) + { + \OC::$server->getConfig()->deleteAppValue('user_sql', $param); + } +} diff --git a/css/settings.css b/css/settings.css new file mode 100644 index 0000000..a899217 --- /dev/null +++ b/css/settings.css @@ -0,0 +1,24 @@ +.statusmessage { + background-color: #DDDDFF; +} +.errormessage { + background-color: #FFDDDD; +} +.successmessage { + background-color: #DDFFDD; +} +.statusmessage,.errormessage,.successmessage{ + display:none; + padding: 1px; +} + +#sql-1 p label:first-child, +#sql-2 p label:first-child, +#sql-3 p label:first-child, +#sql-4 p label:first-child, +#sql-5 p label:first-child { + display: inline-block; + text-align: right; + width: 300px; + padding-right: 10px; +} diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..a77bffb --- /dev/null +++ b/js/settings.js @@ -0,0 +1,314 @@ +// settings.js of user_sql + +// declare namespace +var user_sql = user_sql || +{ +}; + +/** + * init admin settings view + */ +user_sql.adminSettingsUI = function() +{ + + if($('#sqlDiv').length > 0) + { + // enable tabs on settings page + $('#sqlDiv').tabs(); + + // Attach auto-completion to all column fields + $('#col_username, #col_password, #col_displayname, #col_active, #col_email, #col_gethome').autocomplete({ + source: function(request, response) + { + var post = $('#sqlForm').serializeArray(); + var domain = $('#sql_domain_chooser option:selected').val(); + + post.push({ + name: 'function', + value: 'getColumnAutocomplete' + }); + + post.push({ + name: 'domain', + value: domain + }); + + post.push({ + name: 'request', + value: request.term + }); + + // Ajax foobar + $.post(OC.filePath('user_sql', 'ajax', 'settings.php'), post, response, 'json'); + }, + minLength: 0, + open: function() { + $(this).attr('state', 'open'); + }, + close: function() { + $(this).attr('state', 'closed'); + } + }).focus(function() { + if($(this).attr('state') != 'open') + { + $(this).autocomplete("search"); + } + }); + + // Attach auto-completion to all table fields + $('#sql_table').autocomplete({ + source: function(request, response) + { + var post = $('#sqlForm').serializeArray(); + var domain = $('#sql_domain_chooser option:selected').val(); + + post.push({ + name: 'function', + value: 'getTableAutocomplete' + }); + + post.push({ + name: 'domain', + value: domain + }); + + post.push({ + name: 'request', + value: request.term + }); + + // Ajax foobar + $.post(OC.filePath('user_sql', 'ajax', 'settings.php'), post, response, 'json'); + }, + minLength: 0, + open: function() { + $(this).attr('state', 'open'); + }, + close: function() { + $(this).attr('state', 'closed'); + } + }).focus(function() { + if($(this).attr('state') != 'open') + { + $(this).autocomplete("search"); + } + }); + + // Verify the SQL database settings + $('#sqlVerify').click(function(event) + { + event.preventDefault(); + + var post = $('#sqlForm').serializeArray(); + var domain = $('#sql_domain_chooser option:selected').val(); + + post.push({ + name: 'function', + value: 'verifySettings' + }); + + post.push({ + name: 'domain', + value: domain + }); + + $('#sql_verify_message').show(); + $('#sql_success_message').hide(); + $('#sql_error_message').hide(); + $('#sql_update_message').hide(); + // Ajax foobar + $.post(OC.filePath('user_sql', 'ajax', 'settings.php'), post, function(data) + { + $('#sql_verify_message').hide(); + if(data.status == 'success') + { + $('#sql_success_message').html(data.data.message); + $('#sql_success_message').show(); + window.setTimeout(function() + { + $('#sql_success_message').hide(); + }, 10000); + } else + { + $('#sql_error_message').html(data.data.message); + $('#sql_error_message').show(); + } + }, 'json'); + return false; + }); + + // Save the settings for a domain + $('#sqlSubmit').click(function(event) + { + event.preventDefault(); + + var post = $('#sqlForm').serializeArray(); + var domain = $('#sql_domain_chooser option:selected').val(); + + post.push({ + name: 'function', + value: 'saveSettings' + }); + + post.push({ + name: 'domain', + value: domain + }); + + $('#sql_update_message').show(); + $('#sql_success_message').hide(); + $('#sql_verify_message').hide(); + $('#sql_error_message').hide(); + // Ajax foobar + $.post(OC.filePath('user_sql', 'ajax', 'settings.php'), post, function(data) + { + $('#sql_update_message').hide(); + if(data.status == 'success') + { + $('#sql_success_message').html(data.data.message); + $('#sql_success_message').show(); + window.setTimeout(function() + { + $('#sql_success_message').hide(); + }, 10000); + } else + { + $('#sql_error_message').html(data.data.message); + $('#sql_error_message').show(); + } + }, 'json'); + return false; + }); + + // Attach event handler to the domain chooser + $('#sql_domain_chooser').change(function() { + user_sql.loadDomainSettings($('#sql_domain_chooser option:selected').val()); + }); + + $('#set_gethome_mode').change(function() { + user_sql.setGethomeMode(); + }); + + $('#set_enable_gethome').change(function() { + user_sql.setGethomeMode(); + }); + } +}; + +user_sql.setGethomeMode = function() +{ + var enabled = $('#set_enable_gethome').prop('checked'); + if(enabled) + { + $('#set_gethome_mode').prop('disabled', false); + var val = $('#set_gethome_mode option:selected').val(); + if(val === 'query') + { + $('#set_gethome').prop('disabled', true); + $('#col_gethome').prop('disabled', false); + } + else if(val === 'static') + { + $('#set_gethome').prop('disabled', false); + $('#col_gethome').prop('disabled', true); + } + else + { + $('#set_gethome').prop('disabled', true); + $('#col_gethome').prop('disabled', true); + } + } + else + { + $('#set_gethome_mode').prop('disabled', true); + $('#set_gethome').prop('disabled', true); + $('#col_gethome').prop('disabled', true); + } +}; + +/** + * Load the settings for the selected domain + * @param string domain The domain to load + */ +user_sql.loadDomainSettings = function(domain) +{ + $('#sql_success_message').hide(); + $('#sql_error_message').hide(); + $('#sql_verify_message').hide(); + $('#sql_loading_message').show(); + var post = [ + { + name: 'appname', + value: 'user_sql' + }, + { + name: 'function', + value: 'loadSettingsForDomain' + }, + { + name: 'domain', + value: domain + } + ]; + $.post(OC.filePath('user_sql', 'ajax', 'settings.php'), post, function(data) + { + $('#sql_loading_message').hide(); + if(data.status == 'success') + { + for(key in data.settings) + { + if(key == 'set_strip_domain') + { + if(data.settings[key] == 'true') + $('#' + key).prop('checked', true); + else + $('#' + key).prop('checked', false); + } + else if(key == 'set_allow_pwchange') + { + if(data.settings[key] == 'true') + $('#' + key).prop('checked', true); + else + $('#' + key).prop('checked', false); + } + else if(key == 'set_active_invert') + { + if(data.settings[key] == 'true') + $('#' + key).prop('checked', true); + else + $('#' + key).prop('checked', false); + } + else if(key == 'set_enable_gethome') + { + if(data.settings[key] == 'true') + $('#' + key).prop('checked', true); + else + $('#' + key).prop('checked', false); + } + else + { + $('#' + key).val(data.settings[key]); + } + } + } + else + { + $('#sql_error_message').html(data.data.message); + $('#sql_error_message').show(); + } + user_sql.setGethomeMode(); + }, 'json' + ); +}; + +// Run our JS if the SQL settings are present +$(document).ready(function() +{ + if($('#sqlDiv')) + { + user_sql.adminSettingsUI(); + user_sql.loadDomainSettings($('#sql_domain_chooser option:selected').val()); + user_sql.setGethomeMode(); + } +}); + diff --git a/lib/helper.php b/lib/helper.php new file mode 100644 index 0000000..8964ef9 --- /dev/null +++ b/lib/helper.php @@ -0,0 +1,348 @@ +<?php + +/** + * ownCloud - user_sql + * + * @author Andreas Böhler and contributors + * @copyright 2012-2015 Andreas Böhler <dev (at) aboehler (dot) at> + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\user_sql\lib; + +class Helper { + + protected $db; + protected $db_conn; + protected $settings; + + /** + * The default constructor initializes some parameters + */ + public function __construct() + { + $this->db_conn = false; + } + + /** + * Return an array with all supported parameters + * @return array Containing strings of the parameters + */ + public function getParameterArray() + { + $params = array( + 'sql_hostname', + 'sql_username', + 'sql_password', + 'sql_database', + 'sql_table', + 'sql_driver', + 'col_username', + 'col_password', + 'col_active', + 'col_displayname', + 'col_email', + 'col_gethome', + 'set_active_invert', + 'set_allow_pwchange', + 'set_default_domain', + 'set_strip_domain', + 'set_crypt_type', + 'set_mail_sync_mode', + 'set_enable_gethome', + 'set_gethome_mode', + 'set_gethome' + ); + + return $params; + } + + /** + * Load the settings for a given domain. If the domain is not found, + * the settings for 'default' are returned instead. + * @param string $domain The domain name + * @return array of settings + */ + public function loadSettingsForDomain($domain) + { + \OCP\Util::writeLog('OC_USER_SQL', "Trying to load settings for domain: " . $domain, \OCP\Util::DEBUG); + $settings = array(); + $sql_host = \OC::$server->getConfig()->getAppValue('user_sql', 'sql_hostname_'.$domain, ''); + if($sql_host === '') + { + $domain = 'default'; + } + $params = $this -> getParameterArray(); + foreach($params as $param) + { + $settings[$param] = \OC::$server->getConfig()->getAppValue('user_sql', $param.'_'.$domain, ''); + } + \OCP\Util::writeLog('OC_USER_SQL', "Loaded settings for domain: " . $domain, \OCP\Util::DEBUG); + return $settings; + } + + /** + * Run a given query type and return the results + * @param string $type The type of query to run + * @param array $params The parameter array of the query (i.e. the values to bind as key-value pairs) + * @param bool $execOnly Only execute the query, but don't fetch the results (optional, default = false) + * @param bool $fetchArray Fetch an array instead of a single row (optional, default=false) + * @param array $limits use the given limits for the query (optional, default = empty) + * @return mixed + */ + public function runQuery($type, $params, $execOnly = false, $fetchArray = false, $limits = array()) + { + \OCP\Util::writeLog('OC_USER_SQL', "Entering runQuery for type: " . $type, \OCP\Util::DEBUG); + if(!$this -> db_conn) + return false; + + switch($type) + { + case 'getHome': + $query = "SELECT ".$this->settings['col_gethome']." FROM ".$this->settings['sql_table']." WHERE ".$this->settings['col_username']." = :uid"; + break; + case 'getMail': + $query = "SELECT ".$this->settings['col_email']." FROM ".$this->settings['sql_table']." WHERE ".$this->settings['col_username']." = :uid"; + break; + + case 'setMail': + $query = "UPDATE ".$this->settings['sql_table']." SET ".$this->settings['col_email']." = :currMail WHERE ".$this->settings['col_username']." = :uid"; + break; + + case 'getPass': + $query = "SELECT ".$this->settings['col_password']." FROM ".$this->settings['sql_table']." WHERE ".$this->settings['col_username']." = :uid"; + if($this -> settings['col_active'] !== '') + $query .= " AND " .($this -> settings['set_active_invert'] === 'true' ? "NOT " : "" ) . $this -> settings['col_active']; + break; + + case 'setPass': + $query = "UPDATE ".$this->settings['sql_table']." SET ".$this->settings['col_password']." = :enc_password WHERE ".$this->settings['col_username'] ." = :uid"; + break; + + case 'getRedmineSalt': + $query = "SELECT salt FROM ".$this->settings['sql_table']." WHERE ".$this->settings['col_username'] ." = :uid;"; + break; + + case 'countUsers': + $query = "SELECT COUNT(*) FROM ".$this->settings['sql_table']." WHERE ".$this->settings['col_username'] ." LIKE :search"; + if($this -> settings['col_active'] !== '') + $query .= " AND " .($this -> settings['set_active_invert'] === 'true' ? "NOT " : "" ) . $this -> settings['col_active']; + break; + + case 'getUsers': + $query = "SELECT ".$this->settings['col_username']." FROM ".$this->settings['sql_table']; + $query .= " WHERE ".$this->settings['col_username']." LIKE :search"; + if($this -> settings['col_active'] !== '') + $query .= " AND " .($this -> settings['set_active_invert'] === 'true' ? "NOT " : "" ) . $this -> settings['col_active']; + $query .= " ORDER BY ".$this->settings['col_username']; + break; + + case 'userExists': + $query = "SELECT ".$this->settings['col_username']." FROM ".$this->settings['sql_table']." WHERE ".$this->settings['col_username']." = :uid"; + if($this -> settings['col_active'] !== '') + $query .= " AND " .($this -> settings['set_active_invert'] === 'true' ? "NOT " : "" ) . $this -> settings['col_active']; + break; + + case 'getDisplayName': + $query = "SELECT ".$this->settings['col_displayname']." FROM ".$this->settings['sql_table']." WHERE ".$this->settings['col_username']." = :uid"; + if($this -> settings['col_active'] !== '') + $query .= " AND " .($this -> settings['set_active_invert'] === 'true' ? "NOT " : "" ) . $this -> settings['col_active']; + break; + + case 'mysqlEncryptSalt': + $query = "SELECT ENCRYPT(:pw, :salt);"; + break; + + case 'mysqlEncrypt': + $query = "SELECT ENCRYPT(:pw);"; + break; + + case 'mysqlPassword': + $query = "SELECT PASSWORD(:pw);"; + break; + } + + if(isset($limits['limit']) && $limits['limit'] !== null) + { + $limit = intval($limits['limit']); + $query .= " LIMIT ".$limit; + } + + if(isset($limits['offset']) && $limits['offset'] !== null) + { + $offset = intval($limits['offset']); + $query .= " OFFSET ".$offset; + } + + \OCP\Util::writeLog('OC_USER_SQL', "Preparing query: $query", \OCP\Util::DEBUG); + $result = $this -> db -> prepare($query); + foreach($params as $param => $value) + { + $result -> bindValue(":".$param, $value); + } + \OCP\Util::writeLog('OC_USER_SQL', "Executing query...", \OCP\Util::DEBUG); + if(!$result -> execute()) + { + $err = $result -> errorInfo(); + \OCP\Util::writeLog('OC_USER_SQL', "Query failed: " . $err[2], \OCP\Util::DEBUG); + return false; + } + if($execOnly === true) + { + return true; + } + \OCP\Util::writeLog('OC_USER_SQL', "Fetching result...", \OCP\Util::DEBUG); + if($fetchArray === true) + $row = $result -> fetchAll(); + else + $row = $result -> fetch(); + + if(!$row) + { + return false; + } + return $row; + } + + /** + * Connect to the database using ownCloud's DBAL + * @param array $settings The settings for the connection + * @return bool + */ + public function connectToDb($settings) + { + $this -> settings = $settings; + $cm = new \OC\DB\ConnectionFactory(); + $parameters = array('host' => $this -> settings['sql_hostname'], + 'password' => $this -> settings['sql_password'], + 'user' => $this -> settings['sql_username'], + 'dbname' => $this -> settings['sql_database'], + 'tablePrefix' => '' + ); + try + { + $this -> db = $cm -> getConnection($this -> settings['sql_driver'], $parameters); + $this -> db -> query("SET NAMES 'UTF8'"); + $this -> db_conn = true; + return true; + } + catch (\Exception $e) + { + \OCP\Util::writeLog('OC_USER_SQL', 'Failed to connect to the database: ' . $e -> getMessage(), \OCP\Util::ERROR); + $this -> db_conn = false; + return false; + } + } + + /** + * Check if all of the given columns exist + * @param array $parameters The connection parameters + * @param string $sql_driver The SQL driver to use + * @param string $table The table name to check + * @param array $cols The columns to check + * @param array True if found, otherwise false + */ + public function verifyColumns($parameters, $sql_driver, $table, $cols) + { + $columns = $this->getColumns($parameters, $sql_driver, $table); + $res = true; + $err = ''; + foreach($cols as $col) + { + if(!in_array($col, $columns, true)) + { + $res = false; + $err .= $col.' '; + } + } + if($res) + return true; + else + return $err; + } + + /** + * Check if a given table exists + * @param array $parameters The connection parameters + * @param string $sql_driver The SQL driver to use + * @param string $table The table name to check + * @param array True if found, otherwise false + */ + public function verifyTable($parameters, $sql_driver, $table) + { + $tables = $this->getTables($parameters, $sql_driver); + return in_array($table, $tables, true); + } + + /** + * Retrieve a list of tables for the given connection parameters + * @param array $parameters The connection parameters + * @param string $sql_driver The SQL driver to use + * @return array The found tables, empty if an error occured + */ + public function getTables($parameters, $sql_driver) + { + $cm = new \OC\DB\ConnectionFactory(); + try { + $conn = $cm -> getConnection($sql_driver, $parameters); + $platform = $conn -> getDatabasePlatform(); + $query = $platform -> getListTablesSQL(); + $result = $conn -> executeQuery($query); + $ret = array(); + while($row = $result -> fetch()) + { + $name = $row['Tables_in_'.$parameters['dbname']]; + $ret[] = $name; + } + return $ret; + } + catch(\Exception $e) + { + return array(); + } + } + + /** + * Retrieve a list of columns for the given connection parameters + * @param array $parameters The connection parameters + * @param string $sql_driver The SQL driver to use + * @param string $table The SQL table to work with + * @return array The found column, empty if an error occured + */ + public function getColumns($parameters, $sql_driver, $table) + { + $cm = new \OC\DB\ConnectionFactory(); + try { + $conn = $cm -> getConnection($sql_driver, $parameters); + $platform = $conn -> getDatabasePlatform(); + $query = $platform -> getListTableColumnsSQL($table); + $result = $conn -> executeQuery($query); + $ret = array(); + while($row = $result -> fetch()) + { + $name = $row['Field']; + $ret[] = $name; + } + return $ret; + } + catch(\Exception $e) + { + return array(); + } + } + + +} diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..d62d81a --- /dev/null +++ b/settings.php @@ -0,0 +1,52 @@ +<?php + +/** + * ownCloud - user_sql + * + * @author Andreas Böhler + * @copyright 2012 Andreas Böhler <andreas (at) aboehler (dot) at> + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\user_sql; + +use OCA\user_sql\lib\Helper; + +$helper = new \OCA\user_sql\lib\Helper(); +$params = $helper -> getParameterArray(); +$settings = $helper -> loadSettingsForDomain('default'); + +\OCP\Util::addStyle('user_sql', 'settings'); +\OCP\Util::addScript('user_sql', 'settings'); +\OCP\User::checkAdminUser(); + +// fill template +$tmpl = new \OCP\Template('user_sql', 'settings'); +foreach($params as $param) +{ + $value = htmlentities($settings[$param]); + $tmpl -> assign($param, $value); +} + +$trusted_domains = \OC::$server->getConfig()->getSystemValue('trusted_domains'); +$inserted = array('default'); +array_splice($trusted_domains, 0, 0, $inserted); +$tmpl -> assign('allowed_domains', array_unique($trusted_domains)); +// workaround to detect OC version +$ocVersion = @reset(\OCP\Util::getVersion()); +$tmpl -> assign('ocVersion', $ocVersion); + +return $tmpl -> fetchPage(); diff --git a/templates/settings.php b/templates/settings.php new file mode 100644 index 0000000..c555c5c --- /dev/null +++ b/templates/settings.php @@ -0,0 +1,168 @@ +<?php $ocVersion = $_['ocVersion']; +$cfgClass = $ocVersion >= 7 ? 'section' : 'personalblock'; +?> + +<div class="<?php p($cfgClass); ?>"> + <h2><?php p($l->t('SQL User Backend')); ?></h2> + +<form id="sqlForm" action="#" method="post" class="<?php p($cfgClass); ?>"> + + <div id="sqlDiv" class="<?php p($cfgClass); ?>"> + <label for="sql_domain_chooser"><?php p($l -> t('Settings for Domain')) ?></label> + <select id="sql_domain_chooser" name="sql_domain_chooser"> + <?php foreach ($_['allowed_domains'] as $domain): ?> + <option value="<?php p($domain); ?>"><?php p($domain); ?></option> + <?php endforeach ?> + </select> + <ul> + <li><a id="sqlBasicSettings" href="#sql-1"><?php p($l -> t('Connection Settings')); ?></a></li> + <li><a id="sqlColSettings" href="#sql-2"><?php p($l -> t('Column Settings')); ?></a></li> + <li><a id="sqlEmailSettings" href="#sql-3"><?php p($l -> t('E-Mail Settings')); ?></a></li> + <li><a id="sqlDomainSettings" href="#sql-4"><?php p($l -> t('Domain Settings')); ?></a></li> + <li><a id="sqlGethomeSettings" href="#sql-5"><?php p($l -> t('getHome Settings')); ?></a></li> + </ul> + + <fieldset id="sql-1"> + <p><label for="sql_driver"><?php p($l -> t('SQL Driver')); ?></label> + <?php $db_driver = array('mysql' => 'MySQL', 'pgsql' => 'PostgreSQL'); ?> + <select id="sql_driver" name="sql_driver"> + <?php + foreach ($db_driver as $driver => $name): + //echo $_['sql_driver']; + if($_['sql_driver'] === $driver): ?> + <option selected="selected" value="<?php p($driver); ?>"><?php p($name); ?></option> + <?php else: ?> + <option value="<?php p($driver); ?>"><?php p($name); ?></option> + <?php endif; + endforeach; + ?> + </select> + </p> + + <p><label for="sql_hostname"><?php p($l -> t('Host')); ?></label><input type="text" id="sql_hostname" name="sql_hostname" value="<?php p($_['sql_hostname']); ?>"></p> + + <p><label for="sql_username"><?php p($l -> t('Username')); ?></label><input type="text" id="sql_username" name="sql_username" value="<?php p($_['sql_username']); ?>" /></p> + + <p><label for="sql_database"><?php p($l -> t('Database')); ?></label><input type="text" id="sql_database" name="sql_database" value="<?php p($_['sql_database']); ?>" /></p> + + <p><label for="sql_password"><?php p($l -> t('Password')); ?></label><input type="password" id="sql_password" name="sql_password" value="<?php p($_['sql_password']); ?>" /></p> + + <p><input type="submit" id="sqlVerify" value="<?php p($l -> t('Verify Settings')); ?>"></p> + + </fieldset> + <fieldset id="sql-2"> + <p><label for="sql_table"><?php p($l -> t('Table')); ?></label><input type="text" id="sql_table" name="sql_table" value="<?php p($_['sql_table']); ?>" /></p> + + <p><label for="col_username"><?php p($l -> t('Username Column')); ?></label><input type="text" id="col_username" name="col_username" value="<?php p($_['col_username']); ?>" /></p> + + <p><label for="col_password"><?php p($l -> t('Password Column')); ?></label><input type="text" id="col_password" name="col_password" value="<?php p($_['col_password']); ?>" /></p> + + <p><label for="set_allow_pwchange"><?php p($l -> t('Allow password changing (read README!)')); ?></label><input type="checkbox" id="set_allow_pwchange" name="set_allow_pwchange" value="1"<?php + if($_['set_allow_pwchange']) + p(' checked'); + ?>><br> + <em><?php p($l -> t('Allow changing passwords. Imposes a security risk as password salts are not recreated')); ?></em></p> + + <p><label for="col_displayname"><?php p($l -> t('Real Name Column')); ?></label><input type="text" id="col_displayname" name="col_displayname" value="<?php p($_['col_displayname']); ?>" /></p> + + <p><label for="set_crypt_type"><?php p($l -> t('Encryption Type')); ?></label> + <?php $crypt_types = array('md5' => 'MD5', 'md5crypt' => 'MD5 Crypt', 'cleartext' => 'Cleartext', 'mysql_encrypt' => 'mySQL ENCRYPT()', 'system' => 'System (crypt)', 'mysql_password' => 'mySQL PASSWORD()', 'joomla' => 'Joomla MD5 Encryption', 'joomla2' => 'Joomla > 2.5.18 phpass', 'ssha256' => 'Salted SSHA256', 'redmine' => 'Redmine'); ?> + <select id="set_crypt_type" name="set_crypt_type"> + <?php + foreach ($crypt_types as $driver => $name): + //echo $_['set_crypt_type']; + if($_['set_crypt_type'] === $driver): ?> + <option selected="selected" value="<?php p($driver); ?>"><?php p($name); ?></option> + <?php else: ?> + <option value="<?php p($driver); ?>"><?php p($name); ?></option> + <?php endif; + endforeach; + ?> + </select> + </p> + + <p><label for="col_active"><?php p($l -> t('User Active Column')); ?></label><input type="text" id="col_active" name="col_active" value="<?php p($_['col_active']); ?>" /></p> + + <p><label for="set_active_invert"><?php p($l -> t('Invert Active Value')); ?></label><input type="checkbox" id="set_active_invert" name="set_active_invert" value="1"<?php + if($_['set_active_invert']) + p(' checked'); + ?> /><br> + <em><?php p($l -> t("Invert the logic of the active column (for blocked users in the SQL DB)")); ?></em></p> + + </fieldset> + + <fieldset id="sql-3"> + + <p><label for="col_email"><?php p($l -> t('E-Mail Column')); ?></label><input type="text" id="col_email" name="col_email" value="<?php p($_['col_email']); ?>" /></p> + + <p><label for="set_mail_sync_mode"><?php p($l -> t('E-Mail address sync mode')); ?></label> + <?php $mail_modes = array('none' => 'No Synchronisation', 'initial' => 'Synchronise only once', 'forceoc' => 'ownCloud always wins', 'forcesql' => 'SQL always wins'); ?> + <select id="set_mail_sync_mode" name="set_mail_sync_mode"> + <?php + foreach ($mail_modes as $mode => $name): + //echo $_['set_mail_sync_mode']; + if($_['set_mail_sync_mode'] === $mode): ?> + <option selected="selected" value="<?php p($mode); ?>"><?php p($name); ?></option> + <?php else: ?> + <option value="<?php p($mode); ?>"><?php p($name); ?></option> + <?php endif; + endforeach; + ?> + </select> + </p> + + </fieldset> + + <fieldset id="sql-4"> + + <p><label for="set_default_domain"><?php p($l -> t('Append Default Domain')); ?></label><input type="text" id="set_default_domain", name="set_default_domain" value="<?php p($_['set_default_domain']); ?>" /><br> + <em><?php p($l -> t('Append this string, e.g. a domain name, to each user name. The @-sign is automatically inserted.')); ?></em> + </p> + + <p><label for="set_strip_domain"><?php p($l -> t('Strip Domain Part from Username')); ?></label><input type="checkbox" id="set_strip_domain" name="set_strip_domain" value="1"<?php + if($_['set_strip_domain']) + p(' checked'); + ?> /><br> + <em><?php p($l -> t("Strip Domain Part including @-sign from Username when logging in and retrieving username lists")); ?></em></p> + + </fieldset> + + <fieldset id="sql-5"> + <p><label for="set_enable_gethome"><?php p($l -> t('Enable support for getHome()')); ?></label><input type="checkbox" id="set_enable_gethome", name="set_enable_gethome" value="1" <?php + if($_['set_enable_gethome']) + p(' checked'); + ?>/></p> + + <p><label for="set_gethome_mode"><?php p($l -> t('Method for getHome')); ?></label> + <?php $gethome_modes = array('query' => 'SQL Column', 'static' => 'Static (with Variables)'); ?> + <select id="set_gethome_mode" name="set_gethome_mode"> + <?php + foreach ($gethome_modes as $mode => $name): + //echo $_['set_mail_sync_mode']; + if($_['set_gethome_mode'] === $mode): ?> + <option selected="selected" value="<?php p($mode); ?>"><?php p($name); ?></option> + <?php else: ?> + <option value="<?php p($mode); ?>"><?php p($name); ?></option> + <?php endif; + endforeach; + ?> + </select> + </p> + + <p><label for="col_gethome"><?php p($l -> t('Home Column')); ?></label><input type="text" id="col_gethome" name="col_gethome" value="<?php p($_['col_gethome']); ?>"></p> + + <p><label for="set_gethome"><?php p($l -> t('Home Dir')); ?></label><input type="text" id="set_gethome" name="set_gethome" value="<?php p($_['set_gethome']); ?>"><br> + <em><?php p($l -> t('You can use the placeholders %%u to specify the user ID (before appending the default domain), %%ud to specify the user ID (after appending the default domain) and %%d to specify the default domain')); ?></em></p> + + </fieldset> + <input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>" id="requesttoken" /> + <input type="hidden" name="appname" value="user_sql" /> + <input id="sqlSubmit" type="submit" value="<?php p($l -> t('Save')); ?>" /> + <div id="sql_update_message" class="statusmessage"><?php p($l -> t('Saving...')); ?></div> + <div id="sql_loading_message" class="statusmessage"><?php p($l -> t('Loading...')); ?></div> + <div id="sql_verify_message" class="statusmessage"><?php p($l -> t('Verifying...')); ?></div> + <div id="sql_error_message" class="errormessage"></div> + <div id="sql_success_message" class="successmessage"></div> + </div> +</form> +</div> diff --git a/user_sql.php b/user_sql.php new file mode 100644 index 0000000..9283c66 --- /dev/null +++ b/user_sql.php @@ -0,0 +1,782 @@ +<?php + +/** + * ownCloud - user_sql + * + * @author Andreas Böhler and contributors + * @copyright 2012-2015 Andreas Böhler <dev (at) aboehler (dot) at> + * + * 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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\user_sql; + +use \OCA\user_sql\lib\Helper; + +class OC_USER_SQL extends \OC_User_Backend 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) + { + \OCP\Util::writeLog('OC_USER_SQL', "Entering doEmailSync for UID: $uid", \OCP\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) + { + \OCP\Util::writeLog('OC_USER_SQL', "Could not update E-Mail address in SQL database!", \OCP\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'] !== '') + { + \OCP\Util::writeLog('OC_USER_SQL', "Append default domain: ".$this -> settings['set_default_domain'], \OCP\Util::DEBUG); + if(strpos($uid, '@') === false) + { + $uid .= "@" . $this -> settings['set_default_domain']; + } + } + + $uid = strtolower($uid); + \OCP\Util::writeLog('OC_USER_SQL', 'Returning mapped UID: ' . $uid, \OCP\Util::DEBUG); + return $uid; + } + + /** + * Return the actions implemented by this backend + * @param $actions + * @return bool + */ + public function implementsAction($actions) + { + return (bool)((\OC_User_Backend::CHECK_PASSWORD + | \OC_User_Backend::GET_DISPLAYNAME + | \OC_User_Backend::COUNT_USERS + | $this -> settings['set_allow_pwchange'] === 'true' ? \OC_User_Backend::SET_PASSWORD : 0 + | $this -> settings['set_enable_gethome'] === 'true' ? \OC_User_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) + { + \OCP\Util::writeLog('OC_USER_SQL', "Entering getHome for UID: $uid", \OCP\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': + \OCP\Util::writeLog('OC_USER_SQL', "getHome with Query selected, running Query...", \OCP\Util::DEBUG); + $row = $this -> helper -> runQuery('getHome', array('uid' => $uidMapped)); + if($row === false) + { + \OCP\Util::writeLog('OC_USER_SQL', "Got no row, return false", \OCP\Util::DEBUG); + return false; + } + $home = $row[$this -> settings['col_gethome']]; + break; + + case 'static': + \OCP\Util::writeLog('OC_USER_SQL', "getHome with static selected", \OCP\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; + } + \OCP\Util::writeLog('OC_USER_SQL', "Returning getHome for UID: $uid with Home $home", \OCP\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 + \OCP\Util::writeLog('OC_USER_SQL', 'Not possible to create local users from web frontend using SQL user backend', \OCP\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 + \OCP\Util::writeLog('OC_USER_SQL', 'Not possible to delete local users from web frontend using SQL user backend', \OCP\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 + \OCP\Util::writeLog('OC_USER_SQL', "Entering setPassword for UID: $uid", \OCP\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']]; + if($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)); + } else + { + $enc_password = $this -> pacrypt($password, $old_password); + } + $res = $this -> helper -> runQuery('setPass', array('uid' => $uid, 'enc_password' => $enc_password), true); + if($res === false) + { + \OCP\Util::writeLog('OC_USER_SQL', "Could not update password!", \OCP\Util::ERROR); + return false; + } + \OCP\Util::writeLog('OC_USER_SQL', "Updated password successfully, return true", \OCP\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) + { + \OCP\Util::writeLog('OC_USER_SQL', "Entering checkPassword() for UID: $uid", \OCP\Util::DEBUG); + + $uid = $this -> doUserDomainMapping($uid); + + $row = $this -> helper -> runQuery('getPass', array('uid' => $uid)); + if($row === false) + { + \OCP\Util::writeLog('OC_USER_SQL', "Got no row, return false", \OCP\Util::DEBUG); + return false; + } + $db_pass = $row[$this -> settings['col_password']]; + \OCP\Util::writeLog('OC_USER_SQL', "Encrypting and checking password", \OCP\Util::DEBUG); + // Joomla 2.5.18 switched to phPass, which doesn't play nice with the way + // we check passwords + if($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); + } + // 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; + } else + { + $ret = $this -> pacrypt($password, $db_pass) === $db_pass; + } + if($ret) + { + \OCP\Util::writeLog('OC_USER_SQL', "Passwords matching, return true", \OCP\Util::DEBUG); + if($this -> settings['set_strip_domain'] === 'true') + { + $uid = explode("@", $uid); + $uid = $uid[0]; + } + return $uid; + } else + { + \OCP\Util::writeLog('OC_USER_SQL', "Passwords do not match, return false", \OCP\Util::DEBUG); + return false; + } + } + + /** + * Count the number of users + * @return int The user count + */ + public function countUsers() + { + \OCP\Util::writeLog('OC_USER_SQL', "Entering countUsers()", \OCP\Util::DEBUG); + + $search = "%".$this -> doUserDomainMapping(""); + $userCount = $this -> helper -> runQuery('countUsers', array('search' => $search)); + if($userCount === false) + { + $userCount = 0; + } + else { + $userCount = reset($userCount); + } + + \OCP\Util::writeLog('OC_USER_SQL', "Return usercount: ".$userCount, \OCP\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) + { + \OCP\Util::writeLog('OC_USER_SQL', "Entering getUsers() with Search: $search, Limit: $limit, Offset: $offset", \OCP\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); + } + \OCP\Util::writeLog('OC_USER_SQL', "Return list of results", \OCP\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); + \OCP\Util::writeLog('OC_USER_SQL', "userExists() for UID: $uid cacheVal: $cacheVal", \OCP\Util::DEBUG); + if(!is_null($cacheVal)) + return (bool)$cacheVal; + + \OCP\Util::writeLog('OC_USER_SQL', "Entering userExists() for UID: $uid", \OCP\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) + { + \OCP\Util::writeLog('OC_USER_SQL', "Empty row, user does not exists, return false", \OCP\Util::DEBUG); + return false; + } else + { + \OCP\Util::writeLog('OC_USER_SQL', "User exists, return true", \OCP\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) + { + \OCP\Util::writeLog('OC_USER_SQL', "Entering getDisplayName() for UID: $uid", \OCP\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) + { + \OCP\Util::writeLog('OC_USER_SQL', "Empty row, user has no display name or does not exist, return false", \OCP\Util::DEBUG); + return false; + } else + { + \OCP\Util::writeLog('OC_USER_SQL', "User exists, return true", \OCP\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 = "") + { + \OCP\Util::writeLog('OC_USER_SQL', "Entering private pacrypt()", \OCP\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 + { + \OCP\Util::writeLog('OC_USER_SQL', "unknown/invalid crypt_type settings: ".$this->settings['set_crypt_type'], \OCP\Util::ERROR); + die('unknown/invalid Encryption type setting: ' . $this -> settings['set_crypt_type']); + } + \OCP\Util::writeLog('OC_USER_SQL', "pacrypt() done, return", \OCP\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_salt(); + $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_salt() + { + 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; + } + +} +?> |