You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

db.php 14KB

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