Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
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.

1666 lines
42KB

  1. <?php
  2. namespace Friendica\Database;
  3. use Friendica\Core\Config\IConfigCache;
  4. use Friendica\Core\Logger;
  5. use Friendica\Core\System;
  6. use Friendica\Util\DateTimeFormat;
  7. use mysqli;
  8. use mysqli_result;
  9. use mysqli_stmt;
  10. use PDO;
  11. use PDOException;
  12. use PDOStatement;
  13. /**
  14. * @class MySQL database class
  15. *
  16. * This class is for the low level database stuff that does driver specific things.
  17. */
  18. class DBA
  19. {
  20. /**
  21. * Lowest possible date value
  22. */
  23. const NULL_DATE = '0001-01-01';
  24. /**
  25. * Lowest possible datetime value
  26. */
  27. const NULL_DATETIME = '0001-01-01 00:00:00';
  28. public static $connected = false;
  29. /**
  30. * @var IConfigCache
  31. */
  32. private static $config;
  33. private static $server_info = '';
  34. private static $connection;
  35. private static $driver;
  36. private static $error = false;
  37. private static $errorno = 0;
  38. private static $affected_rows = 0;
  39. private static $in_transaction = false;
  40. private static $in_retrial = false;
  41. private static $relation = [];
  42. private static $db_serveraddr = '';
  43. private static $db_user = '';
  44. private static $db_pass = '';
  45. private static $db_name = '';
  46. private static $db_charset = '';
  47. public static function connect($config, $serveraddr, $user, $pass, $db, $charset = null)
  48. {
  49. if (!is_null(self::$connection) && self::connected()) {
  50. return true;
  51. }
  52. // We are storing these values for being able to perform a reconnect
  53. self::$config = $config;
  54. self::$db_serveraddr = $serveraddr;
  55. self::$db_user = $user;
  56. self::$db_pass = $pass;
  57. self::$db_name = $db;
  58. self::$db_charset = $charset;
  59. $port = 0;
  60. $serveraddr = trim($serveraddr);
  61. $serverdata = explode(':', $serveraddr);
  62. $server = $serverdata[0];
  63. if (count($serverdata) > 1) {
  64. $port = trim($serverdata[1]);
  65. }
  66. $server = trim($server);
  67. $user = trim($user);
  68. $pass = trim($pass);
  69. $db = trim($db);
  70. $charset = trim($charset);
  71. if (!(strlen($server) && strlen($user))) {
  72. return false;
  73. }
  74. if (class_exists('\PDO') && in_array('mysql', PDO::getAvailableDrivers())) {
  75. self::$driver = 'pdo';
  76. $connect = "mysql:host=".$server.";dbname=".$db;
  77. if ($port > 0) {
  78. $connect .= ";port=".$port;
  79. }
  80. if ($charset) {
  81. $connect .= ";charset=".$charset;
  82. }
  83. try {
  84. self::$connection = @new PDO($connect, $user, $pass);
  85. self::$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
  86. self::$connected = true;
  87. } catch (PDOException $e) {
  88. /// @TODO At least log exception, don't ignore it!
  89. }
  90. }
  91. if (!self::$connected && class_exists('\mysqli')) {
  92. self::$driver = 'mysqli';
  93. if ($port > 0) {
  94. self::$connection = @new mysqli($server, $user, $pass, $db, $port);
  95. } else {
  96. self::$connection = @new mysqli($server, $user, $pass, $db);
  97. }
  98. if (!mysqli_connect_errno()) {
  99. self::$connected = true;
  100. if ($charset) {
  101. self::$connection->set_charset($charset);
  102. }
  103. }
  104. }
  105. // No suitable SQL driver was found.
  106. if (!self::$connected) {
  107. self::$driver = null;
  108. self::$connection = null;
  109. }
  110. return self::$connected;
  111. }
  112. /**
  113. * Disconnects the current database connection
  114. */
  115. public static function disconnect()
  116. {
  117. if (is_null(self::$connection)) {
  118. return;
  119. }
  120. switch (self::$driver) {
  121. case 'pdo':
  122. self::$connection = null;
  123. break;
  124. case 'mysqli':
  125. self::$connection->close();
  126. self::$connection = null;
  127. break;
  128. }
  129. }
  130. /**
  131. * Perform a reconnect of an existing database connection
  132. */
  133. public static function reconnect() {
  134. self::disconnect();
  135. $ret = self::connect(self::$config, self::$db_serveraddr, self::$db_user, self::$db_pass, self::$db_name, self::$db_charset);
  136. return $ret;
  137. }
  138. /**
  139. * Return the database object.
  140. * @return PDO|mysqli
  141. */
  142. public static function getConnection()
  143. {
  144. return self::$connection;
  145. }
  146. /**
  147. * @brief Returns the MySQL server version string
  148. *
  149. * This function discriminate between the deprecated mysql API and the current
  150. * object-oriented mysqli API. Example of returned string: 5.5.46-0+deb8u1
  151. *
  152. * @return string
  153. */
  154. public static function serverInfo() {
  155. if (self::$server_info == '') {
  156. switch (self::$driver) {
  157. case 'pdo':
  158. self::$server_info = self::$connection->getAttribute(PDO::ATTR_SERVER_VERSION);
  159. break;
  160. case 'mysqli':
  161. self::$server_info = self::$connection->server_info;
  162. break;
  163. }
  164. }
  165. return self::$server_info;
  166. }
  167. /**
  168. * @brief Returns the selected database name
  169. *
  170. * @return string
  171. * @throws \Exception
  172. */
  173. public static function databaseName() {
  174. $ret = self::p("SELECT DATABASE() AS `db`");
  175. $data = self::toArray($ret);
  176. return $data[0]['db'];
  177. }
  178. /**
  179. * @brief Analyze a database query and log this if some conditions are met.
  180. *
  181. * @param string $query The database query that will be analyzed
  182. * @throws \Exception
  183. */
  184. private static function logIndex($query) {
  185. if (!self::$config->get('system', 'db_log_index')) {
  186. return;
  187. }
  188. // Don't explain an explain statement
  189. if (strtolower(substr($query, 0, 7)) == "explain") {
  190. return;
  191. }
  192. // Only do the explain on "select", "update" and "delete"
  193. if (!in_array(strtolower(substr($query, 0, 6)), ["select", "update", "delete"])) {
  194. return;
  195. }
  196. $r = self::p("EXPLAIN ".$query);
  197. if (!self::isResult($r)) {
  198. return;
  199. }
  200. $watchlist = explode(',', self::$config->get('system', 'db_log_index_watch'));
  201. $blacklist = explode(',', self::$config->get('system', 'db_log_index_blacklist'));
  202. while ($row = self::fetch($r)) {
  203. if ((intval(self::$config->get('system', 'db_loglimit_index')) > 0)) {
  204. $log = (in_array($row['key'], $watchlist) &&
  205. ($row['rows'] >= intval(self::$config->get('system', 'db_loglimit_index'))));
  206. } else {
  207. $log = false;
  208. }
  209. if ((intval(self::$config->get('system', 'db_loglimit_index_high')) > 0) && ($row['rows'] >= intval($Config::getConfigValue('system', 'db_loglimit_index_high')))) {
  210. $log = true;
  211. }
  212. if (in_array($row['key'], $blacklist) || ($row['key'] == "")) {
  213. $log = false;
  214. }
  215. if ($log) {
  216. $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
  217. @file_put_contents(self::$config->get('system', 'db_log_index'), DateTimeFormat::utcNow()."\t".
  218. $row['key']."\t".$row['rows']."\t".$row['Extra']."\t".
  219. basename($backtrace[1]["file"])."\t".
  220. $backtrace[1]["line"]."\t".$backtrace[2]["function"]."\t".
  221. substr($query, 0, 2000)."\n", FILE_APPEND);
  222. }
  223. }
  224. }
  225. public static function escape($str) {
  226. if (self::$connected) {
  227. switch (self::$driver) {
  228. case 'pdo':
  229. return substr(@self::$connection->quote($str, PDO::PARAM_STR), 1, -1);
  230. case 'mysqli':
  231. return @self::$connection->real_escape_string($str);
  232. }
  233. } else {
  234. return str_replace("'", "\\'", $str);
  235. }
  236. }
  237. public static function connected() {
  238. $connected = false;
  239. if (is_null(self::$connection)) {
  240. return false;
  241. }
  242. switch (self::$driver) {
  243. case 'pdo':
  244. $r = self::p("SELECT 1");
  245. if (self::isResult($r)) {
  246. $row = self::toArray($r);
  247. $connected = ($row[0]['1'] == '1');
  248. }
  249. break;
  250. case 'mysqli':
  251. $connected = self::$connection->ping();
  252. break;
  253. }
  254. return $connected;
  255. }
  256. /**
  257. * @brief Replaces ANY_VALUE() function by MIN() function,
  258. * if the database server does not support ANY_VALUE().
  259. *
  260. * Considerations for Standard SQL, or MySQL with ONLY_FULL_GROUP_BY (default since 5.7.5).
  261. * ANY_VALUE() is available from MySQL 5.7.5 https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html
  262. * A standard fall-back is to use MIN().
  263. *
  264. * @param string $sql An SQL string without the values
  265. * @return string The input SQL string modified if necessary.
  266. */
  267. public static function anyValueFallback($sql) {
  268. $server_info = self::serverInfo();
  269. if (version_compare($server_info, '5.7.5', '<') ||
  270. (stripos($server_info, 'MariaDB') !== false)) {
  271. $sql = str_ireplace('ANY_VALUE(', 'MIN(', $sql);
  272. }
  273. return $sql;
  274. }
  275. /**
  276. * @brief beautifies the query - useful for "SHOW PROCESSLIST"
  277. *
  278. * This is safe when we bind the parameters later.
  279. * The parameter values aren't part of the SQL.
  280. *
  281. * @param string $sql An SQL string without the values
  282. * @return string The input SQL string modified if necessary.
  283. */
  284. public static function cleanQuery($sql) {
  285. $search = ["\t", "\n", "\r", " "];
  286. $replace = [' ', ' ', ' ', ' '];
  287. do {
  288. $oldsql = $sql;
  289. $sql = str_replace($search, $replace, $sql);
  290. } while ($oldsql != $sql);
  291. return $sql;
  292. }
  293. /**
  294. * @brief Replaces the ? placeholders with the parameters in the $args array
  295. *
  296. * @param string $sql SQL query
  297. * @param array $args The parameters that are to replace the ? placeholders
  298. * @return string The replaced SQL query
  299. */
  300. private static function replaceParameters($sql, $args) {
  301. $offset = 0;
  302. foreach ($args AS $param => $value) {
  303. if (is_int($args[$param]) || is_float($args[$param])) {
  304. $replace = intval($args[$param]);
  305. } else {
  306. $replace = "'".self::escape($args[$param])."'";
  307. }
  308. $pos = strpos($sql, '?', $offset);
  309. if ($pos !== false) {
  310. $sql = substr_replace($sql, $replace, $pos, 1);
  311. }
  312. $offset = $pos + strlen($replace);
  313. }
  314. return $sql;
  315. }
  316. /**
  317. * @brief Convert parameter array to an universal form
  318. * @param array $args Parameter array
  319. * @return array universalized parameter array
  320. */
  321. private static function getParam($args) {
  322. unset($args[0]);
  323. // When the second function parameter is an array then use this as the parameter array
  324. if ((count($args) > 0) && (is_array($args[1]))) {
  325. return $args[1];
  326. } else {
  327. return $args;
  328. }
  329. }
  330. /**
  331. * @brief Executes a prepared statement that returns data
  332. * @usage Example: $r = p("SELECT * FROM `item` WHERE `guid` = ?", $guid);
  333. *
  334. * Please only use it with complicated queries.
  335. * For all regular queries please use DBA::select or DBA::exists
  336. *
  337. * @param string $sql SQL statement
  338. * @return bool|object statement object or result object
  339. * @throws \Exception
  340. */
  341. public static function p($sql) {
  342. $a = \get_app();
  343. $stamp1 = microtime(true);
  344. $params = self::getParam(func_get_args());
  345. // Renumber the array keys to be sure that they fit
  346. $i = 0;
  347. $args = [];
  348. foreach ($params AS $param) {
  349. // Avoid problems with some MySQL servers and boolean values. See issue #3645
  350. if (is_bool($param)) {
  351. $param = (int)$param;
  352. }
  353. $args[++$i] = $param;
  354. }
  355. if (!self::$connected) {
  356. return false;
  357. }
  358. if ((substr_count($sql, '?') != count($args)) && (count($args) > 0)) {
  359. // Question: Should we continue or stop the query here?
  360. Logger::log('Parameter mismatch. Query "'.$sql.'" - Parameters '.print_r($args, true), Logger::DEBUG);
  361. }
  362. $sql = self::cleanQuery($sql);
  363. $sql = self::anyValueFallback($sql);
  364. $orig_sql = $sql;
  365. if (self::$config->get('system', 'db_callstack')) {
  366. $sql = "/*".System::callstack()." */ ".$sql;
  367. }
  368. self::$error = '';
  369. self::$errorno = 0;
  370. self::$affected_rows = 0;
  371. // We have to make some things different if this function is called from "e"
  372. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  373. if (isset($trace[1])) {
  374. $called_from = $trace[1];
  375. } else {
  376. // We use just something that is defined to avoid warnings
  377. $called_from = $trace[0];
  378. }
  379. // We are having an own error logging in the function "e"
  380. $called_from_e = ($called_from['function'] == 'e');
  381. switch (self::$driver) {
  382. case 'pdo':
  383. // If there are no arguments we use "query"
  384. if (count($args) == 0) {
  385. if (!$retval = self::$connection->query($sql)) {
  386. $errorInfo = self::$connection->errorInfo();
  387. self::$error = $errorInfo[2];
  388. self::$errorno = $errorInfo[1];
  389. $retval = false;
  390. break;
  391. }
  392. self::$affected_rows = $retval->rowCount();
  393. break;
  394. }
  395. if (!$stmt = self::$connection->prepare($sql)) {
  396. $errorInfo = self::$connection->errorInfo();
  397. self::$error = $errorInfo[2];
  398. self::$errorno = $errorInfo[1];
  399. $retval = false;
  400. break;
  401. }
  402. foreach ($args AS $param => $value) {
  403. if (is_int($args[$param])) {
  404. $data_type = PDO::PARAM_INT;
  405. } else {
  406. $data_type = PDO::PARAM_STR;
  407. }
  408. $stmt->bindParam($param, $args[$param], $data_type);
  409. }
  410. if (!$stmt->execute()) {
  411. $errorInfo = $stmt->errorInfo();
  412. self::$error = $errorInfo[2];
  413. self::$errorno = $errorInfo[1];
  414. $retval = false;
  415. } else {
  416. $retval = $stmt;
  417. self::$affected_rows = $retval->rowCount();
  418. }
  419. break;
  420. case 'mysqli':
  421. // There are SQL statements that cannot be executed with a prepared statement
  422. $parts = explode(' ', $orig_sql);
  423. $command = strtolower($parts[0]);
  424. $can_be_prepared = in_array($command, ['select', 'update', 'insert', 'delete']);
  425. // The fallback routine is called as well when there are no arguments
  426. if (!$can_be_prepared || (count($args) == 0)) {
  427. $retval = self::$connection->query(self::replaceParameters($sql, $args));
  428. if (self::$connection->errno) {
  429. self::$error = self::$connection->error;
  430. self::$errorno = self::$connection->errno;
  431. $retval = false;
  432. } else {
  433. if (isset($retval->num_rows)) {
  434. self::$affected_rows = $retval->num_rows;
  435. } else {
  436. self::$affected_rows = self::$connection->affected_rows;
  437. }
  438. }
  439. break;
  440. }
  441. $stmt = self::$connection->stmt_init();
  442. if (!$stmt->prepare($sql)) {
  443. self::$error = $stmt->error;
  444. self::$errorno = $stmt->errno;
  445. $retval = false;
  446. break;
  447. }
  448. $param_types = '';
  449. $values = [];
  450. foreach ($args AS $param => $value) {
  451. if (is_int($args[$param])) {
  452. $param_types .= 'i';
  453. } elseif (is_float($args[$param])) {
  454. $param_types .= 'd';
  455. } elseif (is_string($args[$param])) {
  456. $param_types .= 's';
  457. } else {
  458. $param_types .= 'b';
  459. }
  460. $values[] = &$args[$param];
  461. }
  462. if (count($values) > 0) {
  463. array_unshift($values, $param_types);
  464. call_user_func_array([$stmt, 'bind_param'], $values);
  465. }
  466. if (!$stmt->execute()) {
  467. self::$error = self::$connection->error;
  468. self::$errorno = self::$connection->errno;
  469. $retval = false;
  470. } else {
  471. $stmt->store_result();
  472. $retval = $stmt;
  473. self::$affected_rows = $retval->affected_rows;
  474. }
  475. break;
  476. }
  477. // We are having an own error logging in the function "e"
  478. if ((self::$errorno != 0) && !$called_from_e) {
  479. // We have to preserve the error code, somewhere in the logging it get lost
  480. $error = self::$error;
  481. $errorno = self::$errorno;
  482. Logger::log('DB Error '.self::$errorno.': '.self::$error."\n".
  483. System::callstack(8)."\n".self::replaceParameters($sql, $args));
  484. // On a lost connection we try to reconnect - but only once.
  485. if ($errorno == 2006) {
  486. if (self::$in_retrial || !self::reconnect()) {
  487. // It doesn't make sense to continue when the database connection was lost
  488. if (self::$in_retrial) {
  489. Logger::log('Giving up retrial because of database error '.$errorno.': '.$error);
  490. } else {
  491. Logger::log("Couldn't reconnect after database error ".$errorno.': '.$error);
  492. }
  493. exit(1);
  494. } else {
  495. // We try it again
  496. Logger::log('Reconnected after database error '.$errorno.': '.$error);
  497. self::$in_retrial = true;
  498. $ret = self::p($sql, $args);
  499. self::$in_retrial = false;
  500. return $ret;
  501. }
  502. }
  503. self::$error = $error;
  504. self::$errorno = $errorno;
  505. }
  506. $a->saveTimestamp($stamp1, 'database');
  507. if (self::$config->get('system', 'db_log')) {
  508. $stamp2 = microtime(true);
  509. $duration = (float)($stamp2 - $stamp1);
  510. if (($duration > self::$config->get('system', 'db_loglimit'))) {
  511. $duration = round($duration, 3);
  512. $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
  513. @file_put_contents(self::$config->get('system', 'db_log'), DateTimeFormat::utcNow()."\t".$duration."\t".
  514. basename($backtrace[1]["file"])."\t".
  515. $backtrace[1]["line"]."\t".$backtrace[2]["function"]."\t".
  516. substr(self::replaceParameters($sql, $args), 0, 2000)."\n", FILE_APPEND);
  517. }
  518. }
  519. return $retval;
  520. }
  521. /**
  522. * @brief Executes a prepared statement like UPDATE or INSERT that doesn't return data
  523. *
  524. * Please use DBA::delete, DBA::insert, DBA::update, ... instead
  525. *
  526. * @param string $sql SQL statement
  527. * @return boolean Was the query successfull? False is returned only if an error occurred
  528. * @throws \Exception
  529. */
  530. public static function e($sql) {
  531. $a = \get_app();
  532. $stamp = microtime(true);
  533. $params = self::getParam(func_get_args());
  534. // In a case of a deadlock we are repeating the query 20 times
  535. $timeout = 20;
  536. do {
  537. $stmt = self::p($sql, $params);
  538. if (is_bool($stmt)) {
  539. $retval = $stmt;
  540. } elseif (is_object($stmt)) {
  541. $retval = true;
  542. } else {
  543. $retval = false;
  544. }
  545. self::close($stmt);
  546. } while ((self::$errorno == 1213) && (--$timeout > 0));
  547. if (self::$errorno != 0) {
  548. // We have to preserve the error code, somewhere in the logging it get lost
  549. $error = self::$error;
  550. $errorno = self::$errorno;
  551. Logger::log('DB Error '.self::$errorno.': '.self::$error."\n".
  552. System::callstack(8)."\n".self::replaceParameters($sql, $params));
  553. // On a lost connection we simply quit.
  554. // A reconnect like in self::p could be dangerous with modifications
  555. if ($errorno == 2006) {
  556. Logger::log('Giving up because of database error '.$errorno.': '.$error);
  557. exit(1);
  558. }
  559. self::$error = $error;
  560. self::$errorno = $errorno;
  561. }
  562. $a->saveTimestamp($stamp, "database_write");
  563. return $retval;
  564. }
  565. /**
  566. * @brief Check if data exists
  567. *
  568. * @param string $table Table name
  569. * @param array $condition array of fields for condition
  570. *
  571. * @return boolean Are there rows for that condition?
  572. * @throws \Exception
  573. */
  574. public static function exists($table, $condition) {
  575. if (empty($table)) {
  576. return false;
  577. }
  578. $fields = [];
  579. if (empty($condition)) {
  580. return DBStructure::existsTable($table);
  581. }
  582. reset($condition);
  583. $first_key = key($condition);
  584. if (!is_int($first_key)) {
  585. $fields = [$first_key];
  586. }
  587. $stmt = self::select($table, $fields, $condition, ['limit' => 1]);
  588. if (is_bool($stmt)) {
  589. $retval = $stmt;
  590. } else {
  591. $retval = (self::numRows($stmt) > 0);
  592. }
  593. self::close($stmt);
  594. return $retval;
  595. }
  596. /**
  597. * Fetches the first row
  598. *
  599. * Please use DBA::selectFirst or DBA::exists whenever this is possible.
  600. *
  601. * @brief Fetches the first row
  602. * @param string $sql SQL statement
  603. * @return array first row of query
  604. * @throws \Exception
  605. */
  606. public static function fetchFirst($sql) {
  607. $params = self::getParam(func_get_args());
  608. $stmt = self::p($sql, $params);
  609. if (is_bool($stmt)) {
  610. $retval = $stmt;
  611. } else {
  612. $retval = self::fetch($stmt);
  613. }
  614. self::close($stmt);
  615. return $retval;
  616. }
  617. /**
  618. * @brief Returns the number of affected rows of the last statement
  619. *
  620. * @return int Number of rows
  621. */
  622. public static function affectedRows() {
  623. return self::$affected_rows;
  624. }
  625. /**
  626. * @brief Returns the number of columns of a statement
  627. *
  628. * @param object Statement object
  629. * @return int Number of columns
  630. */
  631. public static function columnCount($stmt) {
  632. if (!is_object($stmt)) {
  633. return 0;
  634. }
  635. switch (self::$driver) {
  636. case 'pdo':
  637. return $stmt->columnCount();
  638. case 'mysqli':
  639. return $stmt->field_count;
  640. }
  641. return 0;
  642. }
  643. /**
  644. * @brief Returns the number of rows of a statement
  645. *
  646. * @param PDOStatement|mysqli_result|mysqli_stmt Statement object
  647. * @return int Number of rows
  648. */
  649. public static function numRows($stmt) {
  650. if (!is_object($stmt)) {
  651. return 0;
  652. }
  653. switch (self::$driver) {
  654. case 'pdo':
  655. return $stmt->rowCount();
  656. case 'mysqli':
  657. return $stmt->num_rows;
  658. }
  659. return 0;
  660. }
  661. /**
  662. * @brief Fetch a single row
  663. *
  664. * @param mixed $stmt statement object
  665. * @return array current row
  666. */
  667. public static function fetch($stmt) {
  668. $a = \get_app();
  669. $stamp1 = microtime(true);
  670. $columns = [];
  671. if (!is_object($stmt)) {
  672. return false;
  673. }
  674. switch (self::$driver) {
  675. case 'pdo':
  676. $columns = $stmt->fetch(PDO::FETCH_ASSOC);
  677. break;
  678. case 'mysqli':
  679. if (get_class($stmt) == 'mysqli_result') {
  680. $columns = $stmt->fetch_assoc();
  681. break;
  682. }
  683. // This code works, but is slow
  684. // Bind the result to a result array
  685. $cols = [];
  686. $cols_num = [];
  687. for ($x = 0; $x < $stmt->field_count; $x++) {
  688. $cols[] = &$cols_num[$x];
  689. }
  690. call_user_func_array([$stmt, 'bind_result'], $cols);
  691. if (!$stmt->fetch()) {
  692. return false;
  693. }
  694. // The slow part:
  695. // We need to get the field names for the array keys
  696. // It seems that there is no better way to do this.
  697. $result = $stmt->result_metadata();
  698. $fields = $result->fetch_fields();
  699. foreach ($cols_num AS $param => $col) {
  700. $columns[$fields[$param]->name] = $col;
  701. }
  702. }
  703. $a->saveTimestamp($stamp1, 'database');
  704. return $columns;
  705. }
  706. /**
  707. * @brief Insert a row into a table
  708. *
  709. * @param string $table Table name
  710. * @param array $param parameter array
  711. * @param bool $on_duplicate_update Do an update on a duplicate entry
  712. *
  713. * @return boolean was the insert successful?
  714. * @throws \Exception
  715. */
  716. public static function insert($table, $param, $on_duplicate_update = false) {
  717. if (empty($table) || empty($param)) {
  718. Logger::log('Table and fields have to be set');
  719. return false;
  720. }
  721. $sql = "INSERT INTO `".self::escape($table)."` (`".implode("`, `", array_keys($param))."`) VALUES (".
  722. substr(str_repeat("?, ", count($param)), 0, -2).")";
  723. if ($on_duplicate_update) {
  724. $sql .= " ON DUPLICATE KEY UPDATE `".implode("` = ?, `", array_keys($param))."` = ?";
  725. $values = array_values($param);
  726. $param = array_merge_recursive($values, $values);
  727. }
  728. return self::e($sql, $param);
  729. }
  730. /**
  731. * @brief Fetch the id of the last insert command
  732. *
  733. * @return integer Last inserted id
  734. */
  735. public static function lastInsertId() {
  736. switch (self::$driver) {
  737. case 'pdo':
  738. $id = self::$connection->lastInsertId();
  739. break;
  740. case 'mysqli':
  741. $id = self::$connection->insert_id;
  742. break;
  743. }
  744. return $id;
  745. }
  746. /**
  747. * @brief Locks a table for exclusive write access
  748. *
  749. * This function can be extended in the future to accept a table array as well.
  750. *
  751. * @param string $table Table name
  752. *
  753. * @return boolean was the lock successful?
  754. * @throws \Exception
  755. */
  756. public static function lock($table) {
  757. // See here: https://dev.mysql.com/doc/refman/5.7/en/lock-tables-and-transactions.html
  758. if (self::$driver == 'pdo') {
  759. self::e("SET autocommit=0");
  760. self::$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
  761. } else {
  762. self::$connection->autocommit(false);
  763. }
  764. $success = self::e("LOCK TABLES `".self::escape($table)."` WRITE");
  765. if (self::$driver == 'pdo') {
  766. self::$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
  767. }
  768. if (!$success) {
  769. if (self::$driver == 'pdo') {
  770. self::e("SET autocommit=1");
  771. } else {
  772. self::$connection->autocommit(true);
  773. }
  774. } else {
  775. self::$in_transaction = true;
  776. }
  777. return $success;
  778. }
  779. /**
  780. * @brief Unlocks all locked tables
  781. *
  782. * @return boolean was the unlock successful?
  783. * @throws \Exception
  784. */
  785. public static function unlock() {
  786. // See here: https://dev.mysql.com/doc/refman/5.7/en/lock-tables-and-transactions.html
  787. self::performCommit();
  788. if (self::$driver == 'pdo') {
  789. self::$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
  790. }
  791. $success = self::e("UNLOCK TABLES");
  792. if (self::$driver == 'pdo') {
  793. self::$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
  794. self::e("SET autocommit=1");
  795. } else {
  796. self::$connection->autocommit(true);
  797. }
  798. self::$in_transaction = false;
  799. return $success;
  800. }
  801. /**
  802. * @brief Starts a transaction
  803. *
  804. * @return boolean Was the command executed successfully?
  805. */
  806. public static function transaction() {
  807. if (!self::performCommit()) {
  808. return false;
  809. }
  810. switch (self::$driver) {
  811. case 'pdo':
  812. if (!self::$connection->inTransaction() && !self::$connection->beginTransaction()) {
  813. return false;
  814. }
  815. break;
  816. case 'mysqli':
  817. if (!self::$connection->begin_transaction()) {
  818. return false;
  819. }
  820. break;
  821. }
  822. self::$in_transaction = true;
  823. return true;
  824. }
  825. private static function performCommit()
  826. {
  827. switch (self::$driver) {
  828. case 'pdo':
  829. if (!self::$connection->inTransaction()) {
  830. return true;
  831. }
  832. return self::$connection->commit();
  833. case 'mysqli':
  834. return self::$connection->commit();
  835. }
  836. return true;
  837. }
  838. /**
  839. * @brief Does a commit
  840. *
  841. * @return boolean Was the command executed successfully?
  842. */
  843. public static function commit() {
  844. if (!self::performCommit()) {
  845. return false;
  846. }
  847. self::$in_transaction = false;
  848. return true;
  849. }
  850. /**
  851. * @brief Does a rollback
  852. *
  853. * @return boolean Was the command executed successfully?
  854. */
  855. public static function rollback() {
  856. $ret = false;
  857. switch (self::$driver) {
  858. case 'pdo':
  859. if (!self::$connection->inTransaction()) {
  860. $ret = true;
  861. break;
  862. }
  863. $ret = self::$connection->rollBack();
  864. break;
  865. case 'mysqli':
  866. $ret = self::$connection->rollback();
  867. break;
  868. }
  869. self::$in_transaction = false;
  870. return $ret;
  871. }
  872. /**
  873. * @brief Build the array with the table relations
  874. *
  875. * The array is build from the database definitions in DBStructure.php
  876. *
  877. * This process must only be started once, since the value is cached.
  878. */
  879. private static function buildRelationData() {
  880. $definition = DBStructure::definition(self::$config->get('system', 'basepath'));
  881. foreach ($definition AS $table => $structure) {
  882. foreach ($structure['fields'] AS $field => $field_struct) {
  883. if (isset($field_struct['relation'])) {
  884. foreach ($field_struct['relation'] AS $rel_table => $rel_field) {
  885. self::$relation[$rel_table][$rel_field][$table][] = $field;
  886. }
  887. }
  888. }
  889. }
  890. }
  891. /**
  892. * @brief Delete a row from a table
  893. *
  894. * @param string $table Table name
  895. * @param array $conditions Field condition(s)
  896. * @param array $options
  897. * - cascade: If true we delete records in other tables that depend on the one we're deleting through
  898. * relations (default: true)
  899. * @param array $callstack Internal use: prevent endless loops
  900. *
  901. * @return boolean was the delete successful?
  902. * @throws \Exception
  903. */
  904. public static function delete($table, array $conditions, array $options = [], array &$callstack = [])
  905. {
  906. if (empty($table) || empty($conditions)) {
  907. Logger::log('Table and conditions have to be set');
  908. return false;
  909. }
  910. $commands = [];
  911. // Create a key for the loop prevention
  912. $key = $table . ':' . json_encode($conditions);
  913. // We quit when this key already exists in the callstack.
  914. if (isset($callstack[$key])) {
  915. return $commands;
  916. }
  917. $callstack[$key] = true;
  918. $table = self::escape($table);
  919. $commands[$key] = ['table' => $table, 'conditions' => $conditions];
  920. // Don't use "defaults" here, since it would set "false" to "true"
  921. if (isset($options['cascade'])) {
  922. $cascade = $options['cascade'];
  923. } else {
  924. $cascade = true;
  925. }
  926. // To speed up the whole process we cache the table relations
  927. if ($cascade && count(self::$relation) == 0) {
  928. self::buildRelationData();
  929. }
  930. // Is there a relation entry for the table?
  931. if ($cascade && isset(self::$relation[$table])) {
  932. // We only allow a simple "one field" relation.
  933. $field = array_keys(self::$relation[$table])[0];
  934. $rel_def = array_values(self::$relation[$table])[0];
  935. // Create a key for preventing double queries
  936. $qkey = $field . '-' . $table . ':' . json_encode($conditions);
  937. // When the search field is the relation field, we don't need to fetch the rows
  938. // This is useful when the leading record is already deleted in the frontend but the rest is done in the backend
  939. if ((count($conditions) == 1) && ($field == array_keys($conditions)[0])) {
  940. foreach ($rel_def AS $rel_table => $rel_fields) {
  941. foreach ($rel_fields AS $rel_field) {
  942. self::delete($rel_table, [$rel_field => array_values($conditions)[0]], $options, $callstack);
  943. }
  944. }
  945. // We quit when this key already exists in the callstack.
  946. } elseif (!isset($callstack[$qkey])) {
  947. $callstack[$qkey] = true;
  948. // Fetch all rows that are to be deleted
  949. $data = self::select($table, [$field], $conditions);
  950. while ($row = self::fetch($data)) {
  951. self::delete($table, [$field => $row[$field]], $options, $callstack);
  952. }
  953. self::close($data);
  954. // Since we had split the delete command we don't need the original command anymore
  955. unset($commands[$key]);
  956. }
  957. }
  958. // Now we finalize the process
  959. $do_transaction = !self::$in_transaction;
  960. if ($do_transaction) {
  961. self::transaction();
  962. }
  963. $compacted = [];
  964. $counter = [];
  965. foreach ($commands AS $command) {
  966. $conditions = $command['conditions'];
  967. reset($conditions);
  968. $first_key = key($conditions);
  969. $condition_string = self::buildCondition($conditions);
  970. if ((count($command['conditions']) > 1) || is_int($first_key)) {
  971. $sql = "DELETE FROM `" . $command['table'] . "`" . $condition_string;
  972. Logger::log(self::replaceParameters($sql, $conditions), Logger::DATA);
  973. if (!self::e($sql, $conditions)) {
  974. if ($do_transaction) {
  975. self::rollback();
  976. }
  977. return false;
  978. }
  979. } else {
  980. $key_table = $command['table'];
  981. $key_condition = array_keys($command['conditions'])[0];
  982. $value = array_values($command['conditions'])[0];
  983. // Split the SQL queries in chunks of 100 values
  984. // We do the $i stuff here to make the code better readable
  985. $i = isset($counter[$key_table][$key_condition]) ? $counter[$key_table][$key_condition] : 0;
  986. if (isset($compacted[$key_table][$key_condition][$i]) && count($compacted[$key_table][$key_condition][$i]) > 100) {
  987. ++$i;
  988. }
  989. $compacted[$key_table][$key_condition][$i][$value] = $value;
  990. $counter[$key_table][$key_condition] = $i;
  991. }
  992. }
  993. foreach ($compacted AS $table => $values) {
  994. foreach ($values AS $field => $field_value_list) {
  995. foreach ($field_value_list AS $field_values) {
  996. $sql = "DELETE FROM `" . $table . "` WHERE `" . $field . "` IN (" .
  997. substr(str_repeat("?, ", count($field_values)), 0, -2) . ");";
  998. Logger::log(self::replaceParameters($sql, $field_values), Logger::DATA);
  999. if (!self::e($sql, $field_values)) {
  1000. if ($do_transaction) {
  1001. self::rollback();
  1002. }
  1003. return false;
  1004. }
  1005. }
  1006. }
  1007. }
  1008. if ($do_transaction) {
  1009. self::commit();
  1010. }
  1011. return true;
  1012. }
  1013. /**
  1014. * @brief Updates rows
  1015. *
  1016. * Updates rows in the database. When $old_fields is set to an array,
  1017. * the system will only do an update if the fields in that array changed.
  1018. *
  1019. * Attention:
  1020. * Only the values in $old_fields are compared.
  1021. * This is an intentional behaviour.
  1022. *
  1023. * Example:
  1024. * We include the timestamp field in $fields but not in $old_fields.
  1025. * Then the row will only get the new timestamp when the other fields had changed.
  1026. *
  1027. * When $old_fields is set to a boolean value the system will do this compare itself.
  1028. * When $old_fields is set to "true" the system will do an insert if the row doesn't exists.
  1029. *
  1030. * Attention:
  1031. * Only set $old_fields to a boolean value when you are sure that you will update a single row.
  1032. * When you set $old_fields to "true" then $fields must contain all relevant fields!
  1033. *
  1034. * @param string $table Table name
  1035. * @param array $fields contains the fields that are updated
  1036. * @param array $condition condition array with the key values
  1037. * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate)
  1038. *
  1039. * @return boolean was the update successfull?
  1040. * @throws \Exception
  1041. */
  1042. public static function update($table, $fields, $condition, $old_fields = []) {
  1043. if (empty($table) || empty($fields) || empty($condition)) {
  1044. Logger::log('Table, fields and condition have to be set');
  1045. return false;
  1046. }
  1047. $table = self::escape($table);
  1048. $condition_string = self::buildCondition($condition);
  1049. if (is_bool($old_fields)) {
  1050. $do_insert = $old_fields;
  1051. $old_fields = self::selectFirst($table, [], $condition);
  1052. if (is_bool($old_fields)) {
  1053. if ($do_insert) {
  1054. $values = array_merge($condition, $fields);
  1055. return self::insert($table, $values, $do_insert);
  1056. }
  1057. $old_fields = [];
  1058. }
  1059. }
  1060. $do_update = (count($old_fields) == 0);
  1061. foreach ($old_fields AS $fieldname => $content) {
  1062. if (isset($fields[$fieldname])) {
  1063. if (($fields[$fieldname] == $content) && !is_null($content)) {
  1064. unset($fields[$fieldname]);
  1065. } else {
  1066. $do_update = true;
  1067. }
  1068. }
  1069. }
  1070. if (!$do_update || (count($fields) == 0)) {
  1071. return true;
  1072. }
  1073. $sql = "UPDATE `".$table."` SET `".
  1074. implode("` = ?, `", array_keys($fields))."` = ?".$condition_string;
  1075. $params1 = array_values($fields);
  1076. $params2 = array_values($condition);
  1077. $params = array_merge_recursive($params1, $params2);
  1078. return self::e($sql, $params);
  1079. }
  1080. /**
  1081. * Retrieve a single record from a table and returns it in an associative array
  1082. *
  1083. * @brief Retrieve a single record from a table
  1084. * @param string $table
  1085. * @param array $fields
  1086. * @param array $condition
  1087. * @param array $params
  1088. * @return bool|array
  1089. * @throws \Exception
  1090. * @see self::select
  1091. */
  1092. public static function selectFirst($table, array $fields = [], array $condition = [], $params = [])
  1093. {
  1094. $params['limit'] = 1;
  1095. $result = self::select($table, $fields, $condition, $params);
  1096. if (is_bool($result)) {
  1097. return $result;
  1098. } else {
  1099. $row = self::fetch($result);
  1100. self::close($result);
  1101. return $row;
  1102. }
  1103. }
  1104. /**
  1105. * @brief Select rows from a table
  1106. *
  1107. * @param string $table Table name
  1108. * @param array $fields Array of selected fields, empty for all
  1109. * @param array $condition Array of fields for condition
  1110. * @param array $params Array of several parameters
  1111. *
  1112. * @return boolean|object
  1113. *
  1114. * Example:
  1115. * $table = "item";
  1116. * $fields = array("id", "uri", "uid", "network");
  1117. *
  1118. * $condition = array("uid" => 1, "network" => 'dspr');
  1119. * or:
  1120. * $condition = array("`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr');
  1121. *
  1122. * $params = array("order" => array("id", "received" => true), "limit" => 10);
  1123. *
  1124. * $data = DBA::select($table, $fields, $condition, $params);
  1125. * @throws \Exception
  1126. */
  1127. public static function select($table, array $fields = [], array $condition = [], array $params = [])
  1128. {
  1129. if ($table == '') {
  1130. return false;
  1131. }
  1132. $table = self::escape($table);
  1133. if (count($fields) > 0) {
  1134. $select_fields = "`" . implode("`, `", array_values($fields)) . "`";
  1135. } else {
  1136. $select_fields = "*";
  1137. }
  1138. $condition_string = self::buildCondition($condition);
  1139. $param_string = self::buildParameter($params);
  1140. $sql = "SELECT " . $select_fields . " FROM `" . $table . "`" . $condition_string . $param_string;
  1141. $result = self::p($sql, $condition);
  1142. return $result;
  1143. }
  1144. /**
  1145. * @brief Counts the rows from a table satisfying the provided condition
  1146. *
  1147. * @param string $table Table name
  1148. * @param array $condition array of fields for condition
  1149. *
  1150. * @return int
  1151. *
  1152. * Example:
  1153. * $table = "item";
  1154. *
  1155. * $condition = ["uid" => 1, "network" => 'dspr'];
  1156. * or:
  1157. * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
  1158. *
  1159. * $count = DBA::count($table, $condition);
  1160. * @throws \Exception
  1161. */
  1162. public static function count($table, array $condition = [])
  1163. {
  1164. if ($table == '') {
  1165. return false;
  1166. }
  1167. $condition_string = self::buildCondition($condition);
  1168. $sql = "SELECT COUNT(*) AS `count` FROM `".$table."`".$condition_string;
  1169. $row = self::fetchFirst($sql, $condition);
  1170. return $row['count'];
  1171. }
  1172. /**
  1173. * @brief Returns the SQL condition string built from the provided condition array
  1174. *
  1175. * This function operates with two modes.
  1176. * - Supplied with a filed/value associative array, it builds simple strict
  1177. * equality conditions linked by AND.
  1178. * - Supplied with a flat list, the first element is the condition string and
  1179. * the following arguments are the values to be interpolated
  1180. *
  1181. * $condition = ["uid" => 1, "network" => 'dspr'];
  1182. * or:
  1183. * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
  1184. *
  1185. * In either case, the provided array is left with the parameters only
  1186. *
  1187. * @param array $condition
  1188. * @return string
  1189. */
  1190. public static function buildCondition(array &$condition = [])
  1191. {
  1192. $condition_string = '';
  1193. if (count($condition) > 0) {
  1194. reset($condition);
  1195. $first_key = key($condition);
  1196. if (is_int($first_key)) {
  1197. $condition_string = " WHERE (" . array_shift($condition) . ")";
  1198. } else {
  1199. $new_values = [];
  1200. $condition_string = "";
  1201. foreach ($condition as $field => $value) {
  1202. if ($condition_string != "") {
  1203. $condition_string .= " AND ";
  1204. }
  1205. if (is_array($value)) {
  1206. /* Workaround for MySQL Bug #64791.
  1207. * Never mix data types inside any IN() condition.
  1208. * In case of mixed types, cast all as string.
  1209. * Logic needs to be consistent with DBA::p() data types.
  1210. */
  1211. $is_int = false;
  1212. $is_alpha = false;
  1213. foreach ($value as $single_value) {
  1214. if (is_int($single_value)) {
  1215. $is_int = true;
  1216. } else {
  1217. $is_alpha = true;
  1218. }
  1219. }
  1220. if ($is_int && $is_alpha) {
  1221. foreach ($value as &$ref) {
  1222. if (is_int($ref)) {
  1223. $ref = (string)$ref;
  1224. }
  1225. }
  1226. unset($ref); //Prevent accidental re-use.
  1227. }
  1228. $new_values = array_merge($new_values, array_values($value));
  1229. $placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
  1230. $condition_string .= "`" . $field . "` IN (" . $placeholders . ")";
  1231. } else {
  1232. $new_values[$field] = $value;
  1233. $condition_string .= "`" . $field . "` = ?";
  1234. }
  1235. }
  1236. $condition_string = " WHERE (" . $condition_string . ")";
  1237. $condition = $new_values;
  1238. }
  1239. }
  1240. return $condition_string;
  1241. }
  1242. /**
  1243. * @brief Returns the SQL parameter string built from the provided parameter array
  1244. *
  1245. * @param array $params
  1246. * @return string
  1247. */
  1248. public static function buildParameter(array $params = [])
  1249. {
  1250. $order_string = '';
  1251. if (isset($params['order'])) {
  1252. $order_string = " ORDER BY ";
  1253. foreach ($params['order'] AS $fields => $order) {
  1254. if (!is_int($fields)) {
  1255. $order_string .= "`" . $fields . "` " . ($order ? "DESC" : "ASC") . ", ";
  1256. } else {
  1257. $order_string .= "`" . $order . "`, ";
  1258. }
  1259. }
  1260. $order_string = substr($order_string, 0, -2);
  1261. }
  1262. $limit_string = '';
  1263. if (isset($params['limit']) && is_int($params['limit'])) {
  1264. $limit_string = " LIMIT " . intval($params['limit']);
  1265. }
  1266. if (isset($params['limit']) && is_array($params['limit'])) {
  1267. $limit_string = " LIMIT " . intval($params['limit'][0]) . ", " . intval($params['limit'][1]);
  1268. }
  1269. return $order_string.$limit_string;
  1270. }
  1271. /**
  1272. * @brief Fills an array with data from a query
  1273. *
  1274. * @param object $stmt statement object
  1275. * @param bool $do_close
  1276. * @return array Data array
  1277. */
  1278. public static function toArray($stmt, $do_close = true) {
  1279. if (is_bool($stmt)) {
  1280. return $stmt;
  1281. }
  1282. $data = [];
  1283. while ($row = self::fetch($stmt)) {
  1284. $data[] = $row;
  1285. }
  1286. if ($do_close) {
  1287. self::close($stmt);
  1288. }
  1289. return $data;
  1290. }
  1291. /**
  1292. * @brief Returns the error number of the last query
  1293. *
  1294. * @return string Error number (0 if no error)
  1295. */
  1296. public static function errorNo() {
  1297. return self::$errorno;
  1298. }
  1299. /**
  1300. * @brief Returns the error message of the last query
  1301. *
  1302. * @return string Error message ('' if no error)
  1303. */
  1304. public static function errorMessage() {
  1305. return self::$error;
  1306. }
  1307. /**
  1308. * @brief Closes the current statement
  1309. *
  1310. * @param object $stmt statement object
  1311. * @return boolean was the close successful?
  1312. */
  1313. public static function close($stmt) {
  1314. $a = \get_app();
  1315. $stamp1 = microtime(true);
  1316. if (!is_object($stmt)) {
  1317. return false;
  1318. }
  1319. switch (self::$driver) {
  1320. case 'pdo':
  1321. $ret = $stmt->closeCursor();
  1322. break;
  1323. case 'mysqli':
  1324. // MySQLi offers both a mysqli_stmt and a mysqli_result class.
  1325. // We should be careful not to assume the object type of $stmt
  1326. // because DBA::p() has been able to return both types.
  1327. if ($stmt instanceof mysqli_stmt) {
  1328. $stmt->free_result();
  1329. $ret = $stmt->close();
  1330. } elseif ($stmt instanceof mysqli_result) {
  1331. $stmt->free();
  1332. $ret = true;
  1333. } else {
  1334. $ret = false;
  1335. }
  1336. break;
  1337. }
  1338. $a->saveTimestamp($stamp1, 'database');
  1339. return $ret;
  1340. }
  1341. /**
  1342. * @brief Return a list of database processes
  1343. *
  1344. * @return array
  1345. * 'list' => List of processes, separated in their different states
  1346. * 'amount' => Number of concurrent database processes
  1347. * @throws \Exception
  1348. */
  1349. public static function processlist()
  1350. {
  1351. $ret = self::p("SHOW PROCESSLIST");
  1352. $data = self::toArray($ret);
  1353. $processes = 0;
  1354. $states = [];
  1355. foreach ($data as $process) {
  1356. $state = trim($process["State"]);
  1357. // Filter out all non blocking processes
  1358. if (!in_array($state, ["", "init", "statistics", "updating"])) {
  1359. ++$states[$state];
  1360. ++$processes;
  1361. }
  1362. }
  1363. $statelist = "";
  1364. foreach ($states as $state => $usage) {
  1365. if ($statelist != "") {
  1366. $statelist .= ", ";
  1367. }
  1368. $statelist .= $state.": ".$usage;
  1369. }
  1370. return(["list" => $statelist, "amount" => $processes]);
  1371. }
  1372. /**
  1373. * Checks if $array is a filled array with at least one entry.
  1374. *
  1375. * @param mixed $array A filled array with at least one entry
  1376. *
  1377. * @return boolean Whether $array is a filled array or an object with rows
  1378. */
  1379. public static function isResult($array)
  1380. {
  1381. // It could be a return value from an update statement
  1382. if (is_bool($array)) {
  1383. return $array;
  1384. }
  1385. if (is_object($array)) {
  1386. return self::numRows($array) > 0;
  1387. }
  1388. return (is_array($array) && (count($array) > 0));
  1389. }
  1390. /**
  1391. * @brief Callback function for "esc_array"
  1392. *
  1393. * @param mixed $value Array value
  1394. * @param string $key Array key
  1395. * @param boolean $add_quotation add quotation marks for string values
  1396. * @return void
  1397. */
  1398. private static function escapeArrayCallback(&$value, $key, $add_quotation)
  1399. {
  1400. if (!$add_quotation) {
  1401. if (is_bool($value)) {
  1402. $value = ($value ? '1' : '0');
  1403. } else {
  1404. $value = self::escape($value);
  1405. }
  1406. return;
  1407. }
  1408. if (is_bool($value)) {
  1409. $value = ($value ? 'true' : 'false');
  1410. } elseif (is_float($value) || is_integer($value)) {
  1411. $value = (string) $value;
  1412. } else {
  1413. $value = "'" . self::escape($value) . "'";
  1414. }
  1415. }
  1416. /**
  1417. * @brief Escapes a whole array
  1418. *
  1419. * @param mixed $arr Array with values to be escaped
  1420. * @param boolean $add_quotation add quotation marks for string values
  1421. * @return void
  1422. */
  1423. public static function escapeArray(&$arr, $add_quotation = false)
  1424. {
  1425. array_walk($arr, 'self::escapeArrayCallback', $add_quotation);
  1426. }
  1427. }