From 3a3934392a628d19008a1cf3ebeb0c788a148689 Mon Sep 17 00:00:00 2001 From: Andreas Boehler Date: Thu, 30 Jul 2015 11:22:50 +0200 Subject: [PATCH] Support autocompletion for tables and columns Update documentation Switch to ownCloud's DBAL (not fully tested, yet) Bump to v2.0 --- ajax/settings.php | 158 +++++++++++++++++++++++++++++++++++++++-- appinfo/info.xml | 2 +- js/settings.js | 141 ++++++++++++++++++++++++++++++++++-- lib/helper.php | 39 +++++++++- templates/settings.php | 8 ++- user_sql.php | 120 +++++++++++++++++++++++++------ 6 files changed, 428 insertions(+), 40 deletions(-) diff --git a/ajax/settings.php b/ajax/settings.php index 4ae6bfa..1567dc5 100644 --- a/ajax/settings.php +++ b/ajax/settings.php @@ -1,5 +1,39 @@ + * + * 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 . + * + */ + + /** + * 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 @@ -17,15 +51,19 @@ $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': foreach($params as $param) { + // Special handling for checkbox fields if(isset($_POST[$param])) { if($param === 'set_strip_domain') @@ -60,24 +98,132 @@ if(isset($_POST['appname']) && ($_POST['appname'] === 'user_sql') && isset($_POS } } } + $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, ''); } - \OCP\JSON::success(array('settings' => $retArr)); - return true; + $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': + $cm = new \OC\DB\ConnectionFactory(); + $search = $_POST['request']; + $table = $_POST['sql_table']; + $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); + $platform = $conn -> getDatabasePlatform(); + $query = $platform -> getListTableColumnsSQL($table); + $result = $conn -> executeQuery($query); + $ret = array(); + while($row = $result -> fetch()) + { + $name = $row['Field']; + if(($search === '') || ($search === 'search') || (strpos($name, $search) === 0)) + { + $ret[] = array('label' => $name, + 'value' => $name); + } + } + $response -> setData($ret); + } + catch(\Exception $e) + { + $response->setData(array()); + } + break; + + // Get the autocompletion values for a table + case 'getTableAutocomplete': + $cm = new \OC\DB\ConnectionFactory(); + $search = $_POST['request']; + $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); + $platform = $conn -> getDatabasePlatform(); + $query = $platform -> getListTablesSQL(); + $result = $conn -> executeQuery($query); + $ret = array(); + while($row = $result -> fetch()) + { + $name = $row['Tables_in_'.$_POST['sql_database']]; + if(($search === '') || ($search === 'search') || (strpos($name, $search) === 0)) + { + $ret[] = array('label' => $name, + 'value' => $name); + } + } + $response -> setData($ret); + } + catch(\Exception $e) + { + $response->setData(array()); + } break; } } else { - \OCP\JSON::error(array('data' => array('message' => $l -> t('Not submitted for us.')))); - return false; + // 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.')))); } -\OCP\JSON::success(array('data' => array('message' => $l -> t('Application settings successfully stored.')))); -return true; +// Return the JSON array +echo $response->render(); diff --git a/appinfo/info.xml b/appinfo/info.xml index 02bacc2..eb45adc 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -6,7 +6,7 @@ AGPL Andreas Boehler <dev (at) aboehler (dot) at > - 1.99 + 2.0 8.1 false user_sql diff --git a/js/settings.js b/js/settings.js index bb5538f..9167fc9 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1,3 +1,5 @@ +// settings.js of user_sql + // declare namespace var user_sql = user_sql || { @@ -13,12 +15,133 @@ user_sql.adminSettingsUI = function() { // enable tabs on settings page $('#sql').tabs(); + + // Attach auto-completion to all column fields + $('#col_username, #col_password, #col_displayname, #col_active, #col_email').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 self = $(this); var post = $('#sqlForm').serializeArray(); var domain = $('#sql_domain_chooser option:selected').val(); @@ -34,6 +157,7 @@ user_sql.adminSettingsUI = function() $('#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) @@ -55,17 +179,23 @@ user_sql.adminSettingsUI = function() }, 'json'); return false; }); - + + // Attach event handler to the domain chooser $('#sql_domain_chooser').change(function() { user_sql.loadDomainSettings($('#sql_domain_chooser option:selected').val()); }); - - } }; +/** + * 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 = [ { @@ -120,10 +250,11 @@ user_sql.loadDomainSettings = function(domain) $('#sql_error_message').html(data.data.message); $('#sql_error_message').show(); } - } + }, 'json' ); }; +// Run our JS if the SQL settings are present $(document).ready(function() { if($('#sql')) diff --git a/lib/helper.php b/lib/helper.php index c1cd49e..08c081b 100644 --- a/lib/helper.php +++ b/lib/helper.php @@ -29,11 +29,18 @@ class Helper { 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( @@ -59,6 +66,12 @@ class Helper { 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); @@ -77,6 +90,15 @@ class Helper { 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); @@ -188,18 +210,29 @@ class Helper { 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; - $dsn = $this -> settings['sql_driver'] . ":host=" . $this -> settings['sql_hostname'] . ";dbname=" . $this -> settings['sql_database']; + $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 = new \PDO($dsn, $this -> settings['sql_username'], $this -> settings['sql_password']); + $this -> db = $cm -> getConnection($this -> settings['sql_driver'], $parameters); $this -> db -> query("SET NAMES 'UTF8'"); $this -> db_conn = true; return true; } - catch (\PDOException $e) + catch (\Exception $e) { \OCP\Util::writeLog('OC_USER_SQL', 'Failed to connect to the database: ' . $e -> getMessage(), \OCP\Util::ERROR); $this -> db_conn = false; diff --git a/templates/settings.php b/templates/settings.php index b1bcd8b..163bf1b 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -15,7 +15,7 @@ $cfgClass = $ocVersion >= 7 ? 'section' : 'personalblock'; @@ -41,11 +41,12 @@ $cfgClass = $ocVersion >= 7 ? 'section' : 'personalblock'; - +
+
= 7 ? 'section' : 'personalblock';
- +
t('Saving...')); ?>
t('Loading...')); ?>
+
t('Verifying...')); ?>
diff --git a/user_sql.php b/user_sql.php index 1fdacce..807cf37 100644 --- a/user_sql.php +++ b/user_sql.php @@ -34,11 +34,14 @@ use \OCA\user_sql\lib\Helper; class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\UserInterface { protected $cache; - // cached settings protected $settings; protected $helper; protected $session_cache_name; + /** + * 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(); @@ -54,6 +57,17 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us 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); @@ -100,6 +114,13 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us 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); @@ -118,6 +139,11 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us return $uid; } + /** + * Return the actions implemented by this backend + * @param $actions + * @return bool + */ public function implementsAction($actions) { return (bool)((\OC_User_Backend::CHECK_PASSWORD @@ -126,11 +152,19 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us ) & $actions); } + /** + * Checks if this backend has user listing support + * @return bool + */ public function hasUserListings() { return true; } + /** + * 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 @@ -138,6 +172,11 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us 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 @@ -145,6 +184,14 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us 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 @@ -192,10 +239,10 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us } /** - * @brief Check if the password is correct - * @param $uid The username - * @param $password The password - * @returns true/false + * 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 */ @@ -250,6 +297,10 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us } } + /** + * Count the number of users + * @return int The user count + */ public function countUsers() { \OCP\Util::writeLog('OC_USER_SQL', "Entering countUsers()", \OCP\Util::DEBUG); @@ -269,12 +320,12 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us } /** - * @brief Get a list of all users - * @returns array with all uids - * - * Get a list of all users. + * 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); @@ -308,11 +359,10 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us } /** - * @brief check if a user exists + * Check if a user exists * @param string $uid the username * @return boolean */ - public function userExists($uid) { @@ -341,6 +391,11 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us } + /** + * 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); @@ -398,8 +453,8 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us * salt used - hence * the second parameter ($pw_db), which is the existing hash from the DB. * - * @param string $pw - * @param string $encrypted password + * @param string $pw cleartext password + * @param string $pw_db encrypted password from database * @return string encrypted password. */ private function pacrypt($pw, $pw_db = "") @@ -486,11 +541,14 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us return $password; } - // - // md5crypt - // Action: Creates MD5 encrypted password - // Call: md5crypt (string cleartextpassword) - // + /** + * 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 = "") { @@ -563,6 +621,10 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us return "$magic$salt\$$passwd"; } + /** + * Create a new salte + * @return string The salt + */ private function create_salt() { srand((double) microtime() * 1000000); @@ -570,11 +632,22 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us 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')) @@ -593,6 +666,9 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us } } + /** + * Convert to 64? + */ private function to64($v, $n) { $ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; @@ -608,11 +684,11 @@ class OC_USER_SQL extends \OC_User_Backend implements \OCP\IUserBackend, \OCP\Us /** * Store a value in memcache or the session, if no memcache is available - * @param string $key - * @param mixed $value + * @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) + private function setCache($key, $value, $ttl=3600) { if ($this -> cache === NULL) {