From 8182d8ab00f1386ee5e388d1ffa4bb89c1982831 Mon Sep 17 00:00:00 2001
From: Till Brehm <tbrehm@ispconfig.org>
Date: Mon, 7 Sep 2020 16:44:51 +0200
Subject: [PATCH] #5418 Add suport for MySQL 8

---
 install/lib/installer_base.lib.php            | 36 ++++++--
 install/lib/mysql.lib.php                     | 35 ++++++++
 .../sql/incremental/upd_dev_collection.sql    |  4 +-
 install/sql/ispconfig3.sql                    |  4 +-
 interface/lib/classes/db_mysql.inc.php        | 70 ++++++++++++++++
 interface/lib/classes/tform_base.inc.php      |  6 +-
 server/lib/classes/db_mysql.inc.php           | 69 ++++++++++++++++
 .../mysql_clientdb_plugin.inc.php             | 82 ++++++++++++++++---
 8 files changed, 278 insertions(+), 28 deletions(-)

diff --git a/install/lib/installer_base.lib.php b/install/lib/installer_base.lib.php
index 11163806ab..b1a7796d01 100644
--- a/install/lib/installer_base.lib.php
+++ b/install/lib/installer_base.lib.php
@@ -235,17 +235,26 @@ class installer_base {
 	public function configure_database() {
 		global $conf;
 
-		//* check sql-mode
-		/*$check_sql_mode = $this->db->queryOneRecord("SELECT @@sql_mode");
+		//** Check for unwanted plugins
+		if ($this->db->getDatabaseType() == 'mysql' && $this->db->getDatabaseVersion(true) >= 8) {
+			// component approach since MySQL 8.0
+			$unwanted_components = [
+				'file://component_validate_password',
+			];
+			$sql_components = $this->db->queryAllRecords("SELECT * FROM mysql.component where component_urn IN ?", $unwanted_components);
+			if(is_array($sql_components) && !empty($sql_components)) {
+				foreach ($sql_components as $component) {
+					$component_name = parse_url($component['component_urn'], PHP_URL_HOST);
+					echo "Login in to MySQL and disable '{$component_name}' with:\n\n    UNINSTALL COMPONENT '{$component['component_urn']}';\n\n";
+				}
+				die();
+			}
 
-		if ($check_sql_mode['@@sql_mode'] != '' && $check_sql_mode['@@sql_mode'] != 'NO_ENGINE_SUBSTITUTION') {
-			echo "Wrong SQL-mode. You should use NO_ENGINE_SUBSTITUTION. Add\n\n";
-			echo "    sql-mode=\"NO_ENGINE_SUBSTITUTION\"\n\n";
-			echo"to the mysqld-section in your mysql-config on this server and restart mysqld afterwards\n";
-			die();
-		}*/
+			$unwanted_sql_plugins = [''];
+		} else {
+			$unwanted_sql_plugins = array('validate_password');
+		}
 
-		$unwanted_sql_plugins = array('validate_password');
 		$sql_plugins = $this->db->queryAllRecords("SELECT plugin_name FROM information_schema.plugins WHERE plugin_status='ACTIVE' AND plugin_name IN ?", $unwanted_sql_plugins);
 		if(is_array($sql_plugins) && !empty($sql_plugins)) {
 			foreach ($sql_plugins as $plugin) echo "Login in to MySQL and disable $plugin[plugin_name] with:\n\n    UNINSTALL PLUGIN $plugin[plugin_name];";
@@ -308,6 +317,15 @@ class installer_base {
 		if(!$this->db->query($query, $conf['mysql']['database'] . ".*", $conf['mysql']['ispconfig_user'], $from_host)) {
 			$this->error('Unable to grant databse permissions to user: '.$conf['mysql']['ispconfig_user'].' Error: '.$this->db->errorMessage);
 		}
+		
+		// add correct administrative rights to IPSConfig user (SUPER is deprecated and unnecessarily powerful)
+		 if ($this->db->getDatabaseType() == 'mysql' && $this->db->getDatabaseVersion(true) >= 8) {
+			// there might be more needed on replicated db environments, this was not tested
+			$query = 'GRANT SYSTEM_VARIABLES_ADMIN ON *.* TO ?@?';
+			if(!$this->db->query($query, $conf['mysql']['ispconfig_user'], $from_host)) {
+				$this->error('Unable to grant administrative permissions to user: '.$conf['mysql']['ispconfig_user'].' Error: '.$this->db->errorMessage);
+			}
+		}
 
 		//* Set the database name in the DB library
 		$this->db->setDBName($conf['mysql']['database']);
diff --git a/install/lib/mysql.lib.php b/install/lib/mysql.lib.php
index c24a454d04..1085ed0d5b 100644
--- a/install/lib/mysql.lib.php
+++ b/install/lib/mysql.lib.php
@@ -761,6 +761,41 @@ class db
 			break;
 		}
 	}
+	
+	/**
+	 * Get the database type (mariadb or mysql)
+	 *
+	 * @access public
+	 * @return string 'mariadb' or string 'mysql'
+	 */
+	
+	public function getDatabaseType() {
+		$tmp = $this->queryOneRecord('SELECT VERSION() as version');
+		if(stristr($tmp['version'],'mariadb')) {
+			return 'mariadb';
+		} else {
+			return 'mysql';
+		}
+	}
+	
+	/**
+	 * Get the database version
+	 *
+	 * @access public
+	 * @param bool   $major_version_only = true will return the major version only, e.g. 8 for MySQL 8
+	 * @return string version number
+	 */
+	
+	public function getDatabaseVersion($major_version_only = false) {
+		$tmp = $this->queryOneRecord('SELECT VERSION() as version');
+		$version = explode('-', $tmp['version']);
+		if($major_version_only == true) {
+			$version_parts = explode('.', $version[0]);
+			return $version_parts[0];
+		} else {
+			return $version[0];
+		}
+	}
 
 }
 
