db.php 13 KB

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