db.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <?php
  2. /**
  3. * ownCloud
  4. *
  5. * @author Frank Karlitschek
  6. * @copyright 2012 Frank Karlitschek frank@owncloud.org
  7. *
  8. * This library is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  10. * License as published by the Free Software Foundation; either
  11. * version 3 of the License, or any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public
  19. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. define('MDB2_SCHEMA_DUMP_STRUCTURE', '1');
  23. class DatabaseException extends Exception {
  24. private $query;
  25. //FIXME getQuery seems to be unused, maybe use parent constructor with $message, $code and $previous
  26. public function __construct($message, $query = null){
  27. parent::__construct($message);
  28. $this->query = $query;
  29. }
  30. public function getQuery() {
  31. return $this->query;
  32. }
  33. }
  34. /**
  35. * This class manages the access to the database. It basically is a wrapper for
  36. * Doctrine with some adaptions.
  37. */
  38. class OC_DB {
  39. /**
  40. * @var \OC\DB\Connection $connection
  41. */
  42. static private $connection; //the prefered connection to use, only Doctrine
  43. static private $prefix=null;
  44. static private $type=null;
  45. /**
  46. * @brief connects to the database
  47. * @return bool true if connection can be established or false on error
  48. *
  49. * Connects to the database as specified in config.php
  50. */
  51. public static function connect() {
  52. if(self::$connection) {
  53. return true;
  54. }
  55. // The global data we need
  56. $name = OC_Config::getValue( "dbname", "owncloud" );
  57. $host = OC_Config::getValue( "dbhost", "" );
  58. $user = OC_Config::getValue( "dbuser", "" );
  59. $pass = OC_Config::getValue( "dbpassword", "" );
  60. $type = OC_Config::getValue( "dbtype", "sqlite" );
  61. if(strpos($host, ':')) {
  62. list($host, $port)=explode(':', $host, 2);
  63. } else {
  64. $port=false;
  65. }
  66. // do nothing if the connection already has been established
  67. if (!self::$connection) {
  68. $config = new \Doctrine\DBAL\Configuration();
  69. $eventManager = new \Doctrine\Common\EventManager();
  70. switch($type) {
  71. case 'sqlite':
  72. case 'sqlite3':
  73. $datadir=OC_Config::getValue( "datadirectory", OC::$SERVERROOT.'/data' );
  74. $connectionParams = array(
  75. 'user' => $user,
  76. 'password' => $pass,
  77. 'path' => $datadir.'/'.$name.'.db',
  78. 'driver' => 'pdo_sqlite',
  79. );
  80. $connectionParams['adapter'] = '\OC\DB\AdapterSqlite';
  81. $connectionParams['wrapperClass'] = 'OC\DB\Connection';
  82. break;
  83. case 'mysql':
  84. $connectionParams = array(
  85. 'user' => $user,
  86. 'password' => $pass,
  87. 'host' => $host,
  88. 'port' => $port,
  89. 'dbname' => $name,
  90. 'charset' => 'UTF8',
  91. 'driver' => 'pdo_mysql',
  92. );
  93. $connectionParams['adapter'] = '\OC\DB\Adapter';
  94. $connectionParams['wrapperClass'] = 'OC\DB\Connection';
  95. break;
  96. case 'pgsql':
  97. $connectionParams = array(
  98. 'user' => $user,
  99. 'password' => $pass,
  100. 'host' => $host,
  101. 'port' => $port,
  102. 'dbname' => $name,
  103. 'driver' => 'pdo_pgsql',
  104. );
  105. $connectionParams['adapter'] = '\OC\DB\AdapterPgSql';
  106. $connectionParams['wrapperClass'] = 'OC\DB\Connection';
  107. break;
  108. case 'oci':
  109. $connectionParams = array(
  110. 'user' => $user,
  111. 'password' => $pass,
  112. 'host' => $host,
  113. 'dbname' => $name,
  114. 'charset' => 'AL32UTF8',
  115. 'driver' => 'oci8',
  116. );
  117. if (!empty($port)) {
  118. $connectionParams['port'] = $port;
  119. }
  120. $connectionParams['adapter'] = '\OC\DB\AdapterOCI8';
  121. $connectionParams['wrapperClass'] = 'OC\DB\OracleConnection';
  122. $eventManager->addEventSubscriber(new \Doctrine\DBAL\Event\Listeners\OracleSessionInit);
  123. break;
  124. case 'mssql':
  125. $connectionParams = array(
  126. 'user' => $user,
  127. 'password' => $pass,
  128. 'host' => $host,
  129. 'port' => $port,
  130. 'dbname' => $name,
  131. 'charset' => 'UTF8',
  132. 'driver' => 'pdo_sqlsrv',
  133. );
  134. $connectionParams['adapter'] = '\OC\DB\AdapterSQLSrv';
  135. $connectionParams['wrapperClass'] = 'OC\DB\Connection';
  136. break;
  137. default:
  138. return false;
  139. }
  140. $connectionParams['tablePrefix'] = OC_Config::getValue('dbtableprefix', 'oc_' );
  141. try {
  142. self::$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config, $eventManager);
  143. if ($type === 'sqlite' || $type === 'sqlite3') {
  144. // Sqlite doesn't handle query caching and schema changes
  145. // TODO: find a better way to handle this
  146. self::$connection->disableQueryStatementCaching();
  147. }
  148. } catch(\Doctrine\DBAL\DBALException $e) {
  149. OC_Log::write('core', $e->getMessage(), OC_Log::FATAL);
  150. OC_User::setUserId(null);
  151. // send http status 503
  152. header('HTTP/1.1 503 Service Temporarily Unavailable');
  153. header('Status: 503 Service Temporarily Unavailable');
  154. OC_Template::printErrorPage('Failed to connect to database');
  155. die();
  156. }
  157. }
  158. return true;
  159. }
  160. /**
  161. * @return \OC\DB\Connection
  162. */
  163. static public function getConnection() {
  164. self::connect();
  165. return self::$connection;
  166. }
  167. /**
  168. * get MDB2 schema manager
  169. *
  170. * @return \OC\DB\MDB2SchemaManager
  171. */
  172. private static function getMDB2SchemaManager()
  173. {
  174. return new \OC\DB\MDB2SchemaManager(self::getConnection());
  175. }
  176. /**
  177. * @brief Prepare a SQL query
  178. * @param string $query Query string
  179. * @param int $limit
  180. * @param int $offset
  181. * @param bool $isManipulation
  182. * @throws DatabaseException
  183. * @return \Doctrine\DBAL\Statement prepared SQL query
  184. *
  185. * SQL query via Doctrine prepare(), needs to be execute()'d!
  186. */
  187. static public function prepare( $query , $limit = null, $offset = null, $isManipulation = null) {
  188. self::connect();
  189. if ($isManipulation === null) {
  190. //try to guess, so we return the number of rows on manipulations
  191. $isManipulation = self::isManipulation($query);
  192. }
  193. // return the result
  194. try {
  195. $result = self::$connection->prepare($query, $limit, $offset);
  196. } catch (\Doctrine\DBAL\DBALException $e) {
  197. throw new \DatabaseException($e->getMessage(), $query);
  198. }
  199. // differentiate between query and manipulation
  200. $result = new OC_DB_StatementWrapper($result, $isManipulation);
  201. return $result;
  202. }
  203. /**
  204. * tries to guess the type of statement based on the first 10 characters
  205. * the current check allows some whitespace but does not work with IF EXISTS or other more complex statements
  206. *
  207. * @param string $sql
  208. * @return bool
  209. */
  210. static public function isManipulation( $sql ) {
  211. $selectOccurrence = stripos($sql, 'SELECT');
  212. if ($selectOccurrence !== false && $selectOccurrence < 10) {
  213. return false;
  214. }
  215. $insertOccurrence = stripos($sql, 'INSERT');
  216. if ($insertOccurrence !== false && $insertOccurrence < 10) {
  217. return true;
  218. }
  219. $updateOccurrence = stripos($sql, 'UPDATE');
  220. if ($updateOccurrence !== false && $updateOccurrence < 10) {
  221. return true;
  222. }
  223. $deleteOccurrence = stripos($sql, 'DELETE');
  224. if ($deleteOccurrence !== false && $deleteOccurrence < 10) {
  225. return true;
  226. }
  227. return false;
  228. }
  229. /**
  230. * @brief execute a prepared statement, on error write log and throw exception
  231. * @param mixed $stmt OC_DB_StatementWrapper,
  232. * an array with 'sql' and optionally 'limit' and 'offset' keys
  233. * .. or a simple sql query string
  234. * @param array $parameters
  235. * @return result
  236. * @throws DatabaseException
  237. */
  238. static public function executeAudited( $stmt, array $parameters = null) {
  239. if (is_string($stmt)) {
  240. // convert to an array with 'sql'
  241. if (stripos($stmt, 'LIMIT') !== false) { //OFFSET requires LIMIT, so we only need to check for LIMIT
  242. // TODO try to convert LIMIT OFFSET notation to parameters, see fixLimitClauseForMSSQL
  243. $message = 'LIMIT and OFFSET are forbidden for portability reasons,'
  244. . ' pass an array with \'limit\' and \'offset\' instead';
  245. throw new DatabaseException($message);
  246. }
  247. $stmt = array('sql' => $stmt, 'limit' => null, 'offset' => null);
  248. }
  249. if (is_array($stmt)) {
  250. // convert to prepared statement
  251. if ( ! array_key_exists('sql', $stmt) ) {
  252. $message = 'statement array must at least contain key \'sql\'';
  253. throw new DatabaseException($message);
  254. }
  255. if ( ! array_key_exists('limit', $stmt) ) {
  256. $stmt['limit'] = null;
  257. }
  258. if ( ! array_key_exists('limit', $stmt) ) {
  259. $stmt['offset'] = null;
  260. }
  261. $stmt = self::prepare($stmt['sql'], $stmt['limit'], $stmt['offset']);
  262. }
  263. self::raiseExceptionOnError($stmt, 'Could not prepare statement');
  264. if ($stmt instanceof OC_DB_StatementWrapper) {
  265. $result = $stmt->execute($parameters);
  266. self::raiseExceptionOnError($result, 'Could not execute statement');
  267. } else {
  268. if (is_object($stmt)) {
  269. $message = 'Expected a prepared statement or array got ' . get_class($stmt);
  270. } else {
  271. $message = 'Expected a prepared statement or array got ' . gettype($stmt);
  272. }
  273. throw new DatabaseException($message);
  274. }
  275. return $result;
  276. }
  277. /**
  278. * @brief gets last value of autoincrement
  279. * @param string $table The optional table name (will replace *PREFIX*) and add sequence suffix
  280. * @return int id
  281. * @throws DatabaseException
  282. *
  283. * \Doctrine\DBAL\Connection lastInsertId
  284. *
  285. * Call this method right after the insert command or other functions may
  286. * cause trouble!
  287. */
  288. public static function insertid($table=null) {
  289. self::connect();
  290. return self::$connection->lastInsertId($table);
  291. }
  292. /**
  293. * @brief Insert a row if a matching row doesn't exists.
  294. * @param string $table. The table to insert into in the form '*PREFIX*tableName'
  295. * @param array $input. An array of fieldname/value pairs
  296. * @return int number of updated rows
  297. */
  298. public static function insertIfNotExist($table, $input) {
  299. self::connect();
  300. return self::$connection->insertIfNotExist($table, $input);
  301. }
  302. /**
  303. * Start a transaction
  304. */
  305. public static function beginTransaction() {
  306. self::connect();
  307. self::$connection->beginTransaction();
  308. }
  309. /**
  310. * Commit the database changes done during a transaction that is in progress
  311. */
  312. public static function commit() {
  313. self::connect();
  314. self::$connection->commit();
  315. }
  316. /**
  317. * @brief saves database schema to xml file
  318. * @param string $file name of file
  319. * @param int $mode
  320. * @return bool
  321. *
  322. * TODO: write more documentation
  323. */
  324. public static function getDbStructure( $file, $mode = 0) {
  325. $schemaManager = self::getMDB2SchemaManager();
  326. return $schemaManager->getDbStructure($file);
  327. }
  328. /**
  329. * @brief Creates tables from XML file
  330. * @param string $file file to read structure from
  331. * @return bool
  332. *
  333. * TODO: write more documentation
  334. */
  335. public static function createDbFromStructure( $file ) {
  336. $schemaManager = self::getMDB2SchemaManager();
  337. $result = $schemaManager->createDbFromStructure($file);
  338. return $result;
  339. }
  340. /**
  341. * @brief update the database schema
  342. * @param string $file file to read structure from
  343. * @throws Exception
  344. * @return bool
  345. */
  346. public static function updateDbFromStructure($file) {
  347. $schemaManager = self::getMDB2SchemaManager();
  348. try {
  349. $result = $schemaManager->updateDbFromStructure($file);
  350. } catch (Exception $e) {
  351. OC_Log::write('core', 'Failed to update database structure ('.$e.')', OC_Log::FATAL);
  352. throw $e;
  353. }
  354. return $result;
  355. }
  356. /**
  357. * @brief drop a table
  358. * @param string $tableName the table to drop
  359. */
  360. public static function dropTable($tableName) {
  361. $schemaManager = self::getMDB2SchemaManager();
  362. $schemaManager->dropTable($tableName);
  363. }
  364. /**
  365. * remove all tables defined in a database structure xml file
  366. * @param string $file the xml file describing the tables
  367. */
  368. public static function removeDBStructure($file) {
  369. $schemaManager = self::getMDB2SchemaManager();
  370. $schemaManager->removeDBStructure($file);
  371. }
  372. /**
  373. * @brief replaces the ownCloud tables with a new set
  374. * @param $file string path to the MDB2 xml db export file
  375. */
  376. public static function replaceDB( $file ) {
  377. $schemaManager = self::getMDB2SchemaManager();
  378. $schemaManager->replaceDB($file);
  379. }
  380. /**
  381. * check if a result is an error, works with Doctrine
  382. * @param mixed $result
  383. * @return bool
  384. */
  385. public static function isError($result) {
  386. //Doctrine returns false on error (and throws an exception)
  387. return $result === false;
  388. }
  389. /**
  390. * check if a result is an error and throws an exception, works with \Doctrine\DBAL\DBALException
  391. * @param mixed $result
  392. * @param string $message
  393. * @return void
  394. * @throws DatabaseException
  395. */
  396. public static function raiseExceptionOnError($result, $message = null) {
  397. if(self::isError($result)) {
  398. if ($message === null) {
  399. $message = self::getErrorMessage($result);
  400. } else {
  401. $message .= ', Root cause:' . self::getErrorMessage($result);
  402. }
  403. throw new DatabaseException($message, self::getErrorCode($result));
  404. }
  405. }
  406. public static function getErrorCode($error) {
  407. $code = self::$connection->errorCode();
  408. return $code;
  409. }
  410. /**
  411. * returns the error code and message as a string for logging
  412. * works with DoctrineException
  413. * @param mixed $error
  414. * @return string
  415. */
  416. public static function getErrorMessage($error) {
  417. if (self::$connection) {
  418. return self::$connection->getError();
  419. }
  420. return '';
  421. }
  422. /**
  423. * @param bool $enabled
  424. */
  425. static public function enableCaching($enabled) {
  426. if ($enabled) {
  427. self::$connection->enableQueryStatementCaching();
  428. } else {
  429. self::$connection->disableQueryStatementCaching();
  430. }
  431. }
  432. }