database.lib.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. use Doctrine\Common\Annotations\AnnotationRegistry;
  4. use Doctrine\DBAL\Connection;
  5. use Doctrine\DBAL\Driver\Statement;
  6. use Doctrine\DBAL\Types\Type;
  7. use Doctrine\ORM\EntityManager;
  8. use Symfony\Component\Debug\ExceptionHandler;
  9. /**
  10. * Class Database.
  11. */
  12. class Database
  13. {
  14. /**
  15. * @var EntityManager
  16. */
  17. private static $em;
  18. private static $connection;
  19. /**
  20. * Only used by the installer.
  21. *
  22. * @param array $params
  23. * @param string $entityRootPath
  24. *
  25. * @throws \Doctrine\ORM\ORMException
  26. */
  27. public function connect(
  28. $params = [],
  29. $entityRootPath = ''
  30. ) {
  31. $config = self::getDoctrineConfig($entityRootPath);
  32. $config->setAutoGenerateProxyClasses(true);
  33. $config->setEntityNamespaces(
  34. [
  35. 'ChamiloUserBundle' => 'Chamilo\UserBundle\Entity',
  36. 'ChamiloCoreBundle' => 'Chamilo\CoreBundle\Entity',
  37. 'ChamiloCourseBundle' => 'Chamilo\CourseBundle\Entity',
  38. 'ChamiloSkillBundle' => 'Chamilo\SkillBundle\Entity',
  39. 'ChamiloTicketBundle' => 'Chamilo\TicketBundle\Entity',
  40. 'ChamiloPluginBundle' => 'Chamilo\PluginBundle\Entity',
  41. ]
  42. );
  43. $params['charset'] = 'utf8';
  44. $entityManager = EntityManager::create($params, $config);
  45. $connection = $entityManager->getConnection();
  46. $sysPath = !empty($sysPath) ? $sysPath : api_get_path(SYS_PATH);
  47. AnnotationRegistry::registerFile(
  48. $sysPath."vendor/symfony/doctrine-bridge/Validator/Constraints/UniqueEntity.php"
  49. );
  50. // Registering gedmo extensions
  51. AnnotationRegistry::registerAutoloadNamespace(
  52. 'Gedmo\Mapping\Annotation',
  53. $sysPath."vendor/gedmo/doctrine-extensions/lib"
  54. );
  55. $this->setConnection($connection);
  56. $this->setManager($entityManager);
  57. }
  58. /**
  59. * @param EntityManager $em
  60. */
  61. public static function setManager($em)
  62. {
  63. self::$em = $em;
  64. }
  65. /**
  66. * @param Connection $connection
  67. */
  68. public static function setConnection(Connection $connection)
  69. {
  70. self::$connection = $connection;
  71. }
  72. /**
  73. * @return Connection
  74. */
  75. public static function getConnection()
  76. {
  77. return self::$connection;
  78. }
  79. /**
  80. * @return EntityManager
  81. */
  82. public static function getManager()
  83. {
  84. return self::$em;
  85. }
  86. /**
  87. * Returns the name of the main database.
  88. *
  89. * @return string
  90. */
  91. public static function get_main_database()
  92. {
  93. return self::getManager()->getConnection()->getDatabase();
  94. }
  95. /**
  96. * Get main table.
  97. *
  98. * @param string $table
  99. *
  100. * @return string
  101. */
  102. public static function get_main_table($table)
  103. {
  104. return $table;
  105. }
  106. /**
  107. * Get course table.
  108. *
  109. * @param string $table
  110. *
  111. * @return string
  112. */
  113. public static function get_course_table($table)
  114. {
  115. return DB_COURSE_PREFIX.$table;
  116. }
  117. /**
  118. * Counts the number of rows in a table.
  119. *
  120. * @param string $table The table of which the rows should be counted
  121. *
  122. * @return int the number of rows in the given table
  123. *
  124. * @deprecated
  125. */
  126. public static function count_rows($table)
  127. {
  128. $obj = self::fetch_object(self::query("SELECT COUNT(*) AS n FROM $table"));
  129. return $obj->n;
  130. }
  131. /**
  132. * Returns the number of affected rows in the last database operation.
  133. *
  134. * @param Statement $result
  135. *
  136. * @return int
  137. */
  138. public static function affected_rows(Statement $result)
  139. {
  140. return $result->rowCount();
  141. }
  142. /**
  143. * Escapes a string to insert into the database as text.
  144. *
  145. * @param string $string
  146. *
  147. * @return string
  148. */
  149. public static function escape_string($string)
  150. {
  151. $string = self::getManager()->getConnection()->quote($string);
  152. // The quote method from PDO also adds quotes around the string, which
  153. // is not how the legacy mysql_real_escape_string() was used in
  154. // Chamilo, so we need to remove the quotes around. Using trim will
  155. // remove more than one quote if they are sequenced, generating
  156. // broken queries and SQL injection risks
  157. return substr($string, 1, -1);
  158. }
  159. /**
  160. * Gets the array from a SQL result (as returned by Database::query).
  161. *
  162. * @param Statement $result
  163. * @param string $option Optional: "ASSOC","NUM" or "BOTH"
  164. *
  165. * @return array|mixed
  166. */
  167. public static function fetch_array(Statement $result, $option = 'BOTH')
  168. {
  169. if ($result === false) {
  170. return [];
  171. }
  172. return $result->fetch(self::customOptionToDoctrineOption($option));
  173. }
  174. /**
  175. * Gets an associative array from a SQL result (as returned by Database::query).
  176. *
  177. * @param Statement $result
  178. *
  179. * @return array
  180. */
  181. public static function fetch_assoc(Statement $result)
  182. {
  183. return $result->fetch(PDO::FETCH_ASSOC);
  184. }
  185. /**
  186. * Gets the next row of the result of the SQL query
  187. * (as returned by Database::query) in an object form.
  188. *
  189. * @param Statement $result
  190. *
  191. * @return mixed
  192. */
  193. public static function fetch_object(Statement $result)
  194. {
  195. return $result->fetch(PDO::FETCH_OBJ);
  196. }
  197. /**
  198. * Gets the array from a SQL result (as returned by Database::query)
  199. * help achieving database independence.
  200. *
  201. * @param Statement $result
  202. *
  203. * @return mixed
  204. */
  205. public static function fetch_row(Statement $result)
  206. {
  207. if ($result === false) {
  208. return [];
  209. }
  210. return $result->fetch(PDO::FETCH_NUM);
  211. }
  212. /**
  213. * Gets the ID of the last item inserted into the database.
  214. *
  215. * @return string
  216. */
  217. public static function insert_id()
  218. {
  219. return self::getManager()->getConnection()->lastInsertId();
  220. }
  221. /**
  222. * @param Statement $result
  223. *
  224. * @return int
  225. */
  226. public static function num_rows(Statement $result)
  227. {
  228. if ($result === false) {
  229. return 0;
  230. }
  231. return $result->rowCount();
  232. }
  233. /**
  234. * Acts as the relative *_result() function of most DB drivers and fetches a
  235. * specific line and a field.
  236. *
  237. * @param Statement $resource
  238. * @param int $row
  239. * @param string $field
  240. *
  241. * @return mixed
  242. */
  243. public static function result(Statement $resource, $row, $field = '')
  244. {
  245. if ($resource->rowCount() > 0) {
  246. $result = $resource->fetchAll(PDO::FETCH_BOTH);
  247. return $result[$row][$field];
  248. }
  249. return false;
  250. }
  251. /**
  252. * @param string $query
  253. *
  254. * @return Statement
  255. */
  256. public static function query($query)
  257. {
  258. $connection = self::getManager()->getConnection();
  259. $result = null;
  260. try {
  261. $result = $connection->executeQuery($query);
  262. } catch (Exception $e) {
  263. self::handleError($e);
  264. }
  265. return $result;
  266. }
  267. /**
  268. * @param Exception $e
  269. */
  270. public static function handleError($e)
  271. {
  272. $debug = api_get_setting('server_type') == 'test';
  273. if ($debug) {
  274. // We use Symfony exception handler for better error information
  275. $handler = new ExceptionHandler();
  276. $handler->handle($e);
  277. exit;
  278. } else {
  279. error_log($e->getMessage());
  280. api_not_allowed(false, get_lang('An error has occured. Please contact your system administrator.'));
  281. exit;
  282. }
  283. }
  284. /**
  285. * @param string $option
  286. *
  287. * @return int
  288. */
  289. public static function customOptionToDoctrineOption($option)
  290. {
  291. switch ($option) {
  292. case 'ASSOC':
  293. return PDO::FETCH_ASSOC;
  294. break;
  295. case 'NUM':
  296. return PDO::FETCH_NUM;
  297. break;
  298. case 'BOTH':
  299. default:
  300. return PDO::FETCH_BOTH;
  301. break;
  302. }
  303. }
  304. /**
  305. * Stores a query result into an array.
  306. *
  307. * @author Olivier Brouckaert
  308. *
  309. * @param Statement $result - the return value of the query
  310. * @param string $option BOTH, ASSOC, or NUM
  311. *
  312. * @return array - the value returned by the query
  313. */
  314. public static function store_result(Statement $result, $option = 'BOTH')
  315. {
  316. return $result->fetchAll(self::customOptionToDoctrineOption($option));
  317. }
  318. /**
  319. * Database insert.
  320. *
  321. * @param string $table_name
  322. * @param array $attributes
  323. * @param bool $show_query
  324. *
  325. * @return false|int
  326. */
  327. public static function insert($table_name, $attributes, $show_query = false)
  328. {
  329. if (empty($attributes) || empty($table_name)) {
  330. return false;
  331. }
  332. $params = array_keys($attributes);
  333. if (!empty($params)) {
  334. $sql = 'INSERT INTO '.$table_name.' ('.implode(',', $params).')
  335. VALUES (:'.implode(', :', $params).')';
  336. if ($show_query) {
  337. var_dump($sql);
  338. error_log($sql);
  339. }
  340. $result = false;
  341. try {
  342. $statement = self::getConnection()->prepare($sql);
  343. $result = $statement->execute($attributes);
  344. } catch (Exception $e) {
  345. self::handleError($e);
  346. }
  347. if ($result) {
  348. return (int) self::getManager()->getConnection()->lastInsertId();
  349. }
  350. }
  351. return false;
  352. }
  353. /**
  354. * @param string $tableName use Database::get_main_table
  355. * @param array $attributes Values to updates
  356. * Example: $params['name'] = 'Julio'; $params['lastname'] = 'Montoya';
  357. * @param array $whereConditions where conditions i.e array('id = ?' =>'4')
  358. * @param bool $showQuery
  359. *
  360. * @return bool|int
  361. */
  362. public static function update(
  363. $tableName,
  364. $attributes,
  365. $whereConditions = [],
  366. $showQuery = false
  367. ) {
  368. if (!empty($tableName) && !empty($attributes)) {
  369. $updateSql = '';
  370. $count = 1;
  371. foreach ($attributes as $key => $value) {
  372. if ($showQuery) {
  373. echo $key.': '.$value.PHP_EOL;
  374. }
  375. $updateSql .= "$key = :$key ";
  376. if ($count < count($attributes)) {
  377. $updateSql .= ', ';
  378. }
  379. $count++;
  380. }
  381. if (!empty($updateSql)) {
  382. // Parsing and cleaning the where conditions
  383. $whereReturn = self::parse_where_conditions($whereConditions);
  384. $sql = "UPDATE $tableName SET $updateSql $whereReturn ";
  385. try {
  386. $statement = self::getManager()->getConnection()->prepare($sql);
  387. $result = $statement->execute($attributes);
  388. } catch (Exception $e) {
  389. self::handleError($e);
  390. }
  391. if ($showQuery) {
  392. var_dump($sql);
  393. var_dump($attributes);
  394. var_dump($whereConditions);
  395. }
  396. if ($result && $statement) {
  397. return $statement->rowCount();
  398. }
  399. }
  400. }
  401. return false;
  402. }
  403. /**
  404. * Experimental useful database finder.
  405. *
  406. * @todo lot of stuff to do here
  407. * @todo known issues, it doesn't work when using LIKE conditions
  408. *
  409. * @example array('where'=> array('course_code LIKE "?%"'))
  410. * @example array('where'=> array('type = ? AND category = ?' => array('setting', 'Plugins'))
  411. * @example array('where'=> array('name = "Julio" AND lastname = "montoya"'))
  412. *
  413. * @param array $columns
  414. * @param string $table_name
  415. * @param array $conditions
  416. * @param string $type_result
  417. * @param string $option
  418. * @param bool $debug
  419. *
  420. * @return array
  421. */
  422. public static function select(
  423. $columns,
  424. $table_name,
  425. $conditions = [],
  426. $type_result = 'all',
  427. $option = 'ASSOC',
  428. $debug = false
  429. ) {
  430. $conditions = self::parse_conditions($conditions);
  431. //@todo we could do a describe here to check the columns ...
  432. if (is_array($columns)) {
  433. $clean_columns = implode(',', $columns);
  434. } else {
  435. if ($columns == '*') {
  436. $clean_columns = '*';
  437. } else {
  438. $clean_columns = (string) $columns;
  439. }
  440. }
  441. $sql = "SELECT $clean_columns FROM $table_name $conditions";
  442. if ($debug) {
  443. var_dump($sql);
  444. }
  445. $result = self::query($sql);
  446. $array = [];
  447. if ($type_result === 'all') {
  448. while ($row = self::fetch_array($result, $option)) {
  449. if (isset($row['id'])) {
  450. $array[$row['id']] = $row;
  451. } else {
  452. $array[] = $row;
  453. }
  454. }
  455. } else {
  456. $array = self::fetch_array($result, $option);
  457. }
  458. return $array;
  459. }
  460. /**
  461. * Parses WHERE/ORDER conditions i.e array('where'=>array('id = ?' =>'4'), 'order'=>'id DESC').
  462. *
  463. * @todo known issues, it doesn't work when using
  464. * LIKE conditions example: array('where'=>array('course_code LIKE "?%"'))
  465. *
  466. * @param array $conditions
  467. *
  468. * @return string Partial SQL string to add to longer query
  469. */
  470. public static function parse_conditions($conditions)
  471. {
  472. if (empty($conditions)) {
  473. return '';
  474. }
  475. $return_value = $where_return = '';
  476. foreach ($conditions as $type_condition => $condition_data) {
  477. if ($condition_data == false) {
  478. continue;
  479. }
  480. $type_condition = strtolower($type_condition);
  481. switch ($type_condition) {
  482. case 'where':
  483. foreach ($condition_data as $condition => $value_array) {
  484. if (is_array($value_array)) {
  485. $clean_values = [];
  486. foreach ($value_array as $item) {
  487. $item = self::escape_string($item);
  488. $clean_values[] = $item;
  489. }
  490. } else {
  491. $value_array = self::escape_string($value_array);
  492. $clean_values = $value_array;
  493. }
  494. if (!empty($condition) && $clean_values != '') {
  495. $condition = str_replace('%', "'@percentage@'", $condition); //replace "%"
  496. $condition = str_replace("'?'", "%s", $condition);
  497. $condition = str_replace("?", "%s", $condition);
  498. $condition = str_replace("@%s@", "@-@", $condition);
  499. $condition = str_replace("%s", "'%s'", $condition);
  500. $condition = str_replace("@-@", "@%s@", $condition);
  501. // Treat conditions as string
  502. $condition = vsprintf($condition, $clean_values);
  503. $condition = str_replace('@percentage@', '%', $condition); //replace "%"
  504. $where_return .= $condition;
  505. }
  506. }
  507. if (!empty($where_return)) {
  508. $return_value = " WHERE $where_return";
  509. }
  510. break;
  511. case 'order':
  512. $order_array = $condition_data;
  513. if (!empty($order_array)) {
  514. // 'order' => 'id desc, name desc'
  515. $order_array = self::escape_string($order_array, null, false);
  516. $new_order_array = explode(',', $order_array);
  517. $temp_value = [];
  518. foreach ($new_order_array as $element) {
  519. $element = explode(' ', $element);
  520. $element = array_filter($element);
  521. $element = array_values($element);
  522. if (!empty($element[1])) {
  523. $element[1] = strtolower($element[1]);
  524. $order = 'DESC';
  525. if (in_array($element[1], ['desc', 'asc'])) {
  526. $order = $element[1];
  527. }
  528. $temp_value[] = $element[0].' '.$order.' ';
  529. } else {
  530. //by default DESC
  531. $temp_value[] = $element[0].' DESC ';
  532. }
  533. }
  534. if (!empty($temp_value)) {
  535. $return_value .= ' ORDER BY '.implode(', ', $temp_value);
  536. }
  537. }
  538. break;
  539. case 'limit':
  540. $limit_array = explode(',', $condition_data);
  541. if (!empty($limit_array)) {
  542. if (count($limit_array) > 1) {
  543. $return_value .= ' LIMIT '.intval($limit_array[0]).' , '.intval($limit_array[1]);
  544. } else {
  545. $return_value .= ' LIMIT '.intval($limit_array[0]);
  546. }
  547. }
  548. break;
  549. }
  550. }
  551. return $return_value;
  552. }
  553. /**
  554. * @param array $conditions
  555. *
  556. * @return string
  557. */
  558. public static function parse_where_conditions($conditions)
  559. {
  560. return self::parse_conditions(['where' => $conditions]);
  561. }
  562. /**
  563. * @param string $table_name
  564. * @param array $where_conditions
  565. * @param bool $show_query
  566. *
  567. * @return int
  568. */
  569. public static function delete($table_name, $where_conditions, $show_query = false)
  570. {
  571. $where_return = self::parse_where_conditions($where_conditions);
  572. $sql = "DELETE FROM $table_name $where_return ";
  573. if ($show_query) {
  574. echo $sql;
  575. echo '<br />';
  576. }
  577. $result = self::query($sql);
  578. $affected_rows = self::affected_rows($result);
  579. //@todo should return affected_rows for
  580. return $affected_rows;
  581. }
  582. /**
  583. * Get Doctrine configuration.
  584. *
  585. * @param string $path
  586. *
  587. * @return \Doctrine\ORM\Configuration
  588. */
  589. public static function getDoctrineConfig($path)
  590. {
  591. $isDevMode = true; // Forces doctrine to use ArrayCache instead of apc/xcache/memcache/redis
  592. $isSimpleMode = false; // related to annotations @Entity
  593. $cache = null;
  594. $path = !empty($path) ? $path : api_get_path(SYS_PATH);
  595. $paths = [
  596. //$path.'src/Chamilo/ClassificationBundle/Entity',
  597. //$path.'src/Chamilo/MediaBundle/Entity',
  598. //$path.'src/Chamilo/PageBundle/Entity',
  599. $path.'src/Chamilo/CoreBundle/Entity',
  600. $path.'src/Chamilo/UserBundle/Entity',
  601. $path.'src/Chamilo/CourseBundle/Entity',
  602. $path.'src/Chamilo/TicketBundle/Entity',
  603. $path.'src/Chamilo/SkillBundle/Entity',
  604. $path.'src/Chamilo/PluginBundle/Entity',
  605. //$path.'vendor/sonata-project/user-bundle/Entity',
  606. //$path.'vendor/sonata-project/user-bundle/Model',
  607. //$path.'vendor/friendsofsymfony/user-bundle/FOS/UserBundle/Entity',
  608. ];
  609. $proxyDir = $path.'var/cache/';
  610. $config = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(
  611. $paths,
  612. $isDevMode,
  613. $proxyDir,
  614. $cache,
  615. $isSimpleMode
  616. );
  617. return $config;
  618. }
  619. /**
  620. * @param string $table
  621. *
  622. * @return bool
  623. */
  624. public static function tableExists($table)
  625. {
  626. return self::getManager()->getConnection()->getSchemaManager()->tablesExist($table);
  627. }
  628. /**
  629. * @param string $table
  630. *
  631. * @return \Doctrine\DBAL\Schema\Column[]
  632. */
  633. public static function listTableColumns($table)
  634. {
  635. return self::getManager()->getConnection()->getSchemaManager()->listTableColumns($table);
  636. }
  637. }