db.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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. break;
  82. case 'mysql':
  83. $connectionParams = array(
  84. 'user' => $user,
  85. 'password' => $pass,
  86. 'host' => $host,
  87. 'port' => $port,
  88. 'dbname' => $name,
  89. 'charset' => 'UTF8',
  90. 'driver' => 'pdo_mysql',
  91. );
  92. $connectionParams['adapter'] = '\OC\DB\Adapter';
  93. break;
  94. case 'pgsql':
  95. $connectionParams = array(
  96. 'user' => $user,
  97. 'password' => $pass,
  98. 'host' => $host,
  99. 'port' => $port,
  100. 'dbname' => $name,
  101. 'driver' => 'pdo_pgsql',
  102. );
  103. $connectionParams['adapter'] = '\OC\DB\AdapterPgSql';
  104. break;
  105. case 'oci':
  106. $connectionParams = array(
  107. 'user' => $user,
  108. 'password' => $pass,
  109. 'host' => $host,
  110. 'dbname' => $name,
  111. 'charset' => 'AL32UTF8',
  112. 'driver' => 'oci8',
  113. );
  114. if (!empty($port)) {
  115. $connectionParams['port'] = $port;
  116. }
  117. $connectionParams['adapter'] = '\OC\DB\AdapterOCI8';
  118. $eventManager->addEventSubscriber(new \Doctrine\DBAL\Event\Listeners\OracleSessionInit);
  119. break;
  120. case 'mssql':
  121. $connectionParams = array(
  122. 'user' => $user,
  123. 'password' => $pass,
  124. 'host' => $host,
  125. 'port' => $port,
  126. 'dbname' => $name,
  127. 'charset' => 'UTF8',
  128. 'driver' => 'pdo_sqlsrv',
  129. );
  130. $connectionParams['adapter'] = '\OC\DB\AdapterSQLSrv';
  131. break;
  132. default:
  133. return false;
  134. }
  135. $connectionParams['wrapperClass'] = 'OC\DB\Connection';
  136. $connectionParams['tablePrefix'] = OC_Config::getValue('dbtableprefix', 'oc_' );
  137. try {
  138. self::$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config, $eventManager);
  139. if ($type === 'sqlite' || $type === 'sqlite3') {
  140. // Sqlite doesn't handle query caching and schema changes
  141. // TODO: find a better way to handle this
  142. self::$connection->disableQueryStatementCaching();
  143. }
  144. } catch(\Doctrine\DBAL\DBALException $e) {
  145. OC_Log::write('core', $e->getMessage(), OC_Log::FATAL);
  146. OC_User::setUserId(null);
  147. // send http status 503
  148. header('HTTP/1.1 503 Service Temporarily Unavailable');
  149. header('Status: 503 Service Temporarily Unavailable');
  150. OC_Template::printErrorPage('Failed to connect to database');
  151. die();
  152. }
  153. }
  154. return true;
  155. }
  156. /**
  157. * @return \OC\DB\Connection
  158. */
  159. static public function getConnection() {
  160. self::connect();
  161. return self::$connection;
  162. }
  163. /**
  164. * get MDB2 schema manager
  165. *
  166. * @return \OC\DB\MDB2SchemaManager
  167. */
  168. private static function getMDB2SchemaManager()
  169. {
  170. return new \OC\DB\MDB2SchemaManager(self::getConnection());
  171. }
  172. /**
  173. * @brief Prepare a SQL query
  174. * @param string $query Query string
  175. * @param int $limit
  176. * @param int $offset
  177. * @param bool $isManipulation
  178. * @throws DatabaseException
  179. * @return \Doctrine\DBAL\Statement prepared SQL query
  180. *
  181. * SQL query via Doctrine prepare(), needs to be execute()'d!
  182. */
  183. static public function prepare( $query , $limit = null, $offset = null, $isManipulation = null) {
  184. self::connect();
  185. if ($isManipulation === null) {
  186. //try to guess, so we return the number of rows on manipulations
  187. $isManipulation = self::isManipulation($query);
  188. }
  189. // return the result
  190. try {
  191. $result = self::$connection->prepare($query, $limit, $offset);
  192. } catch (\Doctrine\DBAL\DBALException $e) {
  193. throw new \DatabaseException($e->getMessage(), $query);
  194. }
  195. // differentiate between query and manipulation
  196. $result = new OC_DB_StatementWrapper($result, $isManipulation);
  197. return $result;
  198. }
  199. /**
  200. * tries to guess the type of statement based on the first 10 characters
  201. * the current check allows some whitespace but does not work with IF EXISTS or other more complex statements
  202. *
  203. * @param string $sql
  204. * @return bool
  205. */
  206. static public function isManipulation( $sql ) {
  207. $selectOccurrence = stripos($sql, 'SELECT');
  208. if ($selectOccurrence !== false && $selectOccurrence < 10) {
  209. return false;
  210. }
  211. $insertOccurrence = stripos($sql, 'INSERT');
  212. if ($insertOccurrence !== false && $insertOccurrence < 10) {
  213. return true;
  214. }
  215. $updateOccurrence = stripos($sql, 'UPDATE');
  216. if ($updateOccurrence !== false && $updateOccurrence < 10) {
  217. return true;
  218. }
  219. $deleteOccurrence = stripos($sql, 'DELETE');
  220. if ($deleteOccurrence !== false && $deleteOccurrence < 10) {
  221. return true;
  222. }
  223. return false;
  224. }
  225. /**
  226. * @brief execute a prepared statement, on error write log and throw exception
  227. * @param mixed $stmt OC_DB_StatementWrapper,
  228. * an array with 'sql' and optionally 'limit' and 'offset' keys
  229. * .. or a simple sql query string
  230. * @param array $parameters
  231. * @return result
  232. * @throws DatabaseException
  233. */
  234. static public function executeAudited( $stmt, array $parameters = null) {
  235. if (is_string($stmt)) {
  236. // convert to an array with 'sql'
  237. if (stripos($stmt, 'LIMIT') !== false) { //OFFSET requires LIMIT, so we only need to check for LIMIT
  238. // TODO try to convert LIMIT OFFSET notation to parameters, see fixLimitClauseForMSSQL
  239. $message = 'LIMIT and OFFSET are forbidden for portability reasons,'
  240. . ' pass an array with \'limit\' and \'offset\' instead';
  241. throw new DatabaseException($message);
  242. }
  243. $stmt = array('sql' => $stmt, 'limit' => null, 'offset' => null);
  244. }
  245. if (is_array($stmt)) {
  246. // convert to prepared statement
  247. if ( ! array_key_exists('sql', $stmt) ) {
  248. $message = 'statement array must at least contain key \'sql\'';
  249. throw new DatabaseException($message);
  250. }
  251. if ( ! array_key_exists('limit', $stmt) ) {
  252. $stmt['limit'] = null;
  253. }
  254. if ( ! array_key_exists('limit', $stmt) ) {
  255. $stmt['offset'] = null;
  256. }
  257. $stmt = self::prepare($stmt['sql'], $stmt['limit'], $stmt['offset']);
  258. }
  259. self::raiseExceptionOnError($stmt, 'Could not prepare statement');
  260. if ($stmt instanceof OC_DB_StatementWrapper) {
  261. $result = $stmt->execute($parameters);
  262. self::raiseExceptionOnError($result, 'Could not execute statement');
  263. } else {
  264. if (is_object($stmt)) {
  265. $message = 'Expected a prepared statement or array got ' . get_class($stmt);
  266. } else {
  267. $message = 'Expected a prepared statement or array got ' . gettype($stmt);
  268. }
  269. throw new DatabaseException($message);
  270. }
  271. return $result;
  272. }
  273. /**
  274. * @brief gets last value of autoincrement
  275. * @param string $table The optional table name (will replace *PREFIX*) and add sequence suffix
  276. * @return int id
  277. * @throws DatabaseException
  278. *
  279. * \Doctrine\DBAL\Connection lastInsertId
  280. *
  281. * Call this method right after the insert command or other functions may
  282. * cause trouble!
  283. */
  284. public static function insertid($table=null) {
  285. self::connect();
  286. return self::$connection->lastInsertId($table);
  287. }
  288. /**
  289. * @brief Insert a row if a matching row doesn't exists.
  290. * @param string $table. The table to insert into in the form '*PREFIX*tableName'
  291. * @param array $input. An array of fieldname/value pairs
  292. * @return int number of updated rows
  293. */
  294. public static function insertIfNotExist($table, $input) {
  295. self::connect();
  296. return self::$connection->insertIfNotExist($table, $input);
  297. }
  298. /**
  299. * Start a transaction
  300. */
  301. public static function beginTransaction() {
  302. self::connect();
  303. self::$connection->beginTransaction();
  304. }
  305. /**
  306. * Commit the database changes done during a transaction that is in progress
  307. */
  308. public static function commit() {
  309. self::connect();
  310. self::$connection->commit();
  311. }
  312. /**
  313. * @brief saves database schema to xml file
  314. * @param string $file name of file
  315. * @param int $mode
  316. * @return bool
  317. *
  318. * TODO: write more documentation
  319. */
  320. public static function getDbStructure( $file, $mode = 0) {
  321. $schemaManager = self::getMDB2SchemaManager();
  322. return $schemaManager->getDbStructure($file);
  323. }
  324. /**
  325. * @brief Creates tables from XML file
  326. * @param string $file file to read structure from
  327. * @return bool
  328. *
  329. * TODO: write more documentation
  330. */
  331. public static function createDbFromStructure( $file ) {
  332. $schemaManager = self::getMDB2SchemaManager();
  333. $result = $schemaManager->createDbFromStructure($file);
  334. return $result;
  335. }
  336. /**
  337. * @brief update the database schema
  338. * @param string $file file to read structure from
  339. * @throws Exception
  340. * @return bool
  341. */
  342. public static function updateDbFromStructure($file) {
  343. $schemaManager = self::getMDB2SchemaManager();
  344. try {
  345. $result = $schemaManager->updateDbFromStructure($file);
  346. } catch (Exception $e) {
  347. OC_Log::write('core', 'Failed to update database structure ('.$e.')', OC_Log::FATAL);
  348. throw $e;
  349. }
  350. return $result;
  351. }
  352. /**
  353. * @brief drop a table
  354. * @param string $tableName the table to drop
  355. */
  356. public static function dropTable($tableName) {
  357. $schemaManager = self::getMDB2SchemaManager();
  358. $schemaManager->dropTable($tableName);
  359. }
  360. /**
  361. * remove all tables defined in a database structure xml file
  362. * @param string $file the xml file describing the tables
  363. */
  364. public static function removeDBStructure($file) {
  365. $schemaManager = self::getMDB2SchemaManager();
  366. $schemaManager->removeDBStructure($file);
  367. }
  368. /**
  369. * @brief replaces the ownCloud tables with a new set
  370. * @param $file string path to the MDB2 xml db export file
  371. */
  372. public static function replaceDB( $file ) {
  373. $schemaManager = self::getMDB2SchemaManager();
  374. $schemaManager->replaceDB($file);
  375. }
  376. /**
  377. * check if a result is an error, works with Doctrine
  378. * @param mixed $result
  379. * @return bool
  380. */
  381. public static function isError($result) {
  382. //Doctrine returns false on error (and throws an exception)
  383. return $result === false;
  384. }
  385. /**
  386. * check if a result is an error and throws an exception, works with \Doctrine\DBAL\DBALException
  387. * @param mixed $result
  388. * @param string $message
  389. * @return void
  390. * @throws DatabaseException
  391. */
  392. public static function raiseExceptionOnError($result, $message = null) {
  393. if(self::isError($result)) {
  394. if ($message === null) {
  395. $message = self::getErrorMessage($result);
  396. } else {
  397. $message .= ', Root cause:' . self::getErrorMessage($result);
  398. }
  399. throw new DatabaseException($message, self::getErrorCode($result));
  400. }
  401. }
  402. public static function getErrorCode($error) {
  403. $code = self::$connection->errorCode();
  404. return $code;
  405. }
  406. /**
  407. * returns the error code and message as a string for logging
  408. * works with DoctrineException
  409. * @param mixed $error
  410. * @return string
  411. */
  412. public static function getErrorMessage($error) {
  413. if (self::$connection) {
  414. return self::$connection->getError();
  415. }
  416. return '';
  417. }
  418. /**
  419. * @param bool $enabled
  420. */
  421. static public function enableCaching($enabled) {
  422. if ($enabled) {
  423. self::$connection->enableQueryStatementCaching();
  424. } else {
  425. self::$connection->disableQueryStatementCaching();
  426. }
  427. }
  428. }