diff --git a/install/sql/incremental/upd_dev_collection.sql b/install/sql/incremental/upd_dev_collection.sql
index f6bd9982f7..2f3d7568e8 100644
--- a/install/sql/incremental/upd_dev_collection.sql
+++ b/install/sql/incremental/upd_dev_collection.sql
@@ -37,7 +37,7 @@ ALTER TABLE `mail_user` ADD `disableindexer-worker` ENUM('n','y') CHARACTER SET
 ALTER TABLE `dns_rr` CHANGE `type` `type` ENUM('A','AAAA','ALIAS','CNAME','DNAME','CAA','DS','HINFO','LOC','MX','NAPTR','NS','PTR','RP','SRV','SSHFP','TXT','TLSA','DNSKEY') NULL DEFAULT NULL AFTER `name`;
 
 -- change cc and sender_cc column type
-ALTER TABLE `mail_user` CHANGE `cc` `cc` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '';
+ALTER TABLE `mail_user` CHANGE `cc` `cc` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci;
 
 -- remove SPDY option
 ALTER TABLE `web_domain` DROP COLUMN `enable_spdy`;
@@ -62,7 +62,7 @@ ALTER TABLE `web_domain` CHANGE `nginx_directives` `nginx_directives` mediumtext
 ALTER TABLE `mail_user` MODIFY `move_junk` enum('y','a','n') NOT NULL DEFAULT 'y';
 
 -- Change id_rsa column to TEXT format
-ALTER TABLE `client` CHANGE `id_rsa` `id_rsa` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '';
+ALTER TABLE `client` CHANGE `id_rsa` `id_rsa` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci;
 
 ALTER TABLE `directive_snippets` ADD `update_sites` ENUM('y','n') NOT NULL DEFAULT 'n' ;
 
diff --git a/install/sql/ispconfig3.sql b/install/sql/ispconfig3.sql
index 095a2d37de..90c39c03a7 100644
--- a/install/sql/ispconfig3.sql
+++ b/install/sql/ispconfig3.sql
@@ -253,7 +253,7 @@ CREATE TABLE `client` (
   `canceled` enum('n','y') NOT NULL DEFAULT 'n',
   `can_use_api` enum('n','y') NOT NULL DEFAULT 'n',
   `tmp_data` mediumblob,
-  `id_rsa` text NOT NULL DEFAULT '',
+  `id_rsa` text,
   `ssh_rsa` varchar(600) NOT NULL DEFAULT '',
   `customer_no_template` varchar(255) DEFAULT 'R[CLIENTID]C[CUSTOMER_NO]',
   `customer_no_start` int(11) NOT NULL DEFAULT '1',
@@ -1040,7 +1040,7 @@ CREATE TABLE `mail_user` (
   `maildir` varchar(255) NOT NULL default '',
   `maildir_format` varchar(255) NOT NULL default 'maildir',
   `quota` bigint(20) NOT NULL default '-1',
-  `cc` text NOT NULL default '',
+  `cc` text,
   `sender_cc` varchar(255) NOT NULL default '',
   `homedir` varchar(255) NOT NULL default '',
   `autoresponder` enum('n','y') NOT NULL default 'n',
diff --git a/interface/lib/classes/db_mysql.inc.php b/interface/lib/classes/db_mysql.inc.php
index 014feec8c3..feab66cd93 100644
--- a/interface/lib/classes/db_mysql.inc.php
+++ b/interface/lib/classes/db_mysql.inc.php
@@ -1106,6 +1106,76 @@ class db
 		}
 	}
 
