database.lib.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * This is the main database library for Chamilo.
  5. * Include/require it in your code to use its functionality.
  6. *
  7. * This library now uses a Doctrine DBAL Silex service provider
  8. *
  9. * @package chamilo.library
  10. */
  11. /**
  12. * Database class definition
  13. * @package chamilo.database
  14. */
  15. class Database
  16. {
  17. /**
  18. * The main connection
  19. *
  20. * @var \Doctrine\DBAL\Connection
  21. */
  22. private static $db;
  23. /**
  24. * Read connection
  25. *
  26. * @var \Doctrine\DBAL\Connection
  27. */
  28. private static $connectionRead;
  29. /**
  30. * Write connection
  31. *
  32. * @var \Doctrine\DBAL\Connection
  33. */
  34. private static $connectionWrite;
  35. /**
  36. * Constructor
  37. *
  38. * @param $db \Doctrine\DBAL\Connection
  39. * @param array $dbs
  40. */
  41. public function __construct($db, $dbs)
  42. {
  43. self::setDatabase($db, $dbs);
  44. }
  45. /**
  46. * @param $db
  47. * @param $dbs
  48. */
  49. public function setDatabase($db, $dbs)
  50. {
  51. self::$db = $db;
  52. // Using read/write connections see the services.php file
  53. self::$connectionRead = isset($dbs['db_read']) ? $dbs['db_read'] : $db;
  54. self::$connectionWrite = isset($dbs['db_write']) ? $dbs['db_write'] : $db;
  55. }
  56. /**
  57. * Return current connection
  58. * @return \Doctrine\DBAL\Connection
  59. */
  60. public function getConnection()
  61. {
  62. return self::$db;
  63. }
  64. /* Variable use only in the installation process to log errors. See the Database::query function */
  65. // static $log_queries = false;
  66. /**
  67. * Returns the name of the main database.
  68. * @return string
  69. */
  70. public static function get_main_database()
  71. {
  72. return self::$db->getDatabase();
  73. }
  74. /**
  75. * The glue is the string needed between database and table.
  76. * The trick is: in multiple databases, this is a period (with backticks).
  77. * In single database, this can be e.g. an underscore so we just fake
  78. * there are multiple databases and the code can be written independent
  79. * of the single / multiple database setting.
  80. * @return string
  81. */
  82. public static function get_database_glue()
  83. {
  84. return `.`;
  85. }
  86. /*
  87. Table name methods
  88. Use these methods to get table names for queries,
  89. instead of constructing them yourself.
  90. Backticks automatically surround the result,
  91. e.g. COURSE_NAME.link
  92. so the queries can look cleaner.
  93. Example:
  94. $table = Database::get_course_table(TABLE_DOCUMENT);
  95. $sql_query = "SELECT * FROM $table WHERE $condition";
  96. $sql_result = Database::query($sql_query);
  97. $result = Database::fetch_array($sql_result);
  98. */
  99. /**
  100. * This function returns the correct complete name of any table of the main
  101. * database of which you pass the short name as a parameter.
  102. * Define table names as constants in this library and use them
  103. * instead of directly using magic words in your tool code.
  104. *
  105. * @param string $short_table_name, the name of the table
  106. * @return string
  107. */
  108. public static function get_main_table($short_table_name)
  109. {
  110. return self::format_table_name(self::get_main_database(), $short_table_name);
  111. }
  112. /**
  113. * This method returns the correct complete name of any course table of
  114. * which you pass the short name as a parameter.
  115. * Define table names as constants in this library and use them
  116. * instead of directly using magic words in your tool code.
  117. *
  118. * @param string $short_table_name, the name of the table
  119. * @return string
  120. *
  121. */
  122. public static function get_course_table($short_table_name)
  123. {
  124. return self::format_table_name(self::get_main_database(), DB_COURSE_PREFIX.$short_table_name);
  125. }
  126. /**
  127. * Returns the number of affected rows in the last database operation.
  128. * @param \Doctrine\DBAL\Driver\Statement $result
  129. * @return int
  130. */
  131. public static function affected_rows(\Doctrine\DBAL\Driver\Statement $result = null)
  132. {
  133. return $result->rowCount();
  134. //return self::use_default_connection($connection) ? mysql_affected_rows() : mysql_affected_rows($connection);
  135. }
  136. /**
  137. * Gets the array from a SQL result (as returned by Database::query) - help achieving database independence
  138. * @param resource The result from a call to sql_query (e.g. Database::query)
  139. * @param string Optional: "ASSOC","NUM" or "BOTH", as the constant used in mysql_fetch_array.
  140. * @return array Array of results as returned by php
  141. * @author Yannick Warnier <yannick.warnier@beeznest.com>
  142. */
  143. public static function fetch_array(\Doctrine\DBAL\Driver\Statement $result, $option = 'BOTH')
  144. {
  145. if ($result === false) {
  146. return array();
  147. }
  148. return $result->fetch(self::customOptionToDoctrineOption($option));
  149. /*return $option == 'ASSOC' ? mysql_fetch_array($result, MYSQL_ASSOC) : ($option == 'NUM' ? mysql_fetch_array(
  150. $result,
  151. MYSQL_NUM
  152. ) : mysql_fetch_array($result));*/
  153. }
  154. /**
  155. * Gets an associative array from a SQL result (as returned by Database::query).
  156. * This method is equivalent to calling Database::fetch_array() with 'ASSOC' value for the optional second parameter.
  157. * @param resource $result The result from a call to sql_query (e.g. Database::query).
  158. * @return array Returns an associative array that corresponds to the fetched row and moves the internal data pointer ahead.
  159. */
  160. public static function fetch_assoc(\Doctrine\DBAL\Driver\Statement $result)
  161. {
  162. return $result->fetch(PDO::FETCH_ASSOC);
  163. //return mysql_fetch_assoc($result);
  164. }
  165. /**
  166. * Gets the next row of the result of the SQL query (as returned by Database::query) in an object form
  167. * @param \Doctrine\DBAL\Driver\Statement The result from a call to Database::query())
  168. * @param string Optional class name to instanciate
  169. * @param array Optional array of parameters
  170. * @return object Object of class StdClass or the required class, containing the query result row
  171. * @author Yannick Warnier <yannick.warnier@dokeos.com>
  172. */
  173. public static function fetch_object(\Doctrine\DBAL\Driver\Statement $result)
  174. {
  175. // Waiting for http://www.doctrine-project.org/jira/browse/DBAL-544 in order to know which constant use.
  176. //return $result->fetch(\Doctrine\ORM\Query::HYDRATE_OBJECT);
  177. return $result->fetch(PDO::FETCH_OBJ);
  178. /*return !empty($class) ? (is_array($params) ? mysql_fetch_object($result, $class, $params) : mysql_fetch_object(
  179. $result,
  180. $class
  181. )) : mysql_fetch_object($result);*/
  182. }
  183. /**
  184. * Gets the array from a SQL result (as returned by Database::query) - help achieving database independence
  185. * @param \Doctrine\DBAL\Driver\Statement The result from a call to Database::query())
  186. * @return array Array of results as returned by php
  187. */
  188. public static function fetch_row(\Doctrine\DBAL\Driver\Statement $result)
  189. {
  190. return $result->fetch(PDO::FETCH_NUM);
  191. //return mysql_fetch_row($result);
  192. }
  193. /**
  194. * Gets the ID of the last item inserted into the database
  195. * @return int The last ID as returned by the DB function
  196. */
  197. public static function insert_id()
  198. {
  199. return self::$connectionWrite->lastInsertId();
  200. }
  201. /**
  202. * Gets the number of rows from the last query result - help achieving database independence
  203. * @param \Doctrine\DBAL\Driver\Statement
  204. * @return integer The number of rows contained in this result
  205. **/
  206. public static function num_rows(\Doctrine\DBAL\Driver\Statement $result)
  207. {
  208. return $result->rowCount();
  209. }
  210. /**
  211. * Acts as the relative *_result() function of most DB drivers and fetches a
  212. * specific line and a field
  213. * @param \Doctrine\DBAL\Driver\Statement The database resource to get data from
  214. * @param integer The row number
  215. * @param string Optional field name or number
  216. * @return mixed One cell of the result, or FALSE on error
  217. */
  218. public static function result(\Doctrine\DBAL\Driver\Statement $resource, $row, $field = 0)
  219. {
  220. if ($resource->rowCount() > 0) {
  221. $result = $resource->fetchAll(PDO::FETCH_BOTH);
  222. return $result[$row][$field];
  223. }
  224. return null;
  225. }
  226. /**
  227. * Frees all the memory associated with the provided result identifier.
  228. * @return bool Returns TRUE on success or FALSE on failure.
  229. * Notes: Use this method if you are concerned about how much memory is being used for queries that return large result sets.
  230. * Anyway, all associated result memory is automatically freed at the end of the script's execution.
  231. */
  232. public static function free_result(\Doctrine\DBAL\Driver\Statement $result)
  233. {
  234. $result->closeCursor();
  235. //return mysql_free_result($result);
  236. }
  237. /**
  238. * Detects if a query is going to modify something in the database in order to use the write connection
  239. * @param string $query
  240. * @return bool
  241. */
  242. public static function isWriteQuery($query)
  243. {
  244. $isWriteQuery = preg_match("/UPDATE(.*) FROM/i", $query) ||
  245. preg_match("/INSERT INTO/i", $query) ||
  246. preg_match("/REPLACE INTO/i", $query) ||
  247. preg_match("/DELETE FROM/i", $query);
  248. return $isWriteQuery;
  249. }
  250. /**
  251. * Escapes a string to insert into the database as text
  252. * @param string The string to escape
  253. * @return string The escaped string
  254. */
  255. public static function escape_string($string)
  256. {
  257. /* The pdo::quote function adds a "'" character we need to remove that '
  258. because in Chamilo, developers builds a query like this:
  259. $sql = "SELECT * FROM $table WHERE id = 'Database::escape_string($id)'";
  260. otherwise we will have an error because the query will be:
  261. SELECT * FROM user WHERE id = ''1'' instead of
  262. SELECT * FROM user WHERE id = '1'
  263. */
  264. // $string = '_@_'.self::$db->quote($string).'_@_';
  265. $string = self::$db->quote($string);
  266. return trim($string, "'");
  267. return $string;
  268. }
  269. /**
  270. * Executes a query in the database
  271. * @author Julio Montoya
  272. * @param string $query The SQL query
  273. * @return \Doctrine\DBAL\Driver\Statement
  274. */
  275. public static function query($query)
  276. {
  277. $isWriteQuery = self::isWriteQuery($query);
  278. if ($isWriteQuery) {
  279. $connection = self::$connectionWrite;
  280. } else {
  281. $connection = self::$connectionRead;
  282. }
  283. /* The solution below does not work because there are some case where we use the "LIKE" option like this:
  284. $sql = 'SELECT * FROM user WHERE id LIKE "%'.Database::escape_string($id).' %" ;
  285. Chamilo queries are formed in many ways:
  286. $sql = "SELECT * FROM user WHERE id = '".Database::escape_string($id)."'; or
  287. $sql = 'SELECT * FROM user WHERE id = '.Database::escape_string($id).';
  288. The problem here is that the function escape_string() calls the quote function that adds a "'" string.
  289. Instead of this we're adding a identifier __@__ so we can identify those cases and replace with a simple '
  290. */
  291. //var_dump($query);
  292. /*$query = str_replace(
  293. array(
  294. "\"_@_'",
  295. "'_@_\"",
  296. "'_@_'",
  297. "_@_'",
  298. "'_@_",
  299. ),
  300. "'",
  301. $query
  302. );*/
  303. //var_dump($query);
  304. return $connection->executeQuery($query);
  305. }
  306. public static function customOptionToDoctrineOption($option)
  307. {
  308. switch($option) {
  309. case 'ASSOC':
  310. return PDO::FETCH_ASSOC;
  311. break;
  312. case 'NUM':
  313. return PDO::FETCH_NUM;
  314. break;
  315. case 'BOTH':
  316. default:
  317. return PDO::FETCH_BOTH;
  318. break;
  319. }
  320. }
  321. /**
  322. * Stores a query result into an array.
  323. * @param \Doctrine\DBAL\Driver\Statement $result - the return value of the query
  324. * @param option BOTH, ASSOC, or NUM
  325. * @return array - the value returned by the query
  326. */
  327. public static function store_result(\Doctrine\DBAL\Driver\Statement $result, $option = 'BOTH')
  328. {
  329. return $result->fetchAll(self::customOptionToDoctrineOption($option));
  330. /*
  331. var_dump($a );
  332. $array = array();
  333. if ($result !== false) { // For isolation from database engine's behaviour.
  334. while ($row = self::fetch_array($result, $option)) {
  335. $array[] = $row;
  336. }
  337. }
  338. return $array;*/
  339. }
  340. /*
  341. Private methods
  342. You should not access these from outside the class
  343. No effort is made to keep the names / results the same.
  344. */
  345. /**
  346. * Structures a database and table name to ready them
  347. * for querying. The database parameter is considered not glued,
  348. * just plain e.g. COURSE001
  349. * @todo not sure if we need this now
  350. */
  351. private static function format_table_name($database, $table)
  352. {
  353. /*$glue = '`.`';
  354. $table_name = '`'.$database.$glue.$table.'`';
  355. */
  356. return $table;
  357. //return $table_name;
  358. }
  359. /*
  360. New useful DB functions
  361. */
  362. /**
  363. * Executes an insert to in the database (dbal already escape strings)
  364. * @param string table name
  365. * @param array An array of field and values
  366. * @param bool show query
  367. * @return int the id of the latest executed query
  368. */
  369. public static function insert($table_name, $attributes, $show_query = false)
  370. {
  371. $result = self::$connectionWrite->insert($table_name, $attributes);
  372. if ($result) {
  373. return self::insert_id();
  374. }
  375. return false;
  376. }
  377. /**
  378. * Experimental useful database finder
  379. * @todo lot of stuff to do here
  380. * @todo known issues, it doesn't work when using LIKE conditions
  381. * @example array('where'=> array('course_code LIKE "?%"'))
  382. * @example array('where'=> array('type = ? AND category = ?' => array('setting', 'Plugins'))
  383. * @example array('where'=> array('name = "Julio" AND lastname = "montoya"))
  384. */
  385. public static function select($columns, $table_name, $conditions = array(), $type_result = 'all', $option = 'ASSOC')
  386. {
  387. //$qb = self::$db->createQueryBuilder();
  388. $conditions = self::parse_conditions($conditions);
  389. //@todo we could do a describe here to check the columns ...
  390. $clean_columns = '';
  391. if (is_array($columns)) {
  392. $clean_columns = implode(',', $columns);
  393. } else {
  394. if ($columns == '*') {
  395. $clean_columns = '*';
  396. } else {
  397. $clean_columns = (string)$columns;
  398. }
  399. }
  400. /*$qb->select($clean_columns);
  401. $qb->from($table_name, 'table');
  402. $qb->orderBy('table.' . $sort_order, 'ASC');*/
  403. $sql = "SELECT $clean_columns FROM $table_name $conditions";
  404. //var_dump($sql);
  405. $result = self::query($sql);
  406. $array = array();
  407. if ($type_result == 'all') {
  408. while ($row = self::fetch_array($result, $option)) {
  409. if (isset($row['id'])) {
  410. $array[$row['id']] = $row;
  411. } else {
  412. $array[] = $row;
  413. }
  414. }
  415. } else {
  416. $array = self::fetch_array($result, $option);
  417. }
  418. return $array;
  419. }
  420. /**
  421. * Parses WHERE/ORDER conditions i.e array('where'=>array('id = ?' =>'4'), 'order'=>'id DESC'))
  422. * @todo known issues, it doesn't work when using LIKE conditions example: array('where'=>array('course_code LIKE "?%"'))
  423. * @param array
  424. * @return string
  425. * @todo lot of stuff to do here
  426. */
  427. static function parse_conditions($conditions)
  428. {
  429. if (empty($conditions)) {
  430. return '';
  431. }
  432. $return_value = $where_return = '';
  433. foreach ($conditions as $type_condition => $condition_data) {
  434. if ($condition_data == false) {
  435. continue;
  436. }
  437. $type_condition = strtolower($type_condition);
  438. switch ($type_condition) {
  439. case 'where':
  440. foreach ($condition_data as $condition => $value_array) {
  441. if (is_array($value_array)) {
  442. $clean_values = array();
  443. foreach ($value_array as $item) {
  444. $item = Database::escape_string($item);
  445. $clean_values[] = $item;
  446. }
  447. } else {
  448. $value_array = Database::escape_string($value_array);
  449. $clean_values = $value_array;
  450. }
  451. if (!empty($condition) && $clean_values != '') {
  452. $condition = str_replace('%', "'@percentage@'", $condition); //replace "%"
  453. $condition = str_replace("'?'", "%s", $condition);
  454. $condition = str_replace("?", "%s", $condition);
  455. $condition = str_replace("@%s@", "@-@", $condition);
  456. $condition = str_replace("%s", "'%s'", $condition);
  457. $condition = str_replace("@-@", "@%s@", $condition);
  458. //Treat conditons as string
  459. $condition = vsprintf($condition, $clean_values);
  460. $condition = str_replace('@percentage@', '%', $condition); //replace "%"
  461. $where_return .= $condition;
  462. }
  463. }
  464. if (!empty($where_return)) {
  465. $return_value = " WHERE $where_return";
  466. }
  467. break;
  468. case 'order':
  469. $order_array = $condition_data;
  470. if (!empty($order_array)) {
  471. // 'order' => 'id desc, name desc'
  472. $order_array = $order_array;
  473. $new_order_array = explode(',', $order_array);
  474. $temp_value = array();
  475. foreach ($new_order_array as $element) {
  476. $element = explode(' ', $element);
  477. $element = array_filter($element);
  478. $element = array_values($element);
  479. if (!empty($element[1])) {
  480. $element[1] = strtolower($element[1]);
  481. $order = 'DESC';
  482. if (in_array($element[1], array('desc', 'asc'))) {
  483. $order = $element[1];
  484. }
  485. $temp_value[] = $element[0].' '.$order.' ';
  486. } else {
  487. //by default DESC
  488. $temp_value[] = $element[0].' DESC ';
  489. }
  490. }
  491. if (!empty($temp_value)) {
  492. $return_value .= ' ORDER BY '.implode(', ', $temp_value);
  493. }
  494. }
  495. break;
  496. case 'limit':
  497. $limit_array = explode(',', $condition_data);
  498. if (!empty($limit_array)) {
  499. if (count($limit_array) > 1) {
  500. $return_value .= ' LIMIT '.intval($limit_array[0]).' , '.intval($limit_array[1]);
  501. } else {
  502. $return_value .= ' LIMIT '.intval($limit_array[0]);
  503. }
  504. }
  505. break;
  506. }
  507. }
  508. return $return_value;
  509. }
  510. /**
  511. * @param array $conditions
  512. * @return string
  513. */
  514. public static function parse_where_conditions($conditions)
  515. {
  516. return self::parse_conditions(array('where' => $conditions));
  517. }
  518. /**
  519. * Deletes an item depending of conditions
  520. * @param string $table_name
  521. * @param array $where_conditions
  522. * @param bool $show_query
  523. * @return int
  524. */
  525. public static function delete($table_name, $where_conditions, $show_query = false)
  526. {
  527. //return self::$connectionWrite->delete($table_name, $where_conditions);
  528. $where_return = self::parse_where_conditions($where_conditions);
  529. $sql = "DELETE FROM $table_name $where_return ";
  530. if ($show_query) {
  531. echo $sql;
  532. echo '<br />';
  533. }
  534. $result = self::query($sql);
  535. $affected_rows = self::affected_rows($result);
  536. //@todo should return affected_rows for
  537. return $affected_rows;
  538. }
  539. /**
  540. * Experimental useful database update
  541. * @param string table name use Database::get_main_table
  542. * @param array array with values to updates, keys are the fields in the database:
  543. * @example: $params['name'] = 'Julio'; $params['lastname'] = 'Montoya';
  544. * @param array where conditions i.e array('id = ?' =>'4')
  545. * @param bool show query
  546. * @todo lot of stuff to do here
  547. */
  548. public static function update($table_name, $attributes, $where_conditions = array(), $show_query = false)
  549. {
  550. if (!empty($table_name) && !empty($attributes)) {
  551. $update_sql = '';
  552. //Cleaning attributes
  553. $count = 1;
  554. foreach ($attributes as $key => $value) {
  555. if (!is_array($value)) {
  556. $value = self::escape_string($value);
  557. }
  558. $update_sql .= "$key = '$value' ";
  559. if ($count < count($attributes)) {
  560. $update_sql .= ', ';
  561. }
  562. $count++;
  563. }
  564. if (!empty($update_sql)) {
  565. //Parsing and cleaning the where conditions
  566. $where_return = self::parse_where_conditions($where_conditions);
  567. $sql = "UPDATE $table_name SET $update_sql $where_return ";
  568. if ($show_query) {
  569. var_dump($sql);
  570. }
  571. $result = self::query($sql);
  572. $affected_rows = self::affected_rows($result);
  573. return $affected_rows;
  574. }
  575. }
  576. return false;
  577. }
  578. /**
  579. * Counts the number of rows in a table
  580. * @param string $table The table of which the rows should be counted
  581. * @return int The number of rows in the given table.
  582. */
  583. public static function count_rows($table)
  584. {
  585. $obj = self::fetch_object(self::query("SELECT COUNT(*) AS n FROM $table"));
  586. return $obj->n;
  587. }
  588. /**
  589. * Returns a list of tables within a database. The list may contain all of the
  590. * available table names or filtered table names by using a pattern.
  591. * @param string $database (optional) The name of the examined database.
  592. * @param string $pattern (optional) A pattern for filtering table names as if it was needed for the SQL's LIKE clause, for example 'access_%'.
  593. * @deprecated
  594. * @return array Returns in an array the retrieved list of table names.
  595. */
  596. public static function get_tables($database = '', $pattern = '')
  597. {
  598. $result = array();
  599. $query = "SHOW TABLES";
  600. if (!empty($database)) {
  601. $query .= " FROM `".self::escape_string($database)."`";
  602. }
  603. if (!empty($pattern)) {
  604. $query .= " LIKE '".self::escape_string($pattern)."'";
  605. }
  606. $query_result = Database::query($query);
  607. while ($row = Database::fetch_row($query_result)) {
  608. $result[] = $row[0];
  609. }
  610. return $result;
  611. }
  612. /**
  613. * Returns a list of databases created on the server. The list may contain all of the
  614. * available database names or filtered database names by using a pattern.
  615. * @return array Returns in an array the retrieved list of database names.
  616. */
  617. public static function get_databases()
  618. {
  619. $sm = self::$db->getSchemaManager();
  620. return $sm->listDatabases();
  621. }
  622. }