ConnectionFactory.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\DB;
  8. use Doctrine\Common\EventManager;
  9. use Doctrine\DBAL\Configuration;
  10. use Doctrine\DBAL\DriverManager;
  11. use Doctrine\DBAL\Event\Listeners\OracleSessionInit;
  12. use OC\SystemConfig;
  13. /**
  14. * Takes care of creating and configuring Doctrine connections.
  15. */
  16. class ConnectionFactory {
  17. /** @var string default database name */
  18. public const DEFAULT_DBNAME = 'owncloud';
  19. /** @var string default database table prefix */
  20. public const DEFAULT_DBTABLEPREFIX = 'oc_';
  21. /**
  22. * @var array
  23. *
  24. * Array mapping DBMS type to default connection parameters passed to
  25. * \Doctrine\DBAL\DriverManager::getConnection().
  26. */
  27. protected $defaultConnectionParams = [
  28. 'mysql' => [
  29. 'adapter' => AdapterMySQL::class,
  30. 'charset' => 'UTF8',
  31. 'driver' => 'pdo_mysql',
  32. 'wrapperClass' => Connection::class,
  33. ],
  34. 'oci' => [
  35. 'adapter' => AdapterOCI8::class,
  36. 'charset' => 'AL32UTF8',
  37. 'driver' => 'oci8',
  38. 'wrapperClass' => OracleConnection::class,
  39. ],
  40. 'pgsql' => [
  41. 'adapter' => AdapterPgSql::class,
  42. 'driver' => 'pdo_pgsql',
  43. 'wrapperClass' => Connection::class,
  44. ],
  45. 'sqlite3' => [
  46. 'adapter' => AdapterSqlite::class,
  47. 'driver' => 'pdo_sqlite',
  48. 'wrapperClass' => Connection::class,
  49. ],
  50. ];
  51. public function __construct(
  52. private SystemConfig $config
  53. ) {
  54. if ($this->config->getValue('mysql.utf8mb4', false)) {
  55. $this->defaultConnectionParams['mysql']['charset'] = 'utf8mb4';
  56. }
  57. $collationOverride = $this->config->getValue('mysql.collation', null);
  58. if ($collationOverride) {
  59. $this->defaultConnectionParams['mysql']['collation'] = $collationOverride;
  60. }
  61. }
  62. /**
  63. * @brief Get default connection parameters for a given DBMS.
  64. * @param string $type DBMS type
  65. * @throws \InvalidArgumentException If $type is invalid
  66. * @return array Default connection parameters.
  67. */
  68. public function getDefaultConnectionParams($type) {
  69. $normalizedType = $this->normalizeType($type);
  70. if (!isset($this->defaultConnectionParams[$normalizedType])) {
  71. throw new \InvalidArgumentException("Unsupported type: $type");
  72. }
  73. $result = $this->defaultConnectionParams[$normalizedType];
  74. // \PDO::MYSQL_ATTR_FOUND_ROWS may not be defined, e.g. when the MySQL
  75. // driver is missing. In this case, we won't be able to connect anyway.
  76. if ($normalizedType === 'mysql' && defined('\PDO::MYSQL_ATTR_FOUND_ROWS')) {
  77. $result['driverOptions'] = [
  78. \PDO::MYSQL_ATTR_FOUND_ROWS => true,
  79. ];
  80. }
  81. return $result;
  82. }
  83. /**
  84. * @brief Get default connection parameters for a given DBMS.
  85. * @param string $type DBMS type
  86. * @param array $additionalConnectionParams Additional connection parameters
  87. * @return \OC\DB\Connection
  88. */
  89. public function getConnection(string $type, array $additionalConnectionParams): Connection {
  90. $normalizedType = $this->normalizeType($type);
  91. $eventManager = new EventManager();
  92. $eventManager->addEventSubscriber(new SetTransactionIsolationLevel());
  93. $connectionParams = $this->createConnectionParams('', $additionalConnectionParams);
  94. switch ($normalizedType) {
  95. case 'pgsql':
  96. // pg_connect used by Doctrine DBAL does not support URI notation (enclosed in brackets)
  97. $matches = [];
  98. if (preg_match('/^\[([^\]]+)\]$/', $connectionParams['host'], $matches)) {
  99. // Host variable carries a port or socket.
  100. $connectionParams['host'] = $matches[1];
  101. }
  102. break;
  103. case 'oci':
  104. $eventManager->addEventSubscriber(new OracleSessionInit);
  105. // the driverOptions are unused in dbal and need to be mapped to the parameters
  106. if (isset($connectionParams['driverOptions'])) {
  107. $connectionParams = array_merge($connectionParams, $connectionParams['driverOptions']);
  108. }
  109. $host = $connectionParams['host'];
  110. $port = $connectionParams['port'] ?? null;
  111. $dbName = $connectionParams['dbname'];
  112. // we set the connect string as dbname and unset the host to coerce doctrine into using it as connect string
  113. if ($host === '') {
  114. $connectionParams['dbname'] = $dbName; // use dbname as easy connect name
  115. } else {
  116. $connectionParams['dbname'] = '//' . $host . (!empty($port) ? ":{$port}" : "") . '/' . $dbName;
  117. }
  118. unset($connectionParams['host']);
  119. break;
  120. case 'sqlite3':
  121. $journalMode = $connectionParams['sqlite.journal_mode'];
  122. $connectionParams['platform'] = new OCSqlitePlatform();
  123. $eventManager->addEventSubscriber(new SQLiteSessionInit(true, $journalMode));
  124. break;
  125. }
  126. /** @var Connection $connection */
  127. $connection = DriverManager::getConnection(
  128. $connectionParams,
  129. new Configuration(),
  130. $eventManager
  131. );
  132. return $connection;
  133. }
  134. /**
  135. * @brief Normalize DBMS type
  136. * @param string $type DBMS type
  137. * @return string Normalized DBMS type
  138. */
  139. public function normalizeType($type) {
  140. return $type === 'sqlite' ? 'sqlite3' : $type;
  141. }
  142. /**
  143. * Checks whether the specified DBMS type is valid.
  144. *
  145. * @param string $type
  146. * @return bool
  147. */
  148. public function isValidType($type) {
  149. $normalizedType = $this->normalizeType($type);
  150. return isset($this->defaultConnectionParams[$normalizedType]);
  151. }
  152. /**
  153. * Create the connection parameters for the config
  154. *
  155. * @param string $configPrefix
  156. * @return array
  157. */
  158. public function createConnectionParams(string $configPrefix = '', array $additionalConnectionParams = []) {
  159. $type = $this->config->getValue('dbtype', 'sqlite');
  160. $connectionParams = array_merge($this->getDefaultConnectionParams($type), [
  161. 'user' => $this->config->getValue($configPrefix . 'dbuser', $this->config->getValue('dbuser', '')),
  162. 'password' => $this->config->getValue($configPrefix . 'dbpassword', $this->config->getValue('dbpassword', '')),
  163. ]);
  164. $name = $this->config->getValue($configPrefix . 'dbname', $this->config->getValue('dbname', self::DEFAULT_DBNAME));
  165. if ($this->normalizeType($type) === 'sqlite3') {
  166. $dataDir = $this->config->getValue("datadirectory", \OC::$SERVERROOT . '/data');
  167. $connectionParams['path'] = $dataDir . '/' . $name . '.db';
  168. } else {
  169. $host = $this->config->getValue($configPrefix . 'dbhost', $this->config->getValue('dbhost', ''));
  170. $connectionParams = array_merge($connectionParams, $this->splitHostFromPortAndSocket($host));
  171. $connectionParams['dbname'] = $name;
  172. }
  173. $connectionParams['tablePrefix'] = $this->config->getValue('dbtableprefix', self::DEFAULT_DBTABLEPREFIX);
  174. $connectionParams['sqlite.journal_mode'] = $this->config->getValue('sqlite.journal_mode', 'WAL');
  175. //additional driver options, eg. for mysql ssl
  176. $driverOptions = $this->config->getValue($configPrefix . 'dbdriveroptions', $this->config->getValue('dbdriveroptions', null));
  177. if ($driverOptions) {
  178. $connectionParams['driverOptions'] = $driverOptions;
  179. }
  180. // set default table creation options
  181. $connectionParams['defaultTableOptions'] = [
  182. 'collate' => 'utf8_bin',
  183. 'tablePrefix' => $connectionParams['tablePrefix']
  184. ];
  185. if ($this->config->getValue('mysql.utf8mb4', false)) {
  186. $connectionParams['defaultTableOptions'] = [
  187. 'collate' => 'utf8mb4_bin',
  188. 'charset' => 'utf8mb4',
  189. 'tablePrefix' => $connectionParams['tablePrefix']
  190. ];
  191. }
  192. if ($this->config->getValue('dbpersistent', false)) {
  193. $connectionParams['persistent'] = true;
  194. }
  195. $connectionParams = array_merge($connectionParams, $additionalConnectionParams);
  196. $replica = $this->config->getValue($configPrefix . 'dbreplica', $this->config->getValue('dbreplica', [])) ?: [$connectionParams];
  197. return array_merge($connectionParams, [
  198. 'primary' => $connectionParams,
  199. 'replica' => $replica,
  200. ]);
  201. }
  202. /**
  203. * @param string $host
  204. * @return array
  205. */
  206. protected function splitHostFromPortAndSocket($host): array {
  207. $params = [
  208. 'host' => $host,
  209. ];
  210. $matches = [];
  211. if (preg_match('/^(.*):([^\]:]+)$/', $host, $matches)) {
  212. // Host variable carries a port or socket.
  213. $params['host'] = $matches[1];
  214. if (is_numeric($matches[2])) {
  215. $params['port'] = (int) $matches[2];
  216. } else {
  217. $params['unix_socket'] = $matches[2];
  218. }
  219. }
  220. return $params;
  221. }
  222. }