+	/**
+	 * Get the database type (mariadb or mysql)
+	 *
+	 * @access public
+	 * @return string 'mariadb' or string 'mysql'
+	 */
+	
+	public function getDatabaseType() {
+		$tmp = $this->queryOneRecord('SELECT VERSION() as version');
+		if(stristr($tmp['version'],'mariadb')) {
+			return 'mariadb';
+		} else {
+			return 'mysql';
+		}
+	}
+
+	/**
+	 * Get the database version
+	 *
+	 * @access public
+	 * @param bool   $major_version_only = true will return the major version only, e.g. 8 for MySQL 8
+	 * @return string version number
+	 */
+
+	public function getDatabaseVersion($major_version_only = false) {
+		$tmp = $this->queryOneRecord('SELECT VERSION() as version');
+		$version = explode('-', $tmp['version']);
+		if($major_version_only == true) {
+			$version_parts = explode('.', $version[0]);
+			return $version_parts[0];
+		} else {
+			return $version[0];
+		}
+	}
+	
+	/**
+	 * Get a mysql password hash
+	 *
+	 * @access public
+	 * @param string   cleartext password
+	 * @return string  Password hash
+	 */
+
+	public function getPasswordHash($password) {
+		
+		$password_type = 'password';
+		
+		/* Disabled until caching_sha2_password is implemented
+		if($this->getDatabaseType() == 'mysql' && $this->getDatabaseVersion(true) >= 8) {
+			// we are in MySQL 8 mode
+			$tmp = $this->queryOneRecord("show variables like 'default_authentication_plugin'");
+			if($tmp['default_authentication_plugin'] == 'caching_sha2_password') {
+				$password_type = 'caching_sha2_password';
+			}
+		}
+		*/
+		
+		if($password_type == 'caching_sha2_password') {
+			/*
+				caching_sha2_password hashing needs to be implemented, have not 
+				found valid PHP implementation for the new password hash type.
+			*/
+		} else {
+			$password_hash = '*'.strtoupper(sha1(sha1($password, true)));
+		}
+		
+		return $password_hash;
+	}
+
+
 }
 
 /**
diff --git a/interface/lib/classes/tform_base.inc.php b/interface/lib/classes/tform_base.inc.php
index cbbb83ee9c..91a855872c 100644
--- a/interface/lib/classes/tform_base.inc.php
+++ b/interface/lib/classes/tform_base.inc.php
@@ -1358,8 +1358,7 @@ class tform_base {
 								$record[$key] = $app->auth->crypt_password(stripslashes($record[$key]),'ISO-8859-1');
 								$sql_insert_val .= "'".$app->db->quote($record[$key])."', ";
 							} elseif (isset($field['encryption']) && $field['encryption'] == 'MYSQL') {
-								$tmp = $app->db->queryOneRecord("SELECT PASSWORD(?) as `crypted`", stripslashes($record[$key]));
-								$record[$key] = $tmp['crypted'];
+								$record[$key] = $app->db->getPasswordHash($record[$key]);
 								$sql_insert_val .= "'".$app->db->quote($record[$key])."', ";
 							} else {
 								$record[$key] = md5(stripslashes($record[$key]));
@@ -1390,8 +1389,7 @@ class tform_base {
 								$record[$key] = $app->auth->crypt_password(stripslashes($record[$key]),'ISO-8859-1');
 								$sql_update .= "`$key` = '".$app->db->quote($record[$key])."', ";
 							} elseif (isset($field['encryption']) && $field['encryption'] == 'MYSQL') {
-								$tmp = $app->db->queryOneRecord("SELECT PASSWORD(?) as `crypted`", stripslashes($record[$key]));
-								$record[$key] = $tmp['crypted'];
+								$record[$key] = $app->db->getPasswordHash($record[$key]);
 								$sql_update .= "`$key` = '".$app->db->quote($record[$key])."', ";
 							} else {
 								$record[$key] = md5(stripslashes($record[$key]));
diff --git a/server/lib/classes/db_mysql.inc.php b/server/lib/classes/db_mysql.inc.php
index 8c38123096..df38086ebe 100644
--- a/server/lib/classes/db_mysql.inc.php
+++ b/server/lib/classes/db_mysql.inc.php
@@ -1106,6 +1106,75 @@ class db
 		}
 	}
 
+	/**
+	 * Get the database type (mariadb or mysql)
+	 *
+	 * @access public
+	 * @return string 'mariadb' or string 'mysql'
+	 */
+
+	public function getDatabaseType() {
+		$tmp = $this->queryOneRecord('SELECT VERSION() as version');
+		if(stristr($tmp['version'],'mariadb')) {
+			return 'mariadb';
+		} else {
+			return 'mysql';
+		}
+	}
+
+	/**
+	 * Get the database version
+	 *
+	 * @access public
+	 * @param bool   $major_version_only = true will return the major version only, e.g. 8 for MySQL 8
+	 * @return string version number
+	 */
+
+	public function getDatabaseVersion($major_version_only = false) {
+		$tmp = $this->queryOneRecord('SELECT VERSION() as version');
+		$version = explode('-', $tmp['version']);
+		if($major_version_only == true) {
+			$version_parts = explode('.', $version[0]);
+			return $version_parts[0];
+		} else {
+			return $version[0];
+		}
+	}
+	
+	/**
+	 * Get a mysql password hash
+	 *
+	 * @access public
+	 * @param string   cleartext password
+	 * @return string  Password hash
+	 */
+	
+	public function getPasswordHash($password) {
+		
+		$password_type = 'password';
+		
+		/* Disabled until caching_sha2_password is implemented
+		if($this->getDatabaseType() == 'mysql' && $this->getDatabaseVersion(true) >= 8) {
+			// we are in MySQL 8 mode
+			$tmp = $this->queryOneRecord("show variables like 'default_authentication_plugin'");
+			if($tmp['default_authentication_plugin'] == 'caching_sha2_password') {
+				$password_type = 'caching_sha2_password';
+			}
+		}
+		*/
+		
+		if($password_type == 'caching_sha2_password') {
+			/*
+				caching_sha2_password hashing needs to be implemented, have not 
+				found valid PHP implementation for the new password hash type.
+			*/
+		} else {
+			$password_hash = '*'.strtoupper(sha1(sha1($password, true)));
+		}
+		
+		return $password_hash;
+	}
+
 }
 
 /**
diff --git a/server/plugins-available/mysql_clientdb_plugin.inc.php b/server/plugins-available/mysql_clientdb_plugin.inc.php
index e1fba6e180..f28e6006ce 100644
--- a/server/plugins-available/mysql_clientdb_plugin.inc.php
+++ b/server/plugins-available/mysql_clientdb_plugin.inc.php
@@ -101,12 +101,7 @@ class mysql_clientdb_plugin {
 
 		$success = true;
 		if(!preg_match('/\*[A-F0-9]{40}$/', $database_password)) {
-				$result = $link->query("SELECT PASSWORD('" . $link->escape_string($database_password) . "') as `crypted`");
-				if($result) {
-						$row = $result->fetch_assoc();
-						$database_password = $row['crypted'];
-						$result->free();
-				}
+				$database_password = $app->db->getPasswordHash($password);
 		}
 
 		$app->log("Calling $action for $database_name with access $user_access_mode and hosts " . implode(', ', $host_list), LOGLEVEL_DEBUG);
@@ -151,9 +146,32 @@ class mysql_clientdb_plugin {
 					$success = true;
 				}
 
-				if(!$link->query("GRANT " . $grants . " ON `".$database_name."`.* TO '".$link->escape_string($database_user)."'@'$db_host' IDENTIFIED BY PASSWORD '".$link->escape_string($database_password)."'")) $success = false;
-				$app->log("GRANT " . $grants . " ON `".$database_name."`.* TO '".$link->escape_string($database_user)."'@'$db_host' IDENTIFIED BY PASSWORD '".$link->escape_string($database_password)."' success? " . ($success ? 'yes' : 'no'), LOGLEVEL_DEBUG);
-			} elseif($action == 'REVOKE') {
+				// Create the user
+				$link->query("CREATE USER '".$link->escape_string($database_user)."'@'$db_host'");
+				$app->log("CREATE USER '".$link->escape_string($database_user)."'@'$db_host'", LOGLEVEL_DEBUG);
+
+				// set the password
+				// MySQL < 5.7 and MariadB 10
+				if(!$link->query("UPDATE mysql.user SET `Password` = '".$link->escape_string($database_password)."' WHERE `Host` = '".$db_host."' AND `User` = '".$link->escape_string($database_user)."'")) {
+					if($this->getDatabaseType($link) == 'mysql' && $this->getDatabaseVersion($link, true) >= 8) {
+						// for MySQL >= 8, we set authentication plugin to old mode to ensure that older additional php versions can still connect to the database
+						if(!$link->query("UPDATE mysql.user SET `authentication_string` = '".$link->escape_string($database_password)."', `plugin` = 'mysql_native_password' WHERE `Host` = '".$db_host."' AND `User` = '".$link->escape_string($database_user)."'")) $success = false;
+					} else {
+						// MySQL 5.7, the Password field has been renamed to authentication_string
+						if(!$link->query("UPDATE mysql.user SET `authentication_string` = '".$link->escape_string($database_password)."' WHERE `Host` = '".$db_host."' AND `User` = '".$link->escape_string($database_user)."'")) $success = false;
+					}
+				}
+				
+				if($success == true){
+					$link->query("FLUSH PRIVILEGES");
+					$app->log("PASSWORD SET FOR '".$link->escape_string($database_user)."'@'$db_host' success? " . ($success ? 'yes' : 'no'), LOGLEVEL_DEBUG);
+				} 
+
+				// Set the grant
+				if(!$link->query("GRANT " . $grants . " ON `".$link->escape_string($database_name)."`.* TO '".$link->escape_string($database_user)."'@'$db_host'")) $success = false;
+				$app->log("GRANT " . $grants . " ON `".$link->escape_string($database_name)."`.* TO '".$link->escape_string($database_user)."'@'$db_host' success? " . ($success ? 'yes' : 'no'), LOGLEVEL_DEBUG);
+
+				} elseif($action == 'REVOKE') {
 				if(!$link->query("REVOKE ALL PRIVILEGES ON `".$database_name."`.* FROM '".$link->escape_string($database_user)."'@'$db_host'")) $success = false;
 			} elseif($action == 'DROP') {
 				if(!$link->query("DROP USER '".$link->escape_string($database_user)."'@'$db_host'")) $success = false;
@@ -165,8 +183,13 @@ class mysql_clientdb_plugin {
 				if(trim($database_password) != '') {
 					// MySQL < 5.7 and MariadB 10
 					if(!$link->query("UPDATE mysql.user SET `Password` = '".$link->escape_string($database_password)."' WHERE `Host` = '".$db_host."' AND `User` = '".$link->escape_string($database_user)."'")) {
-						// MySQL 5.7, the Password field has been renamed to authentication_string
-						if(!$link->query("UPDATE mysql.user SET `authentication_string` = '".$link->escape_string($database_password)."' WHERE `Host` = '".$db_host."' AND `User` = '".$link->escape_string($database_user)."'")) $success = false;
+						if($this->getDatabaseType($link) == 'mysql' && $this->getDatabaseVersion($link, true) >= 8) {
+							// for MySQL >= 8, we set authentication plugin to old mode to ensure that older additional php versions can still connect to the database
+							if(!$link->query("UPDATE mysql.user SET `authentication_string` = '".$link->escape_string($database_password)."', `plugin` = 'mysql_native_password' WHERE `Host` = '".$db_host."' AND `User` = '".$link->escape_string($database_user)."'")) $success = false;
+						} else {
+							// MySQL 5.7, the Password field has been renamed to authentication_string
+							if(!$link->query("UPDATE mysql.user SET `authentication_string` = '".$link->escape_string($database_password)."' WHERE `Host` = '".$db_host."' AND `User` = '".$link->escape_string($database_user)."'")) $success = false;
+						}
 					}
 					if($success == true) $link->query("FLUSH PRIVILEGES");
 				}
@@ -772,6 +795,43 @@ class mysql_clientdb_plugin {
 
 		$link->close();
 	}
+	
+	
+				
+				
+	function getDatabaseType($link) {
+		$result = $link->query('SELECT VERSION() as version');
+		if($result) {
+			$tmp = $result->fetch_assoc();
+			$result->free();
+			
+			if(stristr($tmp['version'],'mariadb')) {
+				return 'mariadb';
+			} else {
+				return 'mysql';
+			}
+		} else {
+			return false;
+		}
+	}
+
+	function getDatabaseVersion($link, $major_version_only = false) {
+		$result = $link->query('SELECT VERSION() as version');
+		if($result) {
+			$tmp = $result->fetch_assoc();
+			$result->free();
+			
+			$version = explode('-', $tmp['version']);
+			if($major_version_only == true) {
+				$version_parts = explode('.', $version[0]);
+				return $version_parts[0];
+			} else {
+				return $version[0];
+			}
+		} else {
+			return false;
+		}
+	}
 
 } // end class
 
-- 
GitLab