PageRenderTime 43ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 1ms

/classes/fDatabase.php

https://bitbucket.org/dsqmoore/flourish
PHP | 3624 lines | 2274 code | 487 blank | 863 comment | 586 complexity | 613c04e1a5b7df0b3bb7b4be610fa0b4 MD5 | raw file
  1. <?php
  2. /**
  3. * Provides a common API for different databases - will automatically use any installed extension
  4. *
  5. * This class is implemented to use the UTF-8 character encoding. Please see
  6. * http://flourishlib.com/docs/UTF-8 for more information.
  7. *
  8. * The following databases are supported:
  9. *
  10. * - [http://ibm.com/db2 DB2]
  11. * - [http://microsoft.com/sql/ MSSQL]
  12. * - [http://mysql.com MySQL]
  13. * - [http://oracle.com Oracle]
  14. * - [http://postgresql.org PostgreSQL]
  15. * - [http://sqlite.org SQLite]
  16. *
  17. * The class will automatically use the first of the following extensions it finds:
  18. *
  19. * - DB2
  20. * - [http://php.net/ibm_db2 ibm_db2]
  21. * - [http://php.net/pdo_ibm pdo_ibm]
  22. * - MSSQL
  23. * - [http://msdn.microsoft.com/en-us/library/cc296221.aspx sqlsrv]
  24. * - [http://php.net/pdo_dblib pdo_dblib]
  25. * - [http://php.net/mssql mssql] (or [http://php.net/sybase sybase])
  26. * - MySQL
  27. * - [http://php.net/mysql mysql]
  28. * - [http://php.net/mysqli mysqli]
  29. * - [http://php.net/pdo_mysql pdo_mysql]
  30. * - Oracle
  31. * - [http://php.net/oci8 oci8]
  32. * - [http://php.net/pdo_oci pdo_oci]
  33. * - PostgreSQL
  34. * - [http://php.net/pgsql pgsql]
  35. * - [http://php.net/pdo_pgsql pdo_pgsql]
  36. * - SQLite
  37. * - [http://php.net/pdo_sqlite pdo_sqlite] (for v3.x)
  38. * - [http://php.net/sqlite sqlite] (for v2.x)
  39. *
  40. * The `odbc` and `pdo_odbc` extensions are not supported due to character
  41. * encoding and stability issues on Windows, and functionality on non-Windows
  42. * operating systems.
  43. *
  44. * @copyright Copyright (c) 2007-2011 Will Bond
  45. * @author Will Bond [wb] <will@flourishlib.com>
  46. * @license http://flourishlib.com/license
  47. *
  48. * @package Flourish
  49. * @link http://flourishlib.com/fDatabase
  50. *
  51. * @version 1.0.0b40
  52. * @changes 1.0.0b40 Fixed a bug with notices being triggered when failing to connect to a SQLite database [wb, 2011-06-20]
  53. * @changes 1.0.0b39 Fixed a bug with detecting some MySQL database version numbers [wb, 2011-05-24]
  54. * @changes 1.0.0b38 Backwards Compatibility Break - callbacks registered to the `extracted` hook via ::registerHookCallback() no longer receive the `$strings` parameter, instead all strings are added into the `$values` parameter - added ::getVersion(), fixed a bug with SQLite messaging, fixed a bug with ::__destruct(), improved handling of transactional queries, added ::close(), enhanced class to throw four different exceptions for different connection errors, silenced PHP warnings upon connection error [wb, 2011-05-09]
  55. * @changes 1.0.0b37 Fixed usage of the mysqli extension to only call mysqli_set_charset() if it exists [wb, 2011-03-04]
  56. * @changes 1.0.0b36 Updated ::escape() and methods that use ::escape() to handle float values that don't contain a digit before or after the . [wb, 2011-02-01]
  57. * @changes 1.0.0b35 Updated the class to replace `LIMIT` and `OFFSET` value placeholders in the SQL with their values before translating since most databases that translate `LIMIT` statements need to move or add values together [wb, 2011-01-11]
  58. * @changes 1.0.0b34 Fixed a bug with creating translated prepared statements [wb, 2011-01-09]
  59. * @changes 1.0.0b33 Added code to explicitly set the connection encoding for the mysql and mysqli extensions since some PHP installs don't see to fully respect `SET NAMES` [wb, 2010-12-06]
  60. * @changes 1.0.0b32 Fixed handling auto-incrementing values for Oracle when the trigger was on `INSERT OR UPDATE` instead of just `INSERT` [wb, 2010-12-04]
  61. * @changes 1.0.0b31 Fixed handling auto-incrementing values for MySQL when the `INTO` keyword is left out of an `INSERT` statement [wb, 2010-11-04]
  62. * @changes 1.0.0b30 Fixed the pgsql, mssql and mysql extensions to force a new connection instead of reusing an existing one [wb, 2010-08-17]
  63. * @changes 1.0.0b29 Backwards Compatibility Break - removed ::enableSlowQueryWarnings(), added ability to replicate via ::registerHookCallback() [wb, 2010-08-10]
  64. * @changes 1.0.0b28 Backwards Compatibility Break - removed ODBC support. Added support for the `pdo_ibm` extension. [wb, 2010-07-31]
  65. * @changes 1.0.0b27 Fixed a bug with running multiple copies of a SQL statement with string values through a single ::translatedQuery() call [wb, 2010-07-14]
  66. * @changes 1.0.0b26 Updated the class to use new fCore functionality [wb, 2010-07-05]
  67. * @changes 1.0.0b25 Added IBM DB2 support [wb, 2010-04-13]
  68. * @changes 1.0.0b24 Fixed an auto-incrementing transaction bug with Oracle and debugging issues with all databases [wb, 2010-03-17]
  69. * @changes 1.0.0b23 Resolved another bug with capturing auto-incrementing values for PostgreSQL and Oracle [wb, 2010-03-15]
  70. * @changes 1.0.0b22 Changed ::clearCache() to also clear the cache on the fSQLTranslation [wb, 2010-03-09]
  71. * @changes 1.0.0b21 Added ::execute() for result-less SQL queries, ::prepare() and ::translatedPrepare() to create fStatement objects for prepared statements, support for prepared statements in ::query() and ::unbufferedQuery(), fixed default caching key for ::enableCaching() [wb, 2010-03-02]
  72. * @changes 1.0.0b20 Added a parameter to ::enableCaching() to provide a key token that will allow cached values to be shared between multiple databases with the same schema [wb, 2009-10-28]
  73. * @changes 1.0.0b19 Added support for escaping identifiers (column and table names) to ::escape(), added support for database schemas, rewrote internal SQL string spliting [wb, 2009-10-22]
  74. * @changes 1.0.0b18 Updated the class for the new fResult and fUnbufferedResult APIs, fixed ::unescape() to not touch NULLs [wb, 2009-08-12]
  75. * @changes 1.0.0b17 Added the ability to pass an array of all values as a single parameter to ::escape() instead of one value per parameter [wb, 2009-08-11]
  76. * @changes 1.0.0b16 Fixed PostgreSQL and Oracle from trying to get auto-incrementing values on inserts when explicit values were given [wb, 2009-08-06]
  77. * @changes 1.0.0b15 Fixed a bug where auto-incremented values would not be detected when table names were quoted [wb, 2009-07-15]
  78. * @changes 1.0.0b14 Changed ::determineExtension() and ::determineCharacterSet() to be protected instead of private [wb, 2009-07-08]
  79. * @changes 1.0.0b13 Updated ::escape() to accept arrays of values for insertion into full SQL strings [wb, 2009-07-06]
  80. * @changes 1.0.0b12 Updates to ::unescape() to improve performance [wb, 2009-06-15]
  81. * @changes 1.0.0b11 Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
  82. * @changes 1.0.0b10 Changed date/time/timestamp escaping from `strtotime()` to fDate/fTime/fTimestamp for better localization support [wb, 2009-06-01]
  83. * @changes 1.0.0b9 Fixed a bug with ::escape() where floats that start with a . were encoded as `NULL` [wb, 2009-05-09]
  84. * @changes 1.0.0b8 Added Oracle support, change PostgreSQL code to no longer cause lastval() warnings, added support for arrays of values to ::escape() [wb, 2009-05-03]
  85. * @changes 1.0.0b7 Updated for new fCore API [wb, 2009-02-16]
  86. * @changes 1.0.0b6 Fixed a bug with executing transaction queries when using the mysqli extension [wb, 2009-02-12]
  87. * @changes 1.0.0b5 Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
  88. * @changes 1.0.0b4 Added a few error suppression operators back in so that developers don't get errors and exceptions [wb, 2009-01-14]
  89. * @changes 1.0.0b3 Removed some unnecessary error suppresion operators [wb, 2008-12-11]
  90. * @changes 1.0.0b2 Fixed a bug with PostgreSQL when using the PDO extension and executing an INSERT statement [wb, 2008-12-11]
  91. * @changes 1.0.0b The initial implementation [wb, 2007-09-25]
  92. */
  93. class fDatabase
  94. {
  95. /**
  96. * Composes text using fText if loaded
  97. *
  98. * @param string $message The message to compose
  99. * @param mixed $component A string or number to insert into the message
  100. * @param mixed ...
  101. * @return string The composed and possible translated message
  102. */
  103. static protected function compose($message)
  104. {
  105. $args = array_slice(func_get_args(), 1);
  106. if (class_exists('fText', FALSE)) {
  107. return call_user_func_array(
  108. array('fText', 'compose'),
  109. array($message, $args)
  110. );
  111. } else {
  112. return vsprintf($message, $args);
  113. }
  114. }
  115. /**
  116. * An fCache object to cache the schema info to
  117. *
  118. * @var fCache
  119. */
  120. private $cache;
  121. /**
  122. * The cache prefix to use for cache entries
  123. *
  124. * @var string
  125. */
  126. private $cache_prefix;
  127. /**
  128. * Database connection resource or PDO object
  129. *
  130. * @var mixed
  131. */
  132. private $connection;
  133. /**
  134. * The database name
  135. *
  136. * @var string
  137. */
  138. private $database;
  139. /**
  140. * If debugging is enabled
  141. *
  142. * @var boolean
  143. */
  144. private $debug;
  145. /**
  146. * A temporary error holder for the mssql extension
  147. *
  148. * @var string
  149. */
  150. private $error;
  151. /**
  152. * The extension to use for the database specified
  153. *
  154. * Options include:
  155. *
  156. * - `'ibm_db2'`
  157. * - `'mssql'`
  158. * - `'mysql'`
  159. * - `'mysqli'`
  160. * - `'oci8'`
  161. * - `'pgsql'`
  162. * - `'sqlite'`
  163. * - `'sqlsrv'`
  164. * - `'pdo'`
  165. *
  166. * @var string
  167. */
  168. protected $extension;
  169. /**
  170. * Hooks callbacks to be used for accessing and modifying queries
  171. *
  172. * This array will have the structure:
  173. *
  174. * {{{
  175. * array(
  176. * 'unmodified' => array({callbacks}),
  177. * 'extracted' => array({callbacks}),
  178. * 'run' => array({callbacks})
  179. * )
  180. * }}}
  181. *
  182. * @var array
  183. */
  184. private $hook_callbacks;
  185. /**
  186. * The host the database server is located on
  187. *
  188. * @var string
  189. */
  190. private $host;
  191. /**
  192. * If a transaction is in progress
  193. *
  194. * @var boolean
  195. */
  196. private $inside_transaction;
  197. /**
  198. * The password for the user specified
  199. *
  200. * @var string
  201. */
  202. private $password;
  203. /**
  204. * The port number for the host
  205. *
  206. * @var string
  207. */
  208. private $port;
  209. /**
  210. * The total number of seconds spent executing queries
  211. *
  212. * @var float
  213. */
  214. private $query_time;
  215. /**
  216. * A cache of database-specific code
  217. *
  218. * @var array
  219. */
  220. protected $schema_info;
  221. /**
  222. * The last executed fStatement object
  223. *
  224. * @var fStatement
  225. */
  226. private $statement;
  227. /**
  228. * The timeout for the database connection
  229. *
  230. * @var integer
  231. */
  232. private $timeout;
  233. /**
  234. * The fSQLTranslation object for this database
  235. *
  236. * @var object
  237. */
  238. private $translation;
  239. /**
  240. * The database type: `'db2'`, `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, or `'sqlite'`
  241. *
  242. * @var string
  243. */
  244. private $type;
  245. /**
  246. * The unbuffered query instance
  247. *
  248. * @var fUnbufferedResult
  249. */
  250. private $unbuffered_result;
  251. /**
  252. * The user to connect to the database as
  253. *
  254. * @var string
  255. */
  256. private $username;
  257. /**
  258. * Configures the connection to a database - connection is not made until the first query is executed
  259. *
  260. * Passing `NULL` to any parameter other than `$type` and `$database` will
  261. * cause the default value to be used.
  262. *
  263. * @param string $type The type of the database: `'db2'`, `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, `'sqlite'`
  264. * @param string $database Name of the database. If SQLite the path to the database file.
  265. * @param string $username Database username - not used for SQLite
  266. * @param string $password The password for the username specified - not used for SQLite
  267. * @param string $host Database server host or IP, defaults to localhost - not used for SQLite. MySQL socket connection can be made by entering `'sock:'` followed by the socket path. PostgreSQL socket connection can be made by passing just `'sock:'`.
  268. * @param integer $port The port to connect to, defaults to the standard port for the database type specified - not used for SQLite
  269. * @param integer $timeout The number of seconds to timeout after if a connection can not be made - not used for SQLite
  270. * @return fDatabase
  271. */
  272. public function __construct($type, $database, $username=NULL, $password=NULL, $host=NULL, $port=NULL, $timeout=NULL)
  273. {
  274. $valid_types = array('db2', 'mssql', 'mysql', 'oracle', 'postgresql', 'sqlite');
  275. if (!in_array($type, $valid_types)) {
  276. throw new fProgrammerException(
  277. 'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
  278. $type,
  279. join(', ', $valid_types)
  280. );
  281. }
  282. if (empty($database)) {
  283. throw new fProgrammerException('No database was specified');
  284. }
  285. if ($host === NULL) {
  286. $host = 'localhost';
  287. }
  288. $this->type = $type;
  289. $this->database = $database;
  290. $this->username = $username;
  291. $this->password = $password;
  292. $this->host = $host;
  293. $this->port = $port;
  294. $this->timeout = $timeout;
  295. $this->hook_callbacks = array(
  296. 'unmodified' => array(),
  297. 'extracted' => array(),
  298. 'run' => array()
  299. );
  300. $this->schema_info = array();
  301. $this->determineExtension();
  302. }
  303. /**
  304. * Closes the open database connection
  305. *
  306. * @internal
  307. *
  308. * @return void
  309. */
  310. public function __destruct()
  311. {
  312. if (!$this->connection) { return; }
  313. fCore::debug('Total query time: ' . $this->query_time . ' seconds', $this->debug);
  314. if ($this->extension == 'ibm_db2') {
  315. db2_close($this->connection);
  316. } elseif ($this->extension == 'mssql') {
  317. mssql_close($this->connection);
  318. } elseif ($this->extension == 'mysql') {
  319. mysql_close($this->connection);
  320. } elseif ($this->extension == 'mysqli') {
  321. // Before 5.2.0 the destructor order would cause mysqli to
  322. // close itself which would make this call trigger a warning
  323. if (fCore::checkVersion('5.2.0')) {
  324. mysqli_close($this->connection);
  325. }
  326. } elseif ($this->extension == 'oci8') {
  327. oci_close($this->connection);
  328. } elseif ($this->extension == 'pgsql') {
  329. pg_close($this->connection);
  330. } elseif ($this->extension == 'sqlite') {
  331. sqlite_close($this->connection);
  332. } elseif ($this->extension == 'sqlsrv') {
  333. sqlsrv_close($this->connection);
  334. } elseif ($this->extension == 'pdo') {
  335. // PDO objects close their own connections when destroyed
  336. }
  337. $this->connection = FALSE;
  338. }
  339. /**
  340. * All requests that hit this method should be requests for callbacks
  341. *
  342. * @internal
  343. *
  344. * @param string $method The method to create a callback for
  345. * @return callback The callback for the method requested
  346. */
  347. public function __get($method)
  348. {
  349. return array($this, $method);
  350. }
  351. /**
  352. * Checks to see if an SQL error occured
  353. *
  354. * @param fResult|fUnbufferedResult|boolean $result The result object for the query
  355. * @param mixed $extra_info The sqlite extension will pass a string error message, the oci8 extension will pass the statement resource
  356. * @param string $sql The SQL that was executed
  357. * @return void
  358. */
  359. private function checkForError($result, $extra_info=NULL, $sql=NULL)
  360. {
  361. if ($result === FALSE || $result->getResult() === FALSE) {
  362. if ($this->extension == 'ibm_db2') {
  363. if (is_resource($extra_info)) {
  364. $message = db2_stmt_errormsg($extra_info);
  365. } else {
  366. $message = db2_stmt_errormsg();
  367. }
  368. } elseif ($this->extension == 'mssql') {
  369. $message = $this->error;
  370. $this->error = '';
  371. } elseif ($this->extension == 'mysql') {
  372. $message = mysql_error($this->connection);
  373. } elseif ($this->extension == 'mysqli') {
  374. if (is_object($extra_info)) {
  375. $message = $extra_info->error;
  376. } else {
  377. $message = mysqli_error($this->connection);
  378. }
  379. } elseif ($this->extension == 'oci8') {
  380. $error_info = oci_error($extra_info ? $extra_info : $this->connection);
  381. $message = $error_info['message'];
  382. } elseif ($this->extension == 'pgsql') {
  383. $message = pg_last_error($this->connection);
  384. } elseif ($this->extension == 'sqlite') {
  385. if ($extra_info === NULL) {
  386. $message = sqlite_error_string(sqlite_last_error($this->connection));
  387. } else {
  388. $message = $extra_info;
  389. }
  390. } elseif ($this->extension == 'sqlsrv') {
  391. $error_info = sqlsrv_errors(SQLSRV_ERR_ALL);
  392. $message = $error_info[0]['message'];
  393. } elseif ($this->extension == 'pdo') {
  394. if ($extra_info instanceof PDOStatement) {
  395. $error_info = $extra_info->errorInfo();
  396. } else {
  397. $error_info = $this->connection->errorInfo();
  398. }
  399. if (empty($error_info[2])) {
  400. $error_info[2] = 'Unknown error - this usually indicates a bug in the PDO driver';
  401. }
  402. $message = $error_info[2];
  403. }
  404. $db_type_map = array(
  405. 'db2' => 'DB2',
  406. 'mssql' => 'MSSQL',
  407. 'mysql' => 'MySQL',
  408. 'oracle' => 'Oracle',
  409. 'postgresql' => 'PostgreSQL',
  410. 'sqlite' => 'SQLite'
  411. );
  412. throw new fSQLException(
  413. '%1$s error (%2$s) in %3$s',
  414. $db_type_map[$this->type],
  415. $message,
  416. is_object($result) ? $result->getSQL() : $sql
  417. );
  418. }
  419. }
  420. /**
  421. * Clears all of the schema info out of the object and, if set, the fCache object
  422. *
  423. * @return void
  424. */
  425. public function clearCache()
  426. {
  427. $this->schema_info = array();
  428. if ($this->cache) {
  429. $this->cache->delete($this->makeCachePrefix() . 'schema_info');
  430. }
  431. if ($this->type == 'mssql') {
  432. $this->determineCharacterSet();
  433. }
  434. if ($this->translation) {
  435. $this->translation->clearCache();
  436. }
  437. }
  438. /**
  439. * Closes the database connection
  440. *
  441. * @return void
  442. */
  443. public function close()
  444. {
  445. $this->__destruct();
  446. }
  447. /**
  448. * Connects to the database specified, if no connection exists
  449. *
  450. * This method is only intended to force a connection, all operations that
  451. * require a database connection will automatically call this method.
  452. *
  453. * @throws fAuthorizationException When the username and password are not accepted
  454. *
  455. * @return void
  456. */
  457. public function connect()
  458. {
  459. // Don't try to reconnect if we are already connected
  460. if ($this->connection) { return; }
  461. $connection_error = FALSE;
  462. $authentication_error = FALSE;
  463. $database_error = FALSE;
  464. $errors = NULL;
  465. // Establish a connection to the database
  466. if ($this->extension == 'pdo') {
  467. $username = $this->username;
  468. $password = $this->password;
  469. $options = array();
  470. if ($this->timeout !== NULL && $this->type != 'sqlite' && $this->type != 'mssql') {
  471. $options[PDO::ATTR_TIMEOUT] = $this->timeout;
  472. }
  473. if ($this->type == 'db2') {
  474. if ($this->host === NULL && $this->port === NULL) {
  475. $dsn = 'ibm:DSN:' . $this->database;
  476. } else {
  477. $dsn = 'ibm:DRIVER={IBM DB2 ODBC DRIVER};DATABASE=' . $this->database . ';HOSTNAME=' . $this->host . ';';
  478. $dsn .= 'PORT=' . ($this->port ? $this->port : 60000) . ';';
  479. $dsn .= 'PROTOCOL=TCPIP;UID=' . $username . ';PWD=' . $password . ';';
  480. if ($this->timeout !== NULL) {
  481. $dsn .= 'CONNECTTIMEOUT=' . $this->timeout . ';';
  482. }
  483. $username = NULL;
  484. $password = NULL;
  485. }
  486. } elseif ($this->type == 'mssql') {
  487. $separator = (fCore::checkOS('windows')) ? ',' : ':';
  488. $port = ($this->port) ? $separator . $this->port : '';
  489. $driver = (fCore::checkOs('windows')) ? 'mssql' : 'dblib';
  490. $dsn = $driver . ':host=' . $this->host . $port . ';dbname=' . $this->database;
  491. // This driver does not support timeouts so we fake it here
  492. if ($this->timeout !== NULL) {
  493. fCore::startErrorCapture();
  494. $resource = fsockopen($this->host, $this->port ? $this->port : 1433, $errno, $errstr, $this->timeout);
  495. $errors = fCore::stopErrorCapture();
  496. if ($resource !== FALSE) {
  497. fclose($resource);
  498. }
  499. }
  500. } elseif ($this->type == 'mysql') {
  501. if (substr($this->host, 0, 5) == 'sock:') {
  502. $dsn = 'mysql:unix_socket=' . substr($this->host, 5) . ';dbname=' . $this->database;
  503. } else {
  504. $port = ($this->port) ? ';port=' . $this->port : '';
  505. $dsn = 'mysql:host=' . $this->host . ';dbname=' . $this->database . $port;
  506. }
  507. } elseif ($this->type == 'oracle') {
  508. $port = ($this->port) ? ':' . $this->port : '';
  509. $dsn = 'oci:dbname=' . $this->host . $port . '/' . $this->database . ';charset=AL32UTF8';
  510. // This driver does not support timeouts so we fake it here
  511. if ($this->timeout !== NULL) {
  512. fCore::startErrorCapture();
  513. $resource = fsockopen($this->host, $this->port ? $this->port : 1521, $errno, $errstr, $this->timeout);
  514. $errors = fCore::stopErrorCapture();
  515. if ($resource !== FALSE) {
  516. fclose($resource);
  517. }
  518. }
  519. } elseif ($this->type == 'postgresql') {
  520. $dsn = 'pgsql:dbname=' . $this->database;
  521. if ($this->host && $this->host != 'sock:') {
  522. $dsn .= ' host=' . $this->host;
  523. }
  524. if ($this->port) {
  525. $dsn .= ' port=' . $this->port;
  526. }
  527. } elseif ($this->type == 'sqlite') {
  528. $dsn = 'sqlite:' . $this->database;
  529. }
  530. try {
  531. if ($errors) {
  532. $this->connection = FALSE;
  533. } else {
  534. $this->connection = new PDO($dsn, $username, $password, $options);
  535. if ($this->type == 'mysql') {
  536. $this->connection->setAttribute(PDO::MYSQL_ATTR_DIRECT_QUERY, 1);
  537. }
  538. }
  539. } catch (PDOException $e) {
  540. $this->connection = FALSE;
  541. $errors = $e->getMessage();
  542. }
  543. }
  544. if ($this->extension == 'sqlite') {
  545. $this->connection = sqlite_open($this->database);
  546. }
  547. if ($this->extension == 'ibm_db2') {
  548. $username = $this->username;
  549. $password = $this->password;
  550. if ($this->host === NULL && $this->port === NULL && $this->timeout === NULL) {
  551. $connection_string = $this->database;
  552. } else {
  553. $connection_string = 'DATABASE=' . $this->database . ';HOSTNAME=' . $this->host . ';';
  554. $connection_string .= 'PORT=' . ($this->port ? $this->port : 60000) . ';';
  555. $connection_string .= 'PROTOCOL=TCPIP;UID=' . $this->username . ';PWD=' . $this->password . ';';
  556. if ($this->timeout !== NULL) {
  557. $connection_string .= 'CONNECTTIMEOUT=' . $this->timeout . ';';
  558. }
  559. $username = NULL;
  560. $password = NULL;
  561. }
  562. $options = array(
  563. 'autocommit' => DB2_AUTOCOMMIT_ON,
  564. 'DB2_ATTR_CASE' => DB2_CASE_LOWER
  565. );
  566. $this->connection = db2_connect($connection_string, $username, $password, $options);
  567. if ($this->connection === FALSE) {
  568. $errors = db2_conn_errormsg();
  569. }
  570. }
  571. if ($this->extension == 'mssql') {
  572. if ($this->timeout !== NULL) {
  573. $old_timeout = ini_get('mssql.connect_timeout');
  574. ini_set('mssql.connect_timeout', $this->timeout);
  575. }
  576. fCore::startErrorCapture();
  577. $separator = (fCore::checkOS('windows')) ? ',' : ':';
  578. $this->connection = mssql_connect(($this->port) ? $this->host . $separator . $this->port : $this->host, $this->username, $this->password, TRUE);
  579. if ($this->connection !== FALSE && mssql_select_db($this->database, $this->connection) === FALSE) {
  580. $this->connection = FALSE;
  581. }
  582. $errors = fCore::stopErrorCapture();
  583. if ($this->timeout !== NULL) {
  584. ini_set('mssql.connect_timeout', $old_timeout);
  585. }
  586. }
  587. if ($this->extension == 'mysql') {
  588. if ($this->timeout !== NULL) {
  589. $old_timeout = ini_get('mysql.connect_timeout');
  590. ini_set('mysql.connect_timeout', $this->timeout);
  591. }
  592. if (substr($this->host, 0, 5) == 'sock:') {
  593. $host = substr($this->host, 4);
  594. } elseif ($this->port) {
  595. $host = $this->host . ':' . $this->port;
  596. } else {
  597. $host = $this->host;
  598. }
  599. fCore::startErrorCapture();
  600. $this->connection = mysql_connect($host, $this->username, $this->password, TRUE);
  601. $errors = fCore::stopErrorCapture();
  602. if ($this->connection !== FALSE && mysql_select_db($this->database, $this->connection) === FALSE) {
  603. $errors = 'Unknown database';
  604. $this->connection = FALSE;
  605. }
  606. if ($this->connection && function_exists('mysql_set_charset') && !mysql_set_charset('utf8', $this->connection)) {
  607. throw new fConnectivityException(
  608. 'There was an error setting the database connection to use UTF-8'
  609. );
  610. }
  611. if ($this->timeout !== NULL) {
  612. ini_set('mysql.connect_timeout', $old_timeout);
  613. }
  614. }
  615. if ($this->extension == 'mysqli') {
  616. $this->connection = mysqli_init();
  617. if ($this->timeout !== NULL) {
  618. mysqli_options($this->connection, MYSQLI_OPT_CONNECT_TIMEOUT, $this->timeout);
  619. }
  620. fCore::startErrorCapture();
  621. if (substr($this->host, 0, 5) == 'sock:') {
  622. $result = mysqli_real_connect($this->connection, 'localhost', $this->username, $this->password, $this->database, $this->port, substr($this->host, 5));
  623. } elseif ($this->port) {
  624. $result = mysqli_real_connect($this->connection, $this->host, $this->username, $this->password, $this->database, $this->port);
  625. } else {
  626. $result = mysqli_real_connect($this->connection, $this->host, $this->username, $this->password, $this->database);
  627. }
  628. if (!$result) {
  629. $this->connection = FALSE;
  630. }
  631. $errors = fCore::stopErrorCapture();
  632. if ($this->connection && function_exists('mysqli_set_charset') && !mysqli_set_charset($this->connection, 'utf8')) {
  633. throw new fConnectivityException(
  634. 'There was an error setting the database connection to use UTF-8'
  635. );
  636. }
  637. }
  638. if ($this->extension == 'oci8') {
  639. fCore::startErrorCapture();
  640. $resource = TRUE;
  641. // This driver does not support timeouts so we fake it here
  642. if ($this->timeout !== NULL) {
  643. $resource = fsockopen($this->host, $this->port ? $this->port : 1521, $errno, $errstr, $this->timeout);
  644. if ($resource !== FALSE) {
  645. fclose($resource);
  646. $resource = TRUE;
  647. } else {
  648. $this->connection = FALSE;
  649. }
  650. }
  651. if ($resource) {
  652. $this->connection = oci_connect($this->username, $this->password, $this->host . ($this->port ? ':' . $this->port : '') . '/' . $this->database, 'AL32UTF8');
  653. }
  654. $errors = fCore::stopErrorCapture();
  655. }
  656. if ($this->extension == 'pgsql') {
  657. $connection_string = "dbname='" . addslashes($this->database) . "'";
  658. if ($this->host && $this->host != 'sock:') {
  659. $connection_string .= " host='" . addslashes($this->host) . "'";
  660. }
  661. if ($this->username) {
  662. $connection_string .= " user='" . addslashes($this->username) . "'";
  663. }
  664. if ($this->password) {
  665. $connection_string .= " password='" . addslashes($this->password) . "'";
  666. }
  667. if ($this->port) {
  668. $connection_string .= " port='" . $this->port . "'";
  669. }
  670. if ($this->timeout !== NULL) {
  671. $connection_string .= " connect_timeout='" . $this->timeout . "'";
  672. }
  673. fCore::startErrorCapture();
  674. $this->connection = pg_connect($connection_string, PGSQL_CONNECT_FORCE_NEW);
  675. $errors = fCore::stopErrorCapture();
  676. }
  677. if ($this->extension == 'sqlsrv') {
  678. $options = array(
  679. 'Database' => $this->database
  680. );
  681. if ($this->username !== NULL) {
  682. $options['UID'] = $this->username;
  683. }
  684. if ($this->password !== NULL) {
  685. $options['PWD'] = $this->password;
  686. }
  687. if ($this->timeout !== NULL) {
  688. $options['LoginTimeout'] = $this->timeout;
  689. }
  690. $this->connection = sqlsrv_connect($this->host . ',' . $this->port, $options);
  691. if ($this->connection === FALSE) {
  692. $errors = sqlsrv_errors();
  693. }
  694. sqlsrv_configure('WarningsReturnAsErrors', 0);
  695. }
  696. // Ensure the connection was established
  697. if ($this->connection === FALSE) {
  698. $this->handleConnectionErrors($errors);
  699. }
  700. // Make MySQL act more strict and use UTF-8
  701. if ($this->type == 'mysql') {
  702. $this->execute("SET SQL_MODE = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE'");
  703. $this->execute("SET NAMES 'utf8'");
  704. $this->execute("SET CHARACTER SET utf8");
  705. }
  706. // Make SQLite behave like other DBs for assoc arrays
  707. if ($this->type == 'sqlite') {
  708. $this->execute('PRAGMA short_column_names = 1');
  709. }
  710. // Fix some issues with mssql
  711. if ($this->type == 'mssql') {
  712. if (!isset($this->schema_info['character_set'])) {
  713. $this->determineCharacterSet();
  714. }
  715. $this->execute('SET TEXTSIZE 65536');
  716. $this->execute('SET QUOTED_IDENTIFIER ON');
  717. }
  718. // Make PostgreSQL use UTF-8
  719. if ($this->type == 'postgresql') {
  720. $this->execute("SET NAMES 'UTF8'");
  721. }
  722. // Oracle has different date and timestamp defaults
  723. if ($this->type == 'oracle') {
  724. $this->execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'");
  725. $this->execute("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'");
  726. $this->execute("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZR'");
  727. $this->execute("ALTER SESSION SET NLS_TIME_FORMAT = 'HH24:MI:SS'");
  728. $this->execute("ALTER SESSION SET NLS_TIME_TZ_FORMAT = 'HH24:MI:SS TZR'");
  729. }
  730. }
  731. /**
  732. * Determines the character set of a SQL Server database
  733. *
  734. * @return void
  735. */
  736. protected function determineCharacterSet()
  737. {
  738. $this->schema_info['character_set'] = 'WINDOWS-1252';
  739. $this->schema_info['character_set'] = $this->query("SELECT 'WINDOWS-' + CONVERT(VARCHAR, COLLATIONPROPERTY(CONVERT(NVARCHAR, DATABASEPROPERTYEX(DB_NAME(), 'Collation')), 'CodePage')) AS charset")->fetchScalar();
  740. if ($this->cache) {
  741. $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);
  742. }
  743. }
  744. /**
  745. * Figures out which extension to use for the database type selected
  746. *
  747. * @return void
  748. */
  749. protected function determineExtension()
  750. {
  751. switch ($this->type) {
  752. case 'db2':
  753. if (extension_loaded('ibm_db2')) {
  754. $this->extension = 'ibm_db2';
  755. } elseif (class_exists('PDO', FALSE) && in_array('ibm', PDO::getAvailableDrivers())) {
  756. $this->extension = 'pdo';
  757. } else {
  758. $type = 'DB2';
  759. $exts = 'ibm_db2, pdo_ibm';
  760. }
  761. break;
  762. case 'mssql':
  763. if (extension_loaded('sqlsrv')) {
  764. $this->extension = 'sqlsrv';
  765. } elseif (extension_loaded('mssql')) {
  766. $this->extension = 'mssql';
  767. } elseif (class_exists('PDO', FALSE) && (in_array('dblib', PDO::getAvailableDrivers()) || in_array('mssql', PDO::getAvailableDrivers()))) {
  768. $this->extension = 'pdo';
  769. } else {
  770. $type = 'MSSQL';
  771. $exts = 'mssql, sqlsrv, pdo_dblib (linux), pdo_mssql (windows)';
  772. }
  773. break;
  774. case 'mysql':
  775. if (extension_loaded('mysqli')) {
  776. $this->extension = 'mysqli';
  777. } elseif (class_exists('PDO', FALSE) && in_array('mysql', PDO::getAvailableDrivers())) {
  778. $this->extension = 'pdo';
  779. } elseif (extension_loaded('mysql')) {
  780. $this->extension = 'mysql';
  781. } else {
  782. $type = 'MySQL';
  783. $exts = 'mysql, pdo_mysql, mysqli';
  784. }
  785. break;
  786. case 'oracle':
  787. if (extension_loaded('oci8')) {
  788. $this->extension = 'oci8';
  789. } elseif (class_exists('PDO', FALSE) && in_array('oci', PDO::getAvailableDrivers())) {
  790. $this->extension = 'pdo';
  791. } else {
  792. $type = 'Oracle';
  793. $exts = 'oci8, pdo_oci';
  794. }
  795. break;
  796. case 'postgresql':
  797. if (extension_loaded('pgsql')) {
  798. $this->extension = 'pgsql';
  799. } elseif (class_exists('PDO', FALSE) && in_array('pgsql', PDO::getAvailableDrivers())) {
  800. $this->extension = 'pdo';
  801. } else {
  802. $type = 'PostgreSQL';
  803. $exts = 'pgsql, pdo_pgsql';
  804. }
  805. break;
  806. case 'sqlite':
  807. $sqlite_version = 0;
  808. if (file_exists($this->database)) {
  809. $database_handle = fopen($this->database, 'r');
  810. $database_version = fread($database_handle, 64);
  811. fclose($database_handle);
  812. if (strpos($database_version, 'SQLite format 3') !== FALSE) {
  813. $sqlite_version = 3;
  814. } elseif (strpos($database_version, '** This file contains an SQLite 2.1 database **') !== FALSE) {
  815. $sqlite_version = 2;
  816. } else {
  817. throw new fConnectivityException(
  818. 'The database specified does not appear to be a valid %1$s or %2$s database',
  819. 'SQLite v2.1',
  820. 'v3'
  821. );
  822. }
  823. }
  824. if ((!$sqlite_version || $sqlite_version == 3) && class_exists('PDO', FALSE) && in_array('sqlite', PDO::getAvailableDrivers())) {
  825. $this->extension = 'pdo';
  826. } elseif ($sqlite_version == 3 && (!class_exists('PDO', FALSE) || !in_array('sqlite', PDO::getAvailableDrivers()))) {
  827. throw new fEnvironmentException(
  828. 'The database specified is an %1$s database and the %2$s extension is not installed',
  829. 'SQLite v3',
  830. 'pdo_sqlite'
  831. );
  832. } elseif ((!$sqlite_version || $sqlite_version == 2) && extension_loaded('sqlite')) {
  833. $this->extension = 'sqlite';
  834. } elseif ($sqlite_version == 2 && !extension_loaded('sqlite')) {
  835. throw new fEnvironmentException(
  836. 'The database specified is an %1$s database and the %2$s extension is not installed',
  837. 'SQLite v2.1',
  838. 'sqlite'
  839. );
  840. } else {
  841. $type = 'SQLite';
  842. $exts = 'pdo_sqlite, sqlite';
  843. }
  844. break;
  845. }
  846. if (!$this->extension) {
  847. throw new fEnvironmentException(
  848. 'The server does not have any of the following extensions for %2$s support: %2$s',
  849. $type,
  850. $exts
  851. );
  852. }
  853. }
  854. /**
  855. * Sets the schema info to be cached to the fCache object specified
  856. *
  857. * @param fCache $cache The cache to cache to
  858. * @param string $key_token Internal use only! (this will be used in the cache key to uniquely identify the cache for this fDatabase object)
  859. * @return void
  860. */
  861. public function enableCaching($cache, $key_token=NULL)
  862. {
  863. $this->cache = $cache;
  864. if ($key_token !== NULL) {
  865. $this->cache_prefix = 'fDatabase::' . $this->type . '::' . $key_token . '::';
  866. }
  867. $this->schema_info = $this->cache->get($this->makeCachePrefix() . 'schema_info', array());
  868. }
  869. /**
  870. * Sets if debug messages should be shown
  871. *
  872. * @param boolean $flag If debugging messages should be shown
  873. * @return void
  874. */
  875. public function enableDebugging($flag)
  876. {
  877. $this->debug = (boolean) $flag;
  878. }
  879. /**
  880. * Escapes a value for insertion into SQL
  881. *
  882. * The valid data types are:
  883. *
  884. * - `'blob'`
  885. * - `'boolean'`
  886. * - `'date'`
  887. * - `'float'`
  888. * - `'identifier'`
  889. * - `'integer'`
  890. * - `'string'` (also varchar, char or text)
  891. * - `'varchar'`
  892. * - `'char'`
  893. * - `'text'`
  894. * - `'time'`
  895. * - `'timestamp'`
  896. *
  897. * In addition to being able to specify the data type, you can also pass
  898. * in an SQL statement with data type placeholders in the following form:
  899. *
  900. * - `%l` for a blob
  901. * - `%b` for a boolean
  902. * - `%d` for a date
  903. * - `%f` for a float
  904. * - `%r` for an indentifier (table or column name)
  905. * - `%i` for an integer
  906. * - `%s` for a string
  907. * - `%t` for a time
  908. * - `%p` for a timestamp
  909. *
  910. * Depending on what `$sql_or_type` and `$value` are, the output will be
  911. * slightly different. If `$sql_or_type` is a data type or a single
  912. * placeholder and `$value` is:
  913. *
  914. * - a scalar value - an escaped SQL string is returned
  915. * - an array - an array of escaped SQL strings is returned
  916. *
  917. * If `$sql_or_type` is a SQL string and `$value` is:
  918. *
  919. * - a scalar value - the escaped value is inserted into the SQL string
  920. * - an array - the escaped values are inserted into the SQL string separated by commas
  921. *
  922. * If `$sql_or_type` is a SQL string, it is also possible to pass an array
  923. * of all values as a single parameter instead of one value per parameter.
  924. * An example would look like the following:
  925. *
  926. * {{{
  927. * #!php
  928. * $db->escape(
  929. * "SELECT * FROM users WHERE status = %s AND authorization_level = %s",
  930. * array('Active', 'Admin')
  931. * );
  932. * }}}
  933. *
  934. * @param string $sql_or_type This can either be the data type to escape or an SQL string with a data type placeholder - see method description
  935. * @param mixed $value The value to escape - both single values and arrays of values are supported, see method description for details
  936. * @param mixed ...
  937. * @return mixed The escaped value/SQL or an array of the escaped values
  938. */
  939. public function escape($sql_or_type, $value)
  940. {
  941. $values = array_slice(func_get_args(), 1);
  942. if (sizeof($values) < 1) {
  943. throw new fProgrammerException(
  944. 'No value was specified to escape'
  945. );
  946. }
  947. // Convert all objects into strings
  948. $values = $this->scalarize($values);
  949. $value = array_shift($values);
  950. // Handle single value escaping
  951. $callback = NULL;
  952. switch ($sql_or_type) {
  953. case 'blob':
  954. case '%l':
  955. $callback = $this->escapeBlob;
  956. break;
  957. case 'boolean':
  958. case '%b':
  959. $callback = $this->escapeBoolean;
  960. break;
  961. case 'date':
  962. case '%d':
  963. $callback = $this->escapeDate;
  964. break;
  965. case 'float':
  966. case '%f':
  967. $callback = $this->escapeFloat;
  968. break;
  969. case 'identifier':
  970. case '%r':
  971. $callback = $this->escapeIdentifier;
  972. break;
  973. case 'integer':
  974. case '%i':
  975. $callback = $this->escapeInteger;
  976. break;
  977. case 'string':
  978. case 'varchar':
  979. case 'char':
  980. case 'text':
  981. case '%s':
  982. $callback = $this->escapeString;
  983. break;
  984. case 'time':
  985. case '%t':
  986. $callback = $this->escapeTime;
  987. break;
  988. case 'timestamp':
  989. case '%p':
  990. $callback = $this->escapeTimestamp;
  991. break;
  992. }
  993. if ($callback) {
  994. if (is_array($value)) {
  995. // If the values were passed as a single array, this handles that
  996. if (count($value) == 1 && is_array(current($value))) {
  997. $value = current($value);
  998. }
  999. return array_map($callback, $value);
  1000. }
  1001. return call_user_func($callback, $value);
  1002. }
  1003. // Separate the SQL from quoted values
  1004. $parts = $this->splitSQL($sql_or_type, $placeholders);
  1005. // If the values were passed as a single array, this handles that
  1006. if (count($values) == 0 && is_array($value) && count($value) == $placeholders) {
  1007. $values = $value;
  1008. $value = array_shift($values);
  1009. }
  1010. array_unshift($values, $value);
  1011. $sql = $this->extractStrings($parts, $values);
  1012. return $this->escapeSQL($sql, $values, FALSE);
  1013. }
  1014. /**
  1015. * Escapes a blob for use in SQL, includes surround quotes when appropriate
  1016. *
  1017. * A `NULL` value will be returned as `'NULL'`
  1018. *
  1019. * @param string $value The blob to escape
  1020. * @return string The escaped blob
  1021. */
  1022. private function escapeBlob($value)
  1023. {
  1024. if ($value === NULL) {
  1025. return 'NULL';
  1026. }
  1027. $this->connect();
  1028. if ($this->type == 'db2') {
  1029. return "BLOB(X'" . bin2hex($value) . "')";
  1030. } elseif ($this->type == 'mysql') {
  1031. return "x'" . bin2hex($value) . "'";
  1032. } elseif ($this->type == 'postgresql') {
  1033. $output = '';
  1034. for ($i=0; $i<strlen($value); $i++) {
  1035. $output .= '\\\\' . str_pad(decoct(ord($value[$i])), 3, '0', STR_PAD_LEFT);
  1036. }
  1037. return "E'" . $output . "'";
  1038. } elseif ($this->extension == 'sqlite') {
  1039. return "'" . bin2hex($value) . "'";
  1040. } elseif ($this->type == 'sqlite') {
  1041. return "X'" . bin2hex($value) . "'";
  1042. } elseif ($this->type == 'mssql') {
  1043. return '0x' . bin2hex($value);
  1044. } elseif ($this->type == 'oracle') {
  1045. return "'" . bin2hex($value) . "'";
  1046. }
  1047. }
  1048. /**
  1049. * Escapes a boolean for use in SQL, includes surround quotes when appropriate
  1050. *
  1051. * A `NULL` value will be returned as `'NULL'`
  1052. *
  1053. * @param boolean $value The boolean to escape
  1054. * @return string The database equivalent of the boolean passed
  1055. */
  1056. private function escapeBoolean($value)
  1057. {
  1058. if ($value === NULL) {
  1059. return 'NULL';
  1060. }
  1061. if (in_array($this->type, array('postgresql', 'mysql'))) {
  1062. return ($value) ? 'TRUE' : 'FALSE';
  1063. } elseif (in_array($this->type, array('mssql', 'sqlite', 'db2'))) {
  1064. return ($value) ? "'1'" : "'0'";
  1065. } elseif ($this->type == 'oracle') {
  1066. return ($value) ? '1' : '0';
  1067. }
  1068. }
  1069. /**
  1070. * Escapes a date for use in SQL, includes surrounding quotes
  1071. *
  1072. * A `NULL` or invalid value will be returned as `'NULL'`
  1073. *
  1074. * @param string $value The date to escape
  1075. * @return string The escaped date
  1076. */
  1077. private function escapeDate($value)
  1078. {
  1079. if ($value === NULL) {
  1080. return 'NULL';
  1081. }
  1082. try {
  1083. $value = new fDate($value);
  1084. return "'" . $value->format('Y-m-d') . "'";
  1085. } catch (fValidationException $e) {
  1086. return 'NULL';
  1087. }
  1088. }
  1089. /**
  1090. * Escapes a float for use in SQL
  1091. *
  1092. * A `NULL` value will be returned as `'NULL'`
  1093. *
  1094. * @param float $value The float to escape
  1095. * @return string The escaped float
  1096. */
  1097. private function escapeFloat($value)
  1098. {
  1099. if ($value === NULL) {
  1100. return 'NULL';
  1101. }
  1102. if (!strlen($value)) {
  1103. return 'NULL';
  1104. }
  1105. if (!preg_match('#^[+\-]?([0-9]+(\.([0-9]+)?)?|(\.[0-9]+))$#D', $value)) {
  1106. return 'NULL';
  1107. }
  1108. $value = rtrim($value, '.');
  1109. $value = preg_replace('#(?<![0-9])\.#', '0.', $value);
  1110. return (string) $value;
  1111. }
  1112. /**
  1113. * Escapes an identifier for use in SQL, necessary for reserved words
  1114. *
  1115. * @param string $value The identifier to escape
  1116. * @return string The escaped identifier
  1117. */
  1118. private function escapeIdentifier($value)
  1119. {
  1120. $value = '"' . str_replace(
  1121. array('"', '.'),
  1122. array('', '"."'),
  1123. $value
  1124. ) . '"';
  1125. if (in_array($this->type, array('oracle', 'db2'))) {
  1126. $value = strtoupper($value);
  1127. }
  1128. return $value;
  1129. }
  1130. /**
  1131. * Escapes an integer for use in SQL
  1132. *
  1133. * A `NULL` or invalid value will be returned as `'NULL'`
  1134. *
  1135. * @param integer $value The integer to escape
  1136. * @return string The escaped integer
  1137. */
  1138. private function escapeInteger($value)
  1139. {
  1140. if ($value === NULL) {
  1141. return 'NULL';
  1142. }
  1143. if (!strlen($value)) {
  1144. return 'NULL';
  1145. }
  1146. if (!preg_match('#^([+\-]?[0-9]+)(\.[0-9]*)?$#D', $value, $matches)) {
  1147. return 'NULL';
  1148. }
  1149. return str_replace('+', '', $matches[1]);
  1150. }
  1151. /**
  1152. * Escapes a string for use in SQL, includes surrounding quotes
  1153. *
  1154. * A `NULL` value will be returned as `'NULL'`.
  1155. *
  1156. * @param string $value The string to escape
  1157. * @return string The escaped string
  1158. */
  1159. private function escapeString($value)
  1160. {
  1161. if ($value === NULL) {
  1162. return 'NULL';
  1163. }
  1164. $this->connect();
  1165. if ($this->type == 'db2') {
  1166. return "'" . str_replace("'", "''", $value) . "'";
  1167. } elseif ($this->extension == 'mysql') {
  1168. return "'" . mysql_real_escape_string($value, $this->connection) . "'";
  1169. } elseif ($this->extension == 'mysqli') {
  1170. return "'" . mysqli_real_escape_string($this->connection, $value) . "'";
  1171. } elseif ($this->extension == 'pgsql') {
  1172. return "'" . pg_escape_string($value) . "'";
  1173. } elseif ($this->extension == 'sqlite') {
  1174. return "'" . sqlite_escape_string($value) . "'";
  1175. } elseif ($this->type == 'oracle') {
  1176. return "'" . str_replace("'", "''", $value) . "'";
  1177. } elseif ($this->type == 'mssql') {
  1178. // If there are any non-ASCII characters, we need to escape
  1179. if (preg_match('#[^\x00-\x7F]#', $value)) {
  1180. preg_match_all('#.|^\z#us', $value, $characters);
  1181. $output = "";
  1182. $last_type = NULL;
  1183. foreach ($characters[0] as $character) {
  1184. if (strlen($character) > 1) {
  1185. $b = array_map('ord', str_split($character));
  1186. switch (strlen($character)) {
  1187. case 2:
  1188. $bin = substr(decbin($b[0]), 3) .
  1189. substr(decbin($b[1]), 2);
  1190. break;
  1191. case 3:
  1192. $bin = substr(decbin($b[0]), 4) .
  1193. substr(decbin($b[1]), 2) .
  1194. substr(decbin($b[2]), 2);
  1195. break;
  1196. // If it is a 4-byte character, MSSQL can't store it
  1197. // so instead store a ?
  1198. default:
  1199. $output .= '?';
  1200. continue;
  1201. }
  1202. if ($last_type == 'nchar') {
  1203. $output .= '+';
  1204. } elseif ($last_type == 'char') {
  1205. $output .= "'+";
  1206. }
  1207. $output .= "NCHAR(" . bindec($bin) . ")";
  1208. $last_type = 'nchar';
  1209. } else {
  1210. if (!$last_type) {
  1211. $output .= "'";
  1212. } elseif ($last_type == 'nchar') {
  1213. $output .= "+'";
  1214. }
  1215. $output .= $character;
  1216. // Escape single quotes
  1217. if ($character == "'") {
  1218. $output .= "'";
  1219. }
  1220. $last_type = 'char';
  1221. }
  1222. }
  1223. if ($last_type == 'char') {
  1224. $output .= "'";
  1225. } elseif (!$last_type) {
  1226. $output .= "''";
  1227. }
  1228. // ASCII text is normal
  1229. } else {
  1230. $output = "'" . str_replace("'", "''", $value) . "'";
  1231. }
  1232. # a \ before a \r\n has to be escaped with another \
  1233. return preg_replace('#(?<!\\\\)\\\\(?=\r\n)#', '\\\\\\\\', $output);
  1234. } elseif ($this->extension == 'pdo') {
  1235. return $this->connection->quote($value);
  1236. }
  1237. }
  1238. /**
  1239. * Takes a SQL string and an array of values and replaces the placeholders with the value
  1240. *
  1241. * @param string $sql The SQL string containing placeholders
  1242. * @param array $values An array of values to escape into the SQL
  1243. * @param boolean $unescape_percent If %% should be translated to % - this should only be done once processing of the string is done
  1244. * @return string The SQL with the values escaped into it
  1245. */
  1246. private function escapeSQL($sql, $values, $unescape_percent)
  1247. {
  1248. $original_sql = $sql;
  1249. $pieces = preg_split('#(?<!%)(%[lbdfristp])\b#', $sql, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  1250. $sql = '';
  1251. $value = array_shift($values);
  1252. $missing_values = -1;
  1253. foreach ($pieces as $piece) {
  1254. switch ($piece) {
  1255. case '%l':
  1256. $callback = $this->escapeBlob;
  1257. break;
  1258. case '%b':
  1259. $callback = $this->escapeBoolean;
  1260. break;
  1261. case '%d':
  1262. $callback = $this->escapeDate;
  1263. break;
  1264. case '%f':
  1265. $callback = $this->escapeFloat;
  1266. break;
  1267. case '%r':
  1268. $callback = $this->escapeIdentifier;
  1269. break;
  1270. case '%i':
  1271. $callback = $this->escapeInteger;
  1272. break;
  1273. case '%s':
  1274. $callback = $this->escapeString;
  1275. break;
  1276. case '%t':
  1277. $callback = $this->escapeTime;
  1278. break;
  1279. case '%p':
  1280. $callback = $this->escapeTimestamp;
  1281. break;
  1282. default:
  1283. if ($unescape_percent) {
  1284. $piece = str_replace('%%', '%', $piece);
  1285. }
  1286. $sql .= $piece;
  1287. continue 2;
  1288. }
  1289. if (is_array($value)) {
  1290. $sql .= join(', ', array_map($callback, $value));
  1291. } else {
  1292. $sql .= call_user_func($callback, $value);
  1293. }
  1294. if (sizeof($values)) {
  1295. $value = array_shift($values);
  1296. } else {
  1297. $value = NULL;
  1298. $missing_values++;
  1299. }
  1300. }
  1301. if ($missing_values > 0) {
  1302. throw new fProgrammerException(
  1303. '%1$s value(s) are missing for the placeholders in: %2$s',
  1304. $missing_values,
  1305. $original_sql
  1306. );
  1307. }
  1308. if (sizeof($values)) {
  1309. throw new fProgrammerException(
  1310. '%1$s extra value(s) were passed for the placeholders in: %2$s',
  1311. sizeof($values),
  1312. $original_sql
  1313. );
  1314. }
  1315. return $sql;
  1316. }
  1317. /**
  1318. * Escapes a time for use in SQL, includes surrounding quotes
  1319. *
  1320. * A `NULL` or invalid value will be returned as `'NULL'`
  1321. *
  1322. * @param string $value The time to escape
  1323. * @return string The escaped time
  1324. */
  1325. private function escapeTime($value)
  1326. {
  1327. if ($value === NULL) {
  1328. return 'NULL';
  1329. }
  1330. try {
  1331. $value = new fTime($value);
  1332. if ($this->type == 'mssql' || $this->type == 'oracle') {
  1333. return "'" . $value->format('1970-01-01 H:i:s') . "'";
  1334. }
  1335. return "'" . $value->format('H:i:s') . "'";
  1336. } catch (fValidationException $e) {
  1337. return 'NULL';
  1338. }
  1339. }
  1340. /**
  1341. * Escapes a timestamp for use in SQL, includes surrounding quotes
  1342. *
  1343. * A `NULL` or invalid value will be returned as `'NULL'`
  1344. *
  1345. * @param string $value The timestamp to escape
  1346. * @return string The escaped timestamp
  1347. */
  1348. private function escapeTimestamp($value)
  1349. {
  1350. if ($value === NULL) {
  1351. return 'NULL';
  1352. }
  1353. try {
  1354. $value = new fTimestamp($value);
  1355. return "'" . $value->format('Y-m-d H:i:s') . "'";
  1356. } catch (fValidationException $e) {
  1357. return 'NULL';
  1358. }
  1359. }
  1360. /**
  1361. * Executes one or more SQL queries without returning any results
  1362. *
  1363. * @param string|fStatement $statement One or more SQL statements in a string or an fStatement prepared statement
  1364. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  1365. * @param mixed ...
  1366. * @return void
  1367. */
  1368. public function execute($statement)
  1369. {
  1370. $args = func_get_args();
  1371. $params = array_slice($args, 1);
  1372. if (is_object($statement)) {
  1373. return $this->run($statement, NULL, $params);
  1374. }
  1375. $queries = $this->preprocess($statement, $params, FALSE);
  1376. $output = array();
  1377. foreach ($queries as $query) {
  1378. $this->run($query);
  1379. }
  1380. }
  1381. /**
  1382. * Pulls quoted strings out into the values array for simpler processing
  1383. *
  1384. * @param array $parts The parts of the SQL - alternating SQL and quoted strings
  1385. * @param array &$values The value to be escaped into the SQL
  1386. * @return string The SQL with all quoted string values extracted into the `$values` array
  1387. */
  1388. private function extractStrings($parts, &$values)
  1389. {
  1390. $sql = '';
  1391. $value_number = 0;
  1392. foreach ($parts as $part) {
  1393. // We leave blank strings in because Oracle treats them like NULL
  1394. if ($part[0] == "'" && $part != "''") {
  1395. $sql .= '%s';
  1396. $value = str_replace("''", "'", substr($part, 1, -1));
  1397. if ($this->type == 'postgresql') {
  1398. $value = str_replace('\\\\', '\\', $value);
  1399. }
  1400. $values = array_merge(
  1401. array_slice($values, 0, $value_number),
  1402. array($value),
  1403. array_slice($values, $value_number)
  1404. );
  1405. $value_number++;
  1406. } else {
  1407. $value_number += preg_match_all('#(?<!%)%[lbdfristp]\b#', $part, $trash);
  1408. unset($trash);
  1409. $sql .= $part;
  1410. }
  1411. }
  1412. return $sql;
  1413. }
  1414. /**
  1415. * Returns the database connection resource or object
  1416. *
  1417. * @return mixed The database connection
  1418. */
  1419. public function getConnection()
  1420. {
  1421. $this->connect();
  1422. return $this->connection;
  1423. }
  1424. /**
  1425. * Gets the name of the database currently connected to
  1426. *
  1427. * @return string The name of the database currently connected to
  1428. */
  1429. public function getDatabase()
  1430. {
  1431. return $this->database;
  1432. }
  1433. /**
  1434. * Gets the php extension being used
  1435. *
  1436. * @internal
  1437. *
  1438. * @return string The php extension used for database interaction
  1439. */
  1440. public function getExtension()
  1441. {
  1442. return $this->extension;
  1443. }
  1444. /**
  1445. * Gets the host for this database
  1446. *
  1447. * @return string The host
  1448. */
  1449. public function getHost()
  1450. {
  1451. return $this->host;
  1452. }
  1453. /**
  1454. * Gets the port for this database
  1455. *
  1456. * @return string The port
  1457. */
  1458. public function getPort()
  1459. {
  1460. return $this->port;
  1461. }
  1462. /**
  1463. * Gets the fSQLTranslation object used for translated queries
  1464. *
  1465. * @return fSQLTranslation The SQL translation object
  1466. */
  1467. public function getSQLTranslation()
  1468. {
  1469. if (!$this->translation) { new fSQLTranslation($this); }
  1470. return $this->translation;
  1471. }
  1472. /**
  1473. * Gets the database type
  1474. *
  1475. * @return string The database type: `'mssql'`, `'mysql'`, `'postgresql'` or `'sqlite'`
  1476. */
  1477. public function getType()
  1478. {
  1479. return $this->type;
  1480. }
  1481. /**
  1482. * Gets the username for this database
  1483. *
  1484. * @return string The username
  1485. */
  1486. public function getUsername()
  1487. {
  1488. return $this->username;
  1489. }
  1490. /**
  1491. * Gets the version of the database system
  1492. *
  1493. * @return string The database system version
  1494. */
  1495. public function getVersion()
  1496. {
  1497. if (isset($this->schema_info['version'])) {
  1498. return $this->schema_info['version'];
  1499. }
  1500. switch ($this->type) {
  1501. case 'db2':
  1502. $sql = "SELECT REPLACE(service_level, 'DB2 v', '') FROM TABLE (sysproc.env_get_inst_info()) AS x";
  1503. break;
  1504. case 'mssql':
  1505. $sql = "SELECT CAST(SERVERPROPERTY('ProductVersion') AS VARCHAR(500)) AS ProductVersion";
  1506. break;
  1507. case 'mysql':
  1508. $sql = "SELECT version()";
  1509. break;
  1510. case 'oracle':
  1511. $sql = "SELECT version FROM product_component_version";
  1512. break;
  1513. case 'postgresql':
  1514. $sql = "SELECT regexp_replace(version(), E'^PostgreSQL +([0-9]+(\\\\.[0-9]+)*).*$', E'\\\\1')";
  1515. break;
  1516. case 'sqlite':
  1517. $sql = "SELECT sqlite_version()";
  1518. break;
  1519. }
  1520. $this->schema_info['version'] = preg_replace('#-?[a-z].*$#Di', '', $this->query($sql)->fetchScalar());
  1521. return $this->schema_info['version'];
  1522. }
  1523. /**
  1524. * Will grab the auto incremented value from the last query (if one exists)
  1525. *
  1526. * @param fResult $result The result object for the query
  1527. * @param mixed $resource Only applicable for `pdo`, `oci8` and `sqlsrv` extentions or `mysqli` prepared statements - this is either the `PDOStatement` object, `mysqli_stmt` object or the `oci8` or `sqlsrv` resource
  1528. * @return void
  1529. */
  1530. private function handleAutoIncrementedValue($result, $resource=NULL)
  1531. {
  1532. if (!preg_match('#^\s*INSERT\s+(?:INTO\s+)?(?:`|"|\[)?(["\w.]+)(?:`|"|\])?#i', $result->getSQL(), $table_match)) {
  1533. $result->setAutoIncrementedValue(NULL);
  1534. return;
  1535. }
  1536. $quoted_table = $table_match[1];
  1537. $table = str_replace('"', '', strtolower($table_match[1]));
  1538. $insert_id = NULL;
  1539. if ($this->type == 'oracle') {
  1540. if (!isset($this->schema_info['sequences'])) {
  1541. $sql = "SELECT
  1542. LOWER(OWNER) AS \"SCHEMA\",
  1543. LOWER(TABLE_NAME) AS \"TABLE\",
  1544. TRIGGER_BODY
  1545. FROM
  1546. ALL_TRIGGERS
  1547. WHERE
  1548. TRIGGERING_EVENT LIKE 'INSERT%' AND
  1549. STATUS = 'ENABLED' AND
  1550. TRIGGER_NAME NOT LIKE 'BIN\$%' AND
  1551. OWNER NOT IN (
  1552. 'SYS',
  1553. 'SYSTEM',
  1554. 'OUTLN',
  1555. 'ANONYMOUS',
  1556. 'AURORA\$ORB\$UNAUTHENTICATED',
  1557. 'AWR_STAGE',
  1558. 'CSMIG',
  1559. 'CTXSYS',
  1560. 'DBSNMP',
  1561. 'DIP',
  1562. 'DMSYS',
  1563. 'DSSYS',
  1564. 'EXFSYS',
  1565. 'FLOWS_020100',
  1566. 'FLOWS_FILES',
  1567. 'LBACSYS',
  1568. 'MDSYS',
  1569. 'ORACLE_OCM',
  1570. 'ORDPLUGINS',
  1571. 'ORDSYS',
  1572. 'PERFSTAT',
  1573. 'TRACESVR',
  1574. 'TSMSYS',
  1575. 'XDB'
  1576. )";
  1577. $this->schema_info['sequences'] = array();
  1578. foreach ($this->query($sql) as $row) {
  1579. if (preg_match('#SELECT\s+(["\w.]+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) {
  1580. $table_name = $row['table'];
  1581. if ($row['schema'] != strtolower($this->username)) {
  1582. $table_name = $row['schema'] . '.' . $table_name;
  1583. }
  1584. $this->schema_info['sequences'][$table_name] = array('sequence' => $matches[1], 'column' => str_replace('"', '', $matches[2]));
  1585. }
  1586. }
  1587. if ($this->cache) {
  1588. $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);
  1589. }
  1590. }
  1591. if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+"?' . preg_quote($quoted_table, '#') . '"?\s+\([^\)]*?(\b|")' . preg_quote($this->schema_info['sequences'][$table]['column'], '#') . '(\b|")#i', $result->getSQL())) {
  1592. return;
  1593. }
  1594. $insert_id_sql = "SELECT " . $this->schema_info['sequences'][$table]['sequence'] . ".currval AS INSERT_ID FROM dual";
  1595. }
  1596. if ($this->type == 'postgresql') {
  1597. if (!isset($this->schema_info['sequences'])) {
  1598. $sql = "SELECT
  1599. pg_namespace.nspname AS \"schema\",
  1600. pg_class.relname AS \"table\",
  1601. pg_attribute.attname AS column
  1602. FROM
  1603. pg_attribute INNER JOIN
  1604. pg_class ON pg_attribute.attrelid = pg_class.oid INNER JOIN
  1605. pg_namespace ON pg_class.relnamespace = pg_namespace.oid INNER JOIN
  1606. pg_attrdef ON pg_class.oid = pg_attrdef.adrelid AND pg_attribute.attnum = pg_attrdef.adnum
  1607. WHERE
  1608. NOT pg_attribute.attisdropped AND
  1609. pg_attrdef.adsrc LIKE 'nextval(%'";
  1610. $this->schema_info['sequences'] = array();
  1611. foreach ($this->query($sql) as $row) {
  1612. $table_name = strtolower($row['table']);
  1613. if ($row['schema'] != 'public') {
  1614. $table_name = $row['schema'] . '.' . $table_name;
  1615. }
  1616. $this->schema_info['sequences'][$table_name] = $row['column'];
  1617. }
  1618. if ($this->cache) {
  1619. $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);
  1620. }
  1621. }
  1622. if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+"?' . preg_quote($quoted_table, '#') . '"?\s+\([^\)]*?(\b|")' . preg_quote($this->schema_info['sequences'][$table], '#') . '(\b|")#i', $result->getSQL())) {
  1623. return;
  1624. }
  1625. }
  1626. if ($this->extension == 'ibm_db2') {
  1627. $insert_id_res = db2_exec($this->connection, "SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1");
  1628. $insert_id_row = db2_fetch_assoc($insert_id_res);
  1629. $insert_id = current($insert_id_row);
  1630. db2_free_result($insert_id_res);
  1631. } elseif ($this->extension == 'mssql') {
  1632. $insert_id_res = mssql_query("SELECT @@IDENTITY AS insert_id", $this->connection);
  1633. $insert_id = mssql_result($insert_id_res, 0, 'insert_id');
  1634. mssql_free_result($insert_id_res);
  1635. } elseif ($this->extension == 'mysql') {
  1636. $insert_id = mysql_insert_id($this->connection);
  1637. } elseif ($this->extension == 'mysqli') {
  1638. if (is_object($resource)) {
  1639. $insert_id = mysqli_stmt_insert_id($resource);
  1640. } else {
  1641. $insert_id = mysqli_insert_id($this->connection);
  1642. }
  1643. } elseif ($this->extension == 'oci8') {
  1644. $oci_statement = oci_parse($this->connection, $insert_id_sql);
  1645. oci_execute($oci_statement, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS);
  1646. $insert_id_row = oci_fetch_array($oci_statement, OCI_ASSOC);
  1647. $insert_id = $insert_id_row['INSERT_ID'];
  1648. oci_free_statement($oci_statement);
  1649. } elseif ($this->extension == 'pgsql') {
  1650. $insert_id_res = pg_query($this->connection, "SELECT lastval()");
  1651. $insert_id_row = pg_fetch_assoc($insert_id_res);
  1652. $insert_id = array_shift($insert_id_row);
  1653. pg_free_result($insert_id_res);
  1654. } elseif ($this->extension == 'sqlite') {
  1655. $insert_id = sqlite_last_insert_rowid($this->connection);
  1656. } elseif ($this->extension == 'sqlsrv') {
  1657. $insert_id_res = sqlsrv_query($this->connection, "SELECT @@IDENTITY AS insert_id");
  1658. $insert_id_row = sqlsrv_fetch_array($insert_id_res, SQLSRV_FETCH_ASSOC);
  1659. $insert_id = $insert_id_row['insert_id'];
  1660. sqlsrv_free_stmt($insert_id_res);
  1661. } elseif ($this->extension == 'pdo') {
  1662. switch ($this->type) {
  1663. case 'db2':
  1664. $insert_id_statement = $this->connection->query("SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1");
  1665. $insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
  1666. $insert_id = array_shift($insert_id_row);
  1667. $insert_id_statement->closeCursor();
  1668. unset($insert_id_statement);
  1669. break;
  1670. case 'mssql':
  1671. try {
  1672. $insert_id_statement = $this->connection->query("SELECT @@IDENTITY AS insert_id");
  1673. if (!$insert_id_statement) {
  1674. throw new Exception();
  1675. }
  1676. $insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
  1677. $insert_id = array_shift($insert_id_row);
  1678. } catch (Exception $e) {
  1679. // If there was an error we don't have an insert id
  1680. }
  1681. break;
  1682. case 'oracle':
  1683. try {
  1684. $insert_id_statement = $this->connection->query($insert_id_sql);
  1685. if (!$insert_id_statement) {
  1686. throw new Exception();
  1687. }
  1688. $insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
  1689. $insert_id = array_shift($insert_id_row);
  1690. } catch (Exception $e) {
  1691. // If there was an error we don't have an insert id
  1692. }
  1693. break;
  1694. case 'postgresql':
  1695. $insert_id_statement = $this->connection->query("SELECT lastval()");
  1696. $insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
  1697. $insert_id = array_shift($insert_id_row);
  1698. $insert_id_statement->closeCursor();
  1699. unset($insert_id_statement);
  1700. break;
  1701. case 'mysql':
  1702. $insert_id = $this->connection->lastInsertId();
  1703. break;
  1704. case 'sqlite':
  1705. $insert_id = $this->connection->lastInsertId();
  1706. break;
  1707. }
  1708. }
  1709. $result->setAutoIncrementedValue($insert_id);
  1710. }
  1711. /**
  1712. * Handles connection errors
  1713. *
  1714. * @param array|string $errors An array or string of error information
  1715. * @return void
  1716. */
  1717. private function handleConnectionErrors($errors)
  1718. {
  1719. if (is_string($errors)) {
  1720. $error = $errors;
  1721. } else {
  1722. $new_errors = array();
  1723. foreach ($errors as $error) {
  1724. $new_errors[] = isset($error['message']) ? $error['message'] : $error['string'];
  1725. }
  1726. $error = join("\n", $new_errors);
  1727. }
  1728. $connection_regexes = array(
  1729. 'db2' => '#selectForConnectTimeout#',
  1730. 'mssql' => '#(Connection refused|Can\'t assign requested address|Server is unavailable or does not exist|unable to connect|target machine actively refused it)#i',
  1731. 'mysql' => '#(Can\'t connect to MySQL server|Lost connection to MySQL server at|Connection refused|Operation timed out|host has failed to respond)#',
  1732. 'oracle' => '#(Connection refused|Can\'t assign requested address|no listener|unable to connect to)#',
  1733. 'postgresql' => '#(Connection refused|timeout expired|Network is unreachable|Can\'t assign requested address)#'
  1734. );
  1735. $authentication_regexes = array(
  1736. 'db2' => '#USERNAME AND/OR PASSWORD INVALID#',
  1737. 'mssql' => '#(Login incorrect|Adaptive Server connection failed|Login failed for user(?!.*Cannot open database))#is',
  1738. 'mysql' => '#Access denied for user#',
  1739. 'oracle' => '#invalid username/password#',
  1740. 'postgresql' => '#authentication failed#'
  1741. );
  1742. $database_regexes = array(
  1743. 'db2' => '#database alias or database name#',
  1744. 'mssql' => '#Could not locate entry in sysdatabases for database|Cannot open database|General SQL Server error: Check messages from the SQL Server#',
  1745. 'mysql' => '#Unknown database#',
  1746. 'oracle' => '#does not currently know of service requested#',
  1747. 'postgresql' => '#database "[^"]+" does not exist#'
  1748. );
  1749. if (isset($authentication_regexes[$this->type]) && preg_match($authentication_regexes[$this->type], $error)) {
  1750. throw new fAuthorizationException(
  1751. 'Unable to connect to database - login credentials refused'
  1752. );
  1753. } elseif (isset($database_regexes[$this->type]) && preg_match($database_regexes[$this->type], $error)) {
  1754. throw new fNotFoundException(
  1755. 'Unable to connect to database - database specified not found'
  1756. );
  1757. }
  1758. // Provide a better error message if we can detect the hostname does not exist
  1759. if (!preg_match('#^\d+\.\d+\.\d+\.\d+$#', $this->host)) {
  1760. $ip_address = gethostbyname($this->host);
  1761. if ($ip_address == $this->host) {
  1762. throw new fConnectivityException(
  1763. 'Unable to connect to database - hostname not found'
  1764. );
  1765. }
  1766. }
  1767. if (isset($connection_regexes[$this->type]) && preg_match($connection_regexes[$this->type], $error)) {
  1768. throw new fConnectivityException(
  1769. 'Unable to connect to database - connection refused or timed out'
  1770. );
  1771. }
  1772. throw new fConnectivityException(
  1773. "Unable to connect to database - unknown error:\n%1\$s",
  1774. $error
  1775. );
  1776. }
  1777. /**
  1778. * Handles a PHP error to extract error information for the mssql extension
  1779. *
  1780. * @param array $errors An array of error information from fCore::stopErrorCapture()
  1781. * @return void
  1782. */
  1783. private function handleErrors($errors)
  1784. {
  1785. if ($this->extension != 'mssql') {
  1786. return;
  1787. }
  1788. foreach ($errors as $error) {
  1789. if (substr($error['string'], 0, 14) == 'mssql_query():') {
  1790. if ($this->error) {
  1791. $this->error .= " ";
  1792. }
  1793. $this->error .= preg_replace('#^mssql_query\(\): ([^:]+: )?#', '', $error['string']);
  1794. }
  1795. }
  1796. }
  1797. /**
  1798. * Makes sure each database and extension handles BEGIN, COMMIT and ROLLBACK
  1799. *
  1800. * @param string|fStatement &$statement The SQL to check for a transaction query
  1801. * @param string $result_class The type of result object to create
  1802. * @return mixed `FALSE` if normal processing should continue, otherwise an object of the type $result_class
  1803. */
  1804. private function handleTransactionQueries(&$statement, $result_class)
  1805. {
  1806. if (is_object($statement)) {
  1807. $sql = $statement->getSQL();
  1808. } else {
  1809. $sql = $statement;
  1810. }
  1811. // SQL Server supports transactions, but the statements are slightly different.
  1812. // For the interest of convenience, we do simple transaction right here.
  1813. if ($this->type == 'mssql') {
  1814. if (preg_match('#^\s*(BEGIN|START(\s+TRANSACTION)?)\s*$#i', $sql)) {
  1815. $statement = 'BEGIN TRANSACTION';
  1816. } elseif (preg_match('#^\s*SAVEPOINT\s+("?\w+"?)\s*$#i', $sql, $match)) {
  1817. $statement = 'SAVE TRANSACTION ' . $match[1];
  1818. } elseif (preg_match('#^\s*ROLLBACK\s+TO\s+SAVEPOINT\s+("?\w+"?)\s*$#i', $sql, $match)) {
  1819. $statement = 'ROLLBACK TRANSACTION ' . $match[1];
  1820. }
  1821. }
  1822. $begin = FALSE;
  1823. $commit = FALSE;
  1824. $rollback = FALSE;
  1825. // Track transactions since most databases don't support nesting
  1826. if (preg_match('#^\s*(BEGIN|START)(\s+(TRAN|TRANSACTION|WORK))?\s*$#iD', $sql)) {
  1827. if ($this->inside_transaction) {
  1828. throw new fProgrammerException('A transaction is already in progress');
  1829. }
  1830. $this->inside_transaction = TRUE;
  1831. $begin = TRUE;
  1832. } elseif (preg_match('#^\s*COMMIT(\s+(TRAN|TRANSACTION|WORK))?\s*$#iD', $sql)) {
  1833. if (!$this->inside_transaction) {
  1834. throw new fProgrammerException('There is no transaction in progress');
  1835. }
  1836. $this->inside_transaction = FALSE;
  1837. $commit = TRUE;
  1838. } elseif (preg_match('#^\s*ROLLBACK(\s+(TRAN|TRANSACTION|WORK))?\s*$#iD', $sql)) {
  1839. if (!$this->inside_transaction) {
  1840. throw new fProgrammerException('There is no transaction in progress');
  1841. }
  1842. $this->inside_transaction = FALSE;
  1843. $rollback = TRUE;
  1844. // MySQL needs to use this construct for starting transactions when using LOCK tables
  1845. } elseif ($this->type == 'mysql' && preg_match('#^\s*SET\s+autocommit\s*=\s*(0|1)#i', $sql, $match)) {
  1846. $this->inside_transaction = TRUE;
  1847. if ($match[1] == '0') {
  1848. $this->schema_info['mysql_autocommit'] = TRUE;
  1849. } else {
  1850. unset($this->schema_info['mysql_autocommit']);
  1851. }
  1852. // We have to track LOCK TABLES for MySQL because UNLOCK TABLES only implicitly commits if LOCK TABLES was used
  1853. } elseif ($this->type == 'mysql' && preg_match('#^\s*LOCK\s+TABLES#i', $sql)) {
  1854. // This command always implicitly commits
  1855. $this->inside_transaction = FALSE;
  1856. $this->schema_info['mysql_lock_tables'] = TRUE;
  1857. // MySQL has complex handling of UNLOCK TABLES
  1858. } elseif ($this->type == 'mysql' && preg_match('#^\s*UNLOCK\s+TABLES#i', $sql)) {
  1859. // This command only implicitly commits if LOCK TABLES was used
  1860. if (isset($this->schema_info['mysql_lock_tables'])) {
  1861. $this->inside_transaction = FALSE;
  1862. }
  1863. unset($this->schema_info['mysql_lock_tables']);
  1864. // These databases issue implicit commit commands when the following statements are run
  1865. } elseif ($this->type == 'mysql' && preg_match('#^\s*(ALTER|CREATE(?!\s+TEMPORARY)|DROP|RENAME|TRUNCATE|LOAD|UNLOCK|GRANT|REVOKE|SET\s+PASSWORD|CACHE|ANALYSE|CHECK|OPTIMIZE|REPAIR|FLUSH|RESET)\b#i', $sql)) {
  1866. $this->inside_transaction = FALSE;
  1867. } elseif ($this->type == 'oracle' && preg_match('#^\s*(CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|REPLACE|ANALYZE|AUDIT|COMMENT)\b#i', $sql)) {
  1868. $this->inside_transaction = FALSE;
  1869. } elseif ($this->type == 'db2' && preg_match('#^\s*CALL\s+SYSPROC\.ADMIN_CMD\(\'REORG\s+TABLE\b#i', $sql)) {
  1870. $this->inside_transaction = FALSE;
  1871. // It appears PDO tracks the transactions, but doesn't know about implicit commits
  1872. if ($this->extension == 'pdo') {
  1873. $this->connection->commit();
  1874. }
  1875. }
  1876. // If MySQL autocommit it set to 0 a new transaction is automatically started
  1877. if (!empty($this->schema_info['mysql_autocommit'])) {
  1878. $this->inside_transaction = TRUE;
  1879. }
  1880. if (!$begin && !$commit && !$rollback) {
  1881. return FALSE;
  1882. }
  1883. // The PDO, OCI8 and SQLSRV extensions require special handling through methods and functions
  1884. $is_pdo = $this->extension == 'pdo';
  1885. $is_oci = $this->extension == 'oci8';
  1886. $is_sqlsrv = $this->extension == 'sqlsrv';
  1887. $is_ibm_db2 = $this->extension == 'ibm_db2';
  1888. if (!$is_pdo && !$is_oci && !$is_sqlsrv && !$is_ibm_db2) {
  1889. return FALSE;
  1890. }
  1891. $this->statement = $statement;
  1892. // PDO seems to act weird if you try to start transactions through a normal query call
  1893. if ($is_pdo) {
  1894. try {
  1895. $is_mssql = $this->type == 'mssql';
  1896. $is_oracle = $this->type == 'oracle';
  1897. if ($begin) {
  1898. // The SQL Server PDO object hasn't implemented transactions
  1899. if ($is_mssql) {
  1900. $this->connection->exec('BEGIN TRANSACTION');
  1901. } elseif ($is_oracle) {
  1902. $this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE);
  1903. } else {
  1904. $this->connection->beginTransaction();
  1905. }
  1906. } elseif ($commit) {
  1907. if ($is_mssql) {
  1908. $this->connection->exec('COMMIT');
  1909. } elseif ($is_oracle) {
  1910. $this->connection->exec('COMMIT');
  1911. $this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
  1912. } else {
  1913. $this->connection->commit();
  1914. }
  1915. } elseif ($rollback) {
  1916. if ($is_mssql) {
  1917. $this->connection->exec('ROLLBACK');
  1918. } elseif ($is_oracle) {
  1919. $this->connection->exec('ROLLBACK');
  1920. $this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
  1921. } else {
  1922. $this->connection->rollBack();
  1923. }
  1924. }
  1925. } catch (Exception $e) {
  1926. $db_type_map = array(
  1927. 'db2' => 'DB2',
  1928. 'mssql' => 'MSSQL',
  1929. 'mysql' => 'MySQL',
  1930. 'oracle' => 'Oracle',
  1931. 'postgresql' => 'PostgreSQL',
  1932. 'sqlite' => 'SQLite'
  1933. );
  1934. throw new fSQLException(
  1935. '%1$s error (%2$s) in %3$s',
  1936. $db_type_map[$this->type],
  1937. $e->getMessage(),
  1938. $sql
  1939. );
  1940. }
  1941. } elseif ($is_oci) {
  1942. if ($commit) {
  1943. oci_commit($this->connection);
  1944. } elseif ($rollback) {
  1945. oci_rollback($this->connection);
  1946. }
  1947. } elseif ($is_sqlsrv) {
  1948. if ($begin) {
  1949. sqlsrv_begin_transaction($this->connection);
  1950. } elseif ($commit) {
  1951. sqlsrv_commit($this->connection);
  1952. } elseif ($rollback) {
  1953. sqlsrv_rollback($this->connection);
  1954. }
  1955. } elseif ($is_ibm_db2) {
  1956. if ($begin) {
  1957. db2_autocommit($this->connection, FALSE);
  1958. } elseif ($commit) {
  1959. db2_commit($this->connection);
  1960. db2_autocommit($this->connection, TRUE);
  1961. } elseif ($rollback) {
  1962. db2_rollback($this->connection);
  1963. db2_autocommit($this->connection, TRUE);
  1964. }
  1965. }
  1966. if ($result_class) {
  1967. $result = new $result_class($this);
  1968. $result->setSQL($sql);
  1969. $result->setResult(TRUE);
  1970. return $result;
  1971. }
  1972. return TRUE;
  1973. }
  1974. /**
  1975. * Injects an fSQLTranslation object to handle translation
  1976. *
  1977. * @internal
  1978. *
  1979. * @param fSQLTranslation $sql_translation The SQL translation object
  1980. * @return void
  1981. */
  1982. public function inject($sql_translation)
  1983. {
  1984. $this->translation = $sql_translation;
  1985. }
  1986. /**
  1987. * Will indicate if a transaction is currently in progress
  1988. *
  1989. * @return boolean If a transaction has been started and not yet rolled back or committed
  1990. */
  1991. public function isInsideTransaction()
  1992. {
  1993. return $this->inside_transaction;
  1994. }
  1995. /**
  1996. * Creates a unique cache prefix to help prevent cache conflicts
  1997. *
  1998. * @return string The cache prefix to use
  1999. */
  2000. private function makeCachePrefix()
  2001. {
  2002. if (!$this->cache_prefix) {
  2003. $prefix = 'fDatabase::' . $this->type . '::';
  2004. if ($this->host) {
  2005. $prefix .= $this->host . '::';
  2006. }
  2007. if ($this->port) {
  2008. $prefix .= $this->port . '::';
  2009. }
  2010. $prefix .= $this->database . '::';
  2011. if ($this->username) {
  2012. $prefix .= $this->username . '::';
  2013. }
  2014. $this->cache_prefix = $prefix;
  2015. }
  2016. return $this->cache_prefix;
  2017. }
  2018. /**
  2019. * Executes a SQL statement
  2020. *
  2021. * @param string|fStatement $statement The statement to perform
  2022. * @param array $params The parameters for prepared statements
  2023. * @return void
  2024. */
  2025. private function perform($statement, $params)
  2026. {
  2027. fCore::startErrorCapture();
  2028. $extra = NULL;
  2029. if (is_object($statement)) {
  2030. $result = $statement->execute($params, $extra, $statement != $this->statement);
  2031. } elseif ($this->extension == 'ibm_db2') {
  2032. $result = db2_exec($this->connection, $statement, array('cursor' => DB2_FORWARD_ONLY));
  2033. } elseif ($this->extension == 'mssql') {
  2034. $result = mssql_query($statement, $this->connection);
  2035. } elseif ($this->extension == 'mysql') {
  2036. $result = mysql_unbuffered_query($statement, $this->connection);
  2037. } elseif ($this->extension == 'mysqli') {
  2038. $result = mysqli_query($this->connection, $statement, MYSQLI_USE_RESULT);
  2039. } elseif ($this->extension == 'oci8') {
  2040. $extra = oci_parse($this->connection, $statement);
  2041. $result = oci_execute($extra, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS);
  2042. } elseif ($this->extension == 'pgsql') {
  2043. $result = pg_query($this->connection, $statement);
  2044. } elseif ($this->extension == 'sqlite') {
  2045. $result = sqlite_exec($this->connection, $statement, $extra);
  2046. } elseif ($this->extension == 'sqlsrv') {
  2047. $result = sqlsrv_query($this->connection, $statement);
  2048. } elseif ($this->extension == 'pdo') {
  2049. if ($this->type == 'mssql' && !fCore::checkOS('windows')) {
  2050. // pdo_dblib is all messed up for return values from ->exec()
  2051. // and even ->query(), but ->query() is closer to correct and
  2052. // we use some heuristics to overcome the limitations
  2053. $result = $this->connection->query($statement);
  2054. if ($result instanceof PDOStatement) {
  2055. $result->closeCursor();
  2056. $extra = $result;
  2057. $result = TRUE;
  2058. if (preg_match('#^\s*EXEC(UTE)?\s+#i', $statement)) {
  2059. $error_info = $extra->errorInfo();
  2060. if (strpos($error_info[2], '(null) [0] (severity 0)') !== 0) {
  2061. $result = FALSE;
  2062. }
  2063. }
  2064. }
  2065. } else {
  2066. $result = $this->connection->exec($statement);
  2067. }
  2068. }
  2069. $this->statement = $statement;
  2070. $this->handleErrors(fCore::stopErrorCapture());
  2071. // The mssql extension will sometimes not return FALSE even if there are errors
  2072. if (strlen($this->error)) {
  2073. $result = FALSE;
  2074. }
  2075. if ($this->extension == 'mssql' && $result) {
  2076. $this->error = '';
  2077. }
  2078. if ($result === FALSE) {
  2079. $this->checkForError($result, $extra, is_object($statement) ? $statement->getSQL() : $statement);
  2080. } elseif (!is_bool($result) && $result !== NULL) {
  2081. if ($this->extension == 'ibm_db2') {
  2082. db2_free_result($result);
  2083. } elseif ($this->extension == 'mssql') {
  2084. mssql_free_result($result);
  2085. } elseif ($this->extension == 'mysql') {
  2086. mysql_free_result($result);
  2087. } elseif ($this->extension == 'mysqli') {
  2088. mysqli_free_result($result);
  2089. } elseif ($this->extension == 'oci8') {
  2090. oci_free_statement($oci_statement);
  2091. } elseif ($this->extension == 'pgsql') {
  2092. pg_free_result($result);
  2093. } elseif ($this->extension == 'sqlsrv') {
  2094. sqlsrv_free_stmt($result);
  2095. }
  2096. }
  2097. }
  2098. /**
  2099. * Executes an SQL query
  2100. *
  2101. * @param string|fStatement $statement The statement to perform
  2102. * @param fResult $result The result object for the query
  2103. * @param array $params The parameters for prepared statements
  2104. * @return void
  2105. */
  2106. private function performQuery($statement, $result, $params)
  2107. {
  2108. fCore::startErrorCapture();
  2109. $extra = NULL;
  2110. if (is_object($statement)) {
  2111. $statement->executeQuery($result, $params, $extra, $statement != $this->statement);
  2112. } elseif ($this->extension == 'ibm_db2') {
  2113. $extra = db2_exec($this->connection, $statement, array('cursor' => DB2_FORWARD_ONLY));
  2114. if (is_resource($extra)) {
  2115. $rows = array();
  2116. while ($row = db2_fetch_assoc($extra)) {
  2117. $rows[] = $row;
  2118. }
  2119. $result->setResult($rows);
  2120. unset($rows);
  2121. } else {
  2122. $result->setResult($extra);
  2123. }
  2124. } elseif ($this->extension == 'mssql') {
  2125. $result->setResult(mssql_query($result->getSQL(), $this->connection));
  2126. } elseif ($this->extension == 'mysql') {
  2127. $result->setResult(mysql_query($result->getSQL(), $this->connection));
  2128. } elseif ($this->extension == 'mysqli') {
  2129. $result->setResult(mysqli_query($this->connection, $result->getSQL()));
  2130. } elseif ($this->extension == 'oci8') {
  2131. $extra = oci_parse($this->connection, $result->getSQL());
  2132. if ($extra && oci_execute($extra, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS)) {
  2133. oci_fetch_all($extra, $rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW + OCI_ASSOC);
  2134. $result->setResult($rows);
  2135. unset($rows);
  2136. } else {
  2137. $result->setResult(FALSE);
  2138. }
  2139. } elseif ($this->extension == 'pgsql') {
  2140. $result->setResult(pg_query($this->connection, $result->getSQL()));
  2141. } elseif ($this->extension == 'sqlite') {
  2142. $result->setResult(sqlite_query($this->connection, $result->getSQL(), SQLITE_ASSOC, $extra));
  2143. } elseif ($this->extension == 'sqlsrv') {
  2144. $extra = sqlsrv_query($this->connection, $result->getSQL());
  2145. if (is_resource($extra)) {
  2146. $rows = array();
  2147. while ($row = sqlsrv_fetch_array($extra, SQLSRV_FETCH_ASSOC)) {
  2148. $rows[] = $row;
  2149. }
  2150. $result->setResult($rows);
  2151. unset($rows);
  2152. } else {
  2153. $result->setResult($extra);
  2154. }
  2155. } elseif ($this->extension == 'pdo') {
  2156. if (preg_match('#^\s*CREATE(\s+OR\s+REPLACE)?\s+TRIGGER#i', $result->getSQL())) {
  2157. $this->connection->exec($result->getSQL());
  2158. $extra = FALSE;
  2159. $returned_rows = array();
  2160. } else {
  2161. $extra = $this->connection->query($result->getSQL());
  2162. if (is_object($extra)) {
  2163. // This fixes a segfault issue with blobs and fetchAll() for pdo_ibm
  2164. if ($this->type == 'db2') {
  2165. $returned_rows = array();
  2166. while (($row = $extra->fetch(PDO::FETCH_ASSOC)) !== FALSE) {
  2167. foreach ($row as $key => $value) {
  2168. if (is_resource($value)) {
  2169. $row[$key] = stream_get_contents($value);
  2170. }
  2171. }
  2172. $returned_rows[] = $row;
  2173. }
  2174. // pdo_dblib doesn't throw an exception on error when executing
  2175. // a prepared statement when compiled against FreeTDS, so we have
  2176. // to manually check the error info to see if something went wrong
  2177. } elseif ($this->type == 'mssql' && !fCore::checkOS('windows') && preg_match('#^\s*EXEC(UTE)?\s+#i', $result->getSQL())) {
  2178. $error_info = $extra->errorInfo();
  2179. if ($error_info && strpos($error_info[2], '(null) [0] (severity 0)') !== 0) {
  2180. $returned_rows = FALSE;
  2181. }
  2182. } else {
  2183. $returned_rows = $extra->fetchAll(PDO::FETCH_ASSOC);
  2184. }
  2185. } else {
  2186. $returned_rows = $extra;
  2187. }
  2188. // The pdo_pgsql driver likes to return empty rows equal to the number of affected rows for insert and deletes
  2189. if ($this->type == 'postgresql' && $returned_rows && $returned_rows[0] == array()) {
  2190. $returned_rows = array();
  2191. }
  2192. }
  2193. $result->setResult($returned_rows);
  2194. }
  2195. $this->statement = $statement;
  2196. $this->handleErrors(fCore::stopErrorCapture());
  2197. // The mssql extension will sometimes not return FALSE even if there are errors
  2198. if (strlen($this->error) && strpos($this->error, 'WARNING!') !== 0) {
  2199. $result->setResult(FALSE);
  2200. }
  2201. $this->checkForError($result, $extra);
  2202. if ($this->extension == 'mssql') {
  2203. $this->error = '';
  2204. }
  2205. if ($this->extension == 'ibm_db2') {
  2206. $this->setAffectedRows($result, $extra);
  2207. if ($extra && !is_object($statement)) {
  2208. db2_free_result($extra);
  2209. }
  2210. } elseif ($this->extension == 'pdo') {
  2211. $this->setAffectedRows($result, $extra);
  2212. if ($extra && !is_object($statement)) {
  2213. $extra->closeCursor();
  2214. }
  2215. } elseif ($this->extension == 'oci8') {
  2216. $this->setAffectedRows($result, $extra);
  2217. if ($extra && !is_object($statement)) {
  2218. oci_free_statement($extra);
  2219. }
  2220. } elseif ($this->extension == 'sqlsrv') {
  2221. $this->setAffectedRows($result, $extra);
  2222. if ($extra && !is_object($statement)) {
  2223. sqlsrv_free_stmt($extra);
  2224. }
  2225. } else {
  2226. $this->setAffectedRows($result, $extra);
  2227. }
  2228. $this->setReturnedRows($result);
  2229. $this->handleAutoIncrementedValue($result, $extra);
  2230. }
  2231. /**
  2232. * Executes an unbuffered SQL query
  2233. *
  2234. * @param string|fStatement $statement The statement to perform
  2235. * @param fUnbufferedResult $result The result object for the query
  2236. * @param array $params The parameters for prepared statements
  2237. * @return void
  2238. */
  2239. private function performUnbufferedQuery($statement, $result, $params)
  2240. {
  2241. fCore::startErrorCapture();
  2242. $extra = NULL;
  2243. if (is_object($statement)) {
  2244. $statement->executeUnbufferedQuery($result, $params, $extra, $statement != $this->statement);
  2245. } elseif ($this->extension == 'ibm_db2') {
  2246. $result->setResult(db2_exec($this->connection, $statement, array('cursor' => DB2_FORWARD_ONLY)));
  2247. } elseif ($this->extension == 'mssql') {
  2248. $result->setResult(mssql_query($result->getSQL(), $this->connection, 20));
  2249. } elseif ($this->extension == 'mysql') {
  2250. $result->setResult(mysql_unbuffered_query($result->getSQL(), $this->connection));
  2251. } elseif ($this->extension == 'mysqli') {
  2252. $result->setResult(mysqli_query($this->connection, $result->getSQL(), MYSQLI_USE_RESULT));
  2253. } elseif ($this->extension == 'oci8') {
  2254. $extra = oci_parse($this->connection, $result->getSQL());
  2255. if (oci_execute($extra, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS)) {
  2256. $result->setResult($extra);
  2257. } else {
  2258. $result->setResult(FALSE);
  2259. }
  2260. } elseif ($this->extension == 'pgsql') {
  2261. $result->setResult(pg_query($this->connection, $result->getSQL()));
  2262. } elseif ($this->extension == 'sqlite') {
  2263. $result->setResult(sqlite_unbuffered_query($this->connection, $result->getSQL(), SQLITE_ASSOC, $extra));
  2264. } elseif ($this->extension == 'sqlsrv') {
  2265. $result->setResult(sqlsrv_query($this->connection, $result->getSQL()));
  2266. } elseif ($this->extension == 'pdo') {
  2267. $result->setResult($this->connection->query($result->getSQL()));
  2268. }
  2269. $this->statement = $statement;
  2270. $this->handleErrors(fCore::stopErrorCapture());
  2271. $this->checkForError($result, $extra);
  2272. }
  2273. /**
  2274. * Prepares a single fStatement object to execute prepared statements
  2275. *
  2276. * Identifier placeholders (%r) are not supported with prepared statements.
  2277. * In addition, multiple values can not be escaped by a placeholder - only
  2278. * a single value can be provided.
  2279. *
  2280. * @param string $sql The SQL to prepare
  2281. * @return fStatement A prepared statement object that can be passed to ::query(), ::unbufferedQuery() or ::execute()
  2282. */
  2283. public function prepare($sql)
  2284. {
  2285. return $this->prepareStatement($sql);
  2286. }
  2287. /**
  2288. * Prepares a single fStatement object to execute prepared statements
  2289. *
  2290. * Identifier placeholders (%r) are not supported with prepared statements.
  2291. * In addition, multiple values can not be escaped by a placeholder - only
  2292. * a single value can be provided.
  2293. *
  2294. * @param string $sql The SQL to prepare
  2295. * @param boolean $translate If the SQL should be translated using fSQLTranslation
  2296. * @return fStatement A prepare statement object that can be passed to ::query(), ::unbufferedQuery() or ::execute()
  2297. */
  2298. private function prepareStatement($sql, $translate=FALSE)
  2299. {
  2300. // Ensure an SQL statement was passed
  2301. if (empty($sql)) {
  2302. throw new fProgrammerException('No SQL statement passed');
  2303. }
  2304. // This is just to keep the callback method signature consistent
  2305. $values = array();
  2306. if ($this->hook_callbacks['unmodified']) {
  2307. foreach ($this->hook_callbacks['unmodified'] as $callback) {
  2308. $params = array(
  2309. $this,
  2310. &$sql,
  2311. &$values
  2312. );
  2313. call_user_func_array($callback, $params);
  2314. }
  2315. }
  2316. // Separate the SQL from quoted values
  2317. $parts = $this->splitSQL($sql);
  2318. $new_parts = array();
  2319. foreach ($parts as $part) {
  2320. if ($part[0] == "'") {
  2321. $new_parts[] = $part;
  2322. } else {
  2323. // We have to escape the placeholders so that the extraction of
  2324. // string to %s placeholder doesn't mess up the creation of the
  2325. // prepare statement
  2326. $new_parts[] = str_replace('%', '%%', $part);
  2327. }
  2328. }
  2329. $query = $this->extractStrings($new_parts, $values);
  2330. if ($this->hook_callbacks['extracted']) {
  2331. foreach ($this->hook_callbacks['extracted'] as $callback) {
  2332. $params = array(
  2333. $this,
  2334. &$query,
  2335. &$values
  2336. );
  2337. call_user_func_array($callback, $params);
  2338. }
  2339. }
  2340. $untranslated_sql = NULL;
  2341. if ($translate) {
  2342. $untranslated_sql = $sql;
  2343. $query = $this->getSQLTranslation()->translate(array($query));
  2344. if (count($query) > 1) {
  2345. throw new fProgrammerException(
  2346. "The SQL statement %1$s can not be used as a prepared statement because translation turns it into multiple SQL statements",
  2347. $untranslated_sql
  2348. );
  2349. }
  2350. $query = current($query);
  2351. }
  2352. // Pull all of the real placeholders (%%) out and replace them with
  2353. // %%s for sprintf() in fStatement. We have to use %% because we are
  2354. // going to put the extracted string back into the statement via %s.
  2355. $pieces = preg_split('#(%%[lbdfistp])\b#', $query, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  2356. $placeholders = array();
  2357. $new_query = '';
  2358. foreach ($pieces as $piece) {
  2359. if (strlen($piece) == 3 && substr($piece, 0, 2) == '%%') {
  2360. $placeholders[] = substr($piece, 1);
  2361. $new_query .= '%%s';
  2362. } else {
  2363. $new_query .= $piece;
  2364. }
  2365. }
  2366. $query = $new_query;
  2367. // Unescape literal semicolons in the queries
  2368. $query = preg_replace('#(?<!\\\\)\\\\;#', ';', $query);
  2369. $query = $this->escapeSQL($query, $values, TRUE);
  2370. return new fStatement($this, $query, $placeholders, $untranslated_sql);
  2371. }
  2372. /**
  2373. * Preprocesses SQL by escaping values, spliting queries, cleaning escaped semicolons, fixing backslashed single quotes and translating
  2374. *
  2375. * @internal
  2376. *
  2377. * @param string $sql The SQL to process
  2378. * @param array $values Literal values to escape into the SQL
  2379. * @param boolean $translate If the SQL should be translated
  2380. * @param array &$rollback_queries MySQL doesn't allow transactions around `ALTER TABLE` statements, and some of those require multiple statements, so this is an array of "undo" SQL statements
  2381. * @return array The split out SQL queries, queries that have been translated will have a string key of a number, `:` and the original SQL, non-translated SQL will have a numeric key
  2382. */
  2383. public function preprocess($sql, $values, $translate, &$rollback_queries=NULL)
  2384. {
  2385. $this->connect();
  2386. // Ensure an SQL statement was passed
  2387. if (empty($sql)) {
  2388. throw new fProgrammerException('No SQL statement passed');
  2389. }
  2390. if ($this->hook_callbacks['unmodified']) {
  2391. foreach ($this->hook_callbacks['unmodified'] as $callback) {
  2392. $params = array(
  2393. $this,
  2394. &$sql,
  2395. &$values
  2396. );
  2397. call_user_func_array($callback, $params);
  2398. }
  2399. }
  2400. // Separate the SQL from quoted values
  2401. $parts = $this->splitSQL($sql, $placeholders);
  2402. // If the values were passed as a single array, this handles that
  2403. if (count($values) == 1 && is_array($values[0]) && count($values[0]) == $placeholders) {
  2404. $values = array_shift($values);
  2405. }
  2406. $sql = $this->extractStrings($parts, $values);
  2407. $queries = preg_split('#(?<!\\\\);#', $sql);
  2408. $queries = array_map('trim', $queries);
  2409. $output = array();
  2410. $value_number = 0;
  2411. foreach ($queries as $query) {
  2412. if (!strlen($query)) {
  2413. continue;
  2414. }
  2415. $sqlite_ddl = $this->type == 'sqlite' && preg_match('#^\s*(ALTER\s+TABLE|CREATE\s+TABLE|COMMENT\s+ON)\s+#i', $query);
  2416. $pieces = preg_split('#(?<!%)(%[lbdfristp])\b#', $query, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  2417. $new_sql = '';
  2418. $query_values = array();
  2419. $num = 0;
  2420. foreach ($pieces as $piece) {
  2421. // A placeholder
  2422. if (strlen($piece) == 2 && $piece[0] == '%') {
  2423. $value = $values[$value_number];
  2424. // Here we put numbers for LIMIT and OFFSET into the SQL so they can be translated properly
  2425. if ($piece == '%i' && preg_match('#\b(LIMIT|OFFSET)\s+#Di', $new_sql)) {
  2426. $new_sql .= (int) $value;
  2427. $value_number++;
  2428. // Here we put blank strings back into the SQL so they can be translated for Oracle
  2429. } elseif ($piece == '%s' && $value !== NULL && ((string) $value) == '') {
  2430. $new_sql .= "''";
  2431. $value_number++;
  2432. // SQLite needs the literal string values for DDL statements
  2433. } elseif ($piece == '%s' && $sqlite_ddl) {
  2434. $new_sql .= $this->escapeString($value);
  2435. $value_number++;
  2436. } elseif ($piece == '%r') {
  2437. if (is_array($value)) {
  2438. $new_sql .= join(', ', array_map($this->escapeIdentifier, $value));
  2439. } else {
  2440. $new_sql .= $this->escapeIdentifier($value);
  2441. }
  2442. $value_number++;
  2443. // Other placeholder/value combos just get added
  2444. } else {
  2445. $value_number++;
  2446. $new_sql .= '%' . $num . '$' . $piece[1];
  2447. $num++;
  2448. $query_values[] = $value;
  2449. }
  2450. // A piece of SQL
  2451. } else {
  2452. $new_sql .= $piece;
  2453. }
  2454. }
  2455. $query = $new_sql;
  2456. if ($this->hook_callbacks['extracted']) {
  2457. foreach ($this->hook_callbacks['extracted'] as $callback) {
  2458. $params = array(
  2459. $this,
  2460. &$query,
  2461. &$query_values
  2462. );
  2463. call_user_func_array($callback, $params);
  2464. }
  2465. }
  2466. if ($translate) {
  2467. $query_set = $this->getSQLTranslation()->translate(array($query), $rollback_queries);
  2468. } else {
  2469. $query_set = array($query);
  2470. }
  2471. foreach ($query_set as $key => $query) {
  2472. // Unescape literal semicolons in the queries
  2473. $query = preg_replace('#(?<!\\\\)\\\\;#', ';', $query);
  2474. // Escape the values into the SQL
  2475. if ($query_values && preg_match_all('#(?<!%)%(\d+)\$([lbdfristp])\b#', $query, $matches, PREG_SET_ORDER)) {
  2476. // If we translated, we may need to shuffle values around
  2477. if ($translate) {
  2478. $new_values = array();
  2479. foreach ($matches as $match) {
  2480. $new_values[] = $query_values[$match[1]];
  2481. }
  2482. $query_values = $new_values;
  2483. }
  2484. $query = preg_replace('#(?<!%)%\d+\$([lbdfristp])\b#', '%\1', $query);
  2485. $query = $this->escapeSQL($query, $query_values, TRUE);
  2486. }
  2487. if (!is_numeric($key)) {
  2488. $key_parts = explode(':', $key);
  2489. $key = count($output) . ':' . $key_parts[1];
  2490. } else {
  2491. $key = count($output);
  2492. }
  2493. $output[$key] = $query;
  2494. }
  2495. }
  2496. return $output;
  2497. }
  2498. /**
  2499. * Executes one or more SQL queries and returns the result(s)
  2500. *
  2501. * @param string|fStatement $statement One or more SQL statements in a string or a single fStatement prepared statement
  2502. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  2503. * @param mixed ...
  2504. * @return fResult|array The fResult object(s) for the query
  2505. */
  2506. public function query($statement)
  2507. {
  2508. $args = func_get_args();
  2509. $params = array_slice($args, 1);
  2510. if (is_object($statement)) {
  2511. return $this->run($statement, 'fResult', $params);
  2512. }
  2513. $queries = $this->preprocess($statement, $params, FALSE);
  2514. $output = array();
  2515. foreach ($queries as $query) {
  2516. $output[] = $this->run($query, 'fResult');
  2517. }
  2518. return sizeof($output) == 1 ? $output[0] : $output;
  2519. }
  2520. /**
  2521. * Registers a callback for one of the various query hooks - multiple callbacks can be registered for each hook
  2522. *
  2523. * The following hooks are available:
  2524. * - `'unmodified'`: The original SQL passed to fDatabase, for prepared statements this is called just once before the fStatement object is created
  2525. * - `'extracted'`: The SQL after all non-empty strings have been extracted and replaced with ordered sprintf-style placeholders
  2526. * - `'run'`: After the SQL has been run
  2527. *
  2528. * Methods for the `'unmodified'` hook should have the following signature:
  2529. *
  2530. * - **`$database`**: The fDatabase instance
  2531. * - **`&$sql`**: The original, unedited SQL
  2532. * - **`&$values`**: The values to be escaped into the placeholders in the SQL
  2533. *
  2534. * Methods for the `'extracted'` hook should have the following signature:
  2535. *
  2536. * - **`$database`**: The fDatabase instance
  2537. * - **`&$sql`**: The SQL with all strings removed and replaced with `%1$s`-style placeholders
  2538. * - **`&$values`**: The values to be escaped into the placeholders in the SQL
  2539. *
  2540. * The `extracted` hook is the best place to modify the SQL since there is
  2541. * no risk of breaking string literals. Please note that there may be empty
  2542. * strings (`''`) present in the SQL since some databases treat those as
  2543. * `NULL`.
  2544. *
  2545. * Methods for the `'run'` hook should have the following signature:
  2546. *
  2547. * - **`$database`**: The fDatabase instance
  2548. * - **`$query`**: The (string) SQL or `array(0 => {fStatement object}, 1 => {values array})`
  2549. * - **`$query_time`**: The (float) number of seconds the query took
  2550. * - **`$result`** The fResult or fUnbufferedResult object, or `FALSE` if no result
  2551. *
  2552. * @param string $hook The hook to register for
  2553. * @param callback $callback The callback to register - see the method description for details about the method signature
  2554. * @return void
  2555. */
  2556. public function registerHookCallback($hook, $callback)
  2557. {
  2558. $valid_hooks = array(
  2559. 'unmodified',
  2560. 'extracted',
  2561. 'run'
  2562. );
  2563. if (!in_array($hook, $valid_hooks)) {
  2564. throw new fProgrammerException(
  2565. 'The hook specified, %1$s, should be one of: %2$s.',
  2566. $hook,
  2567. join(', ', $valid_hooks)
  2568. );
  2569. }
  2570. $this->hook_callbacks[$hook][] = $callback;
  2571. }
  2572. /**
  2573. * Runs a single statement and times it, removes any old unbuffered queries before starting
  2574. *
  2575. * @param string|fStatement $statement The SQL statement or prepared statement to execute
  2576. * @param string $result_type The type of result object to return, fResult or fUnbufferedResult
  2577. * @return fResult|fUnbufferedResult The result for the query
  2578. */
  2579. private function run($statement, $result_type=NULL, $params=array())
  2580. {
  2581. if ($this->unbuffered_result) {
  2582. $this->unbuffered_result->__destruct();
  2583. $this->unbuffered_result = NULL;
  2584. }
  2585. $start_time = microtime(TRUE);
  2586. $result = $this->handleTransactionQueries($statement, $result_type);
  2587. if (is_object($statement)) {
  2588. $sql = $statement->getSQL();
  2589. } else {
  2590. $sql = $statement;
  2591. }
  2592. if (!$result) {
  2593. if ($result_type) {
  2594. $result = new $result_type($this, $this->type == 'mssql' ? $this->schema_info['character_set'] : NULL);
  2595. $result->setSQL($sql);
  2596. if ($result_type == 'fResult') {
  2597. $this->performQuery($statement, $result, $params);
  2598. } else {
  2599. $this->performUnbufferedQuery($statement, $result, $params);
  2600. }
  2601. if ($statement instanceof fStatement && $statement->getUntranslatedSQL()) {
  2602. $result->setUntranslatedSQL($statement->getUntranslatedSQL());
  2603. }
  2604. } else {
  2605. $this->perform($statement, $params);
  2606. }
  2607. }
  2608. // Write some debugging info
  2609. $query_time = microtime(TRUE) - $start_time;
  2610. $this->query_time += $query_time;
  2611. if (fCore::getDebug($this->debug)) {
  2612. fCore::debug(
  2613. self::compose(
  2614. 'Query time was %1$s seconds for:%2$s',
  2615. $query_time,
  2616. "\n" . $sql
  2617. ),
  2618. $this->debug
  2619. );
  2620. }
  2621. if ($this->hook_callbacks['run']) {
  2622. foreach ($this->hook_callbacks['run'] as $callback) {
  2623. $callback_params = array(
  2624. $this,
  2625. is_object($statement) ? array($statement, $params) : $sql,
  2626. $query_time,
  2627. $result
  2628. );
  2629. call_user_func_array($callback, $callback_params);
  2630. }
  2631. }
  2632. if ($result_type) {
  2633. return $result;
  2634. }
  2635. }
  2636. /**
  2637. * Takes an array of rollback statements to undo part of a set of queries which involve one that failed
  2638. *
  2639. * This is only used for MySQL since it is the only database that does not
  2640. * support transactions about `ALTER TABLE` statements, but that also
  2641. * requires more than one query to accomplish many `ALTER TABLE` tasks.
  2642. *
  2643. * @param array $rollback_statements The SQL statements used to rollback `ALTER TABLE` statements
  2644. * @param integer $start_number The number query that failed - this is used to determine which rollback statements to run
  2645. * @return void
  2646. */
  2647. private function runRollbackStatements($rollback_statements, $start_number)
  2648. {
  2649. if ($rollback_statements) {
  2650. $rollback_statements = array_slice($rollback_statements, 0, $start_number);
  2651. $rollback_statements = array_reverse($rollback_statements);
  2652. foreach ($rollback_statements as $rollback_statement) {
  2653. $this->run($rollback_statement);
  2654. }
  2655. }
  2656. }
  2657. /**
  2658. * Turns an array possibly containing objects into an array of all strings
  2659. *
  2660. * @param array $values The array of values to scalarize
  2661. * @return array The scalarized values
  2662. */
  2663. private function scalarize($values)
  2664. {
  2665. $new_values = array();
  2666. foreach ($values as $value) {
  2667. if (is_object($value) && is_callable(array($value, '__toString'))) {
  2668. $value = $value->__toString();
  2669. } elseif (is_object($value)) {
  2670. $value = (string) $value;
  2671. } elseif (is_array($value)) {
  2672. $value = $this->scalarize($value);
  2673. }
  2674. $new_values[] = $value;
  2675. }
  2676. return $new_values;
  2677. }
  2678. /**
  2679. * Sets the number of rows affected by the query
  2680. *
  2681. * @param fResult $result The result object for the query
  2682. * @param mixed $resource Only applicable for `ibm_db2`, `pdo`, `oci8` and `sqlsrv` extentions or `mysqli` prepared statements - this is either the `PDOStatement` object, `mysqli_stmt` object or the `oci8` or `sqlsrv` resource
  2683. * @return void
  2684. */
  2685. private function setAffectedRows($result, $resource=NULL)
  2686. {
  2687. if ($this->extension == 'ibm_db2') {
  2688. $insert_update_delete = preg_match('#^\s*(INSERT|UPDATE|DELETE)\b#i', $result->getSQL());
  2689. $result->setAffectedRows(!$insert_update_delete ? 0 : db2_num_rows($resource));
  2690. } elseif ($this->extension == 'mssql') {
  2691. $affected_rows_result = mssql_query('SELECT @@ROWCOUNT AS rows', $this->connection);
  2692. $result->setAffectedRows((int) mssql_result($affected_rows_result, 0, 'rows'));
  2693. } elseif ($this->extension == 'mysql') {
  2694. $result->setAffectedRows(mysql_affected_rows($this->connection));
  2695. } elseif ($this->extension == 'mysqli') {
  2696. if (is_object($resource)) {
  2697. $result->setAffectedRows($resource->affected_rows);
  2698. } else {
  2699. $result->setAffectedRows(mysqli_affected_rows($this->connection));
  2700. }
  2701. } elseif ($this->extension == 'oci8') {
  2702. $result->setAffectedRows(oci_num_rows($resource));
  2703. } elseif ($this->extension == 'pgsql') {
  2704. $result->setAffectedRows(pg_affected_rows($result->getResult()));
  2705. } elseif ($this->extension == 'sqlite') {
  2706. $result->setAffectedRows(sqlite_changes($this->connection));
  2707. } elseif ($this->extension == 'sqlsrv') {
  2708. $result->setAffectedRows(sqlsrv_rows_affected($resource));
  2709. } elseif ($this->extension == 'pdo') {
  2710. // This fixes the fact that rowCount is not reset for non INSERT/UPDATE/DELETE statements
  2711. try {
  2712. if (!$resource || !$resource->fetch()) {
  2713. throw new PDOException();
  2714. }
  2715. $result->setAffectedRows(0);
  2716. } catch (PDOException $e) {
  2717. // The SQLite PDO driver seems to return 1 when no rows are returned from a SELECT statement
  2718. if ($this->type == 'sqlite' && $this->extension == 'pdo' && preg_match('#^\s*SELECT#i', $result->getSQL())) {
  2719. $result->setAffectedRows(0);
  2720. } elseif (!$resource) {
  2721. $result->setAffectedRows(0);
  2722. } else {
  2723. $result->setAffectedRows($resource->rowCount());
  2724. }
  2725. }
  2726. }
  2727. }
  2728. /**
  2729. * Sets the number of rows returned by the query
  2730. *
  2731. * @param fResult $result The result object for the query
  2732. * @return void
  2733. */
  2734. private function setReturnedRows($result)
  2735. {
  2736. if (is_resource($result->getResult()) || is_object($result->getResult())) {
  2737. if ($this->extension == 'mssql') {
  2738. $result->setReturnedRows(mssql_num_rows($result->getResult()));
  2739. } elseif ($this->extension == 'mysql') {
  2740. $result->setReturnedRows(mysql_num_rows($result->getResult()));
  2741. } elseif ($this->extension == 'mysqli') {
  2742. $result->setReturnedRows(mysqli_num_rows($result->getResult()));
  2743. } elseif ($this->extension == 'pgsql') {
  2744. $result->setReturnedRows(pg_num_rows($result->getResult()));
  2745. } elseif ($this->extension == 'sqlite') {
  2746. $result->setReturnedRows(sqlite_num_rows($result->getResult()));
  2747. }
  2748. } elseif (is_array($result->getResult())) {
  2749. $result->setReturnedRows(sizeof($result->getResult()));
  2750. }
  2751. }
  2752. /**
  2753. * Splits SQL into pieces of SQL and quoted strings
  2754. *
  2755. * @param string $sql The SQL to split
  2756. * @param integer &$placeholders The number of placeholders in the SQL
  2757. * @return array The pieces
  2758. */
  2759. private function splitSQL($sql, &$placeholders=NULL)
  2760. {
  2761. // Fix \' in MySQL and PostgreSQL
  2762. if(($this->type == 'mysql' || $this->type == 'postgresql') && strpos($sql, '\\') !== FALSE) {
  2763. $sql = preg_replace("#(?<!\\\\)((\\\\{2})*)\\\\'#", "\\1''", $sql);
  2764. }
  2765. $parts = array();
  2766. $temp_sql = $sql;
  2767. $start_pos = 0;
  2768. $inside_string = FALSE;
  2769. do {
  2770. $pos = strpos($temp_sql, "'", $start_pos);
  2771. if ($pos !== FALSE) {
  2772. if (!$inside_string) {
  2773. $part = substr($temp_sql, 0, $pos);
  2774. $placeholders += preg_match_all('#(?<!%)%[lbdfristp]\b#', $part, $trash);
  2775. unset($trash);
  2776. $parts[] = $part;
  2777. $temp_sql = substr($temp_sql, $pos);
  2778. $start_pos = 1;
  2779. $inside_string = TRUE;
  2780. } elseif ($pos == strlen($temp_sql)) {
  2781. $parts[] = $temp_sql;
  2782. $temp_sql = '';
  2783. $pos = FALSE;
  2784. // Skip single-quote-escaped single quotes
  2785. } elseif (strlen($temp_sql) > $pos+1 && $temp_sql[$pos+1] == "'") {
  2786. $start_pos = $pos+2;
  2787. } else {
  2788. $parts[] = substr($temp_sql, 0, $pos+1);
  2789. $temp_sql = substr($temp_sql, $pos+1);
  2790. $start_pos = 0;
  2791. $inside_string = FALSE;
  2792. }
  2793. }
  2794. } while ($pos !== FALSE);
  2795. if ($temp_sql) {
  2796. $placeholders += preg_match_all('#(?<!%)%[lbdfristp]\b#', $temp_sql, $trash);
  2797. unset($trash);
  2798. $parts[] = $temp_sql;
  2799. }
  2800. return $parts;
  2801. }
  2802. /**
  2803. * Translates one or more SQL statements using fSQLTranslation and executes them without returning any results
  2804. *
  2805. * @param string $sql One or more SQL statements
  2806. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  2807. * @param mixed ...
  2808. * @return void
  2809. */
  2810. public function translatedExecute($sql)
  2811. {
  2812. $args = func_get_args();
  2813. $queries = $this->preprocess(
  2814. $sql,
  2815. array_slice($args, 1),
  2816. TRUE,
  2817. $rollback_statements
  2818. );
  2819. try {
  2820. $output = array();
  2821. $i = 0;
  2822. foreach ($queries as $i => $query) {
  2823. $this->run($query);
  2824. $i++;
  2825. }
  2826. } catch (fSQLException $e) {
  2827. $this->runRollbackStatements($rollback_statements, $i);
  2828. throw $e;
  2829. }
  2830. }
  2831. /**
  2832. * Translates a SQL statement and creates an fStatement object from it
  2833. *
  2834. * Identifier placeholders (%r) are not supported with prepared statements.
  2835. * In addition, multiple values can not be escaped by a placeholder - only
  2836. * a single value can be provided.
  2837. *
  2838. * @param string $sql The SQL to prepare
  2839. * @return fStatement A prepared statement object that can be passed to ::query(), ::unbufferedQuery() or ::execute()
  2840. */
  2841. public function translatedPrepare($sql)
  2842. {
  2843. return $this->prepareStatement($sql, TRUE);
  2844. }
  2845. /**
  2846. * Translates one or more SQL statements using fSQLTranslation and executes them
  2847. *
  2848. * @param string $sql One or more SQL statements
  2849. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  2850. * @param mixed ...
  2851. * @return fResult|array The fResult object(s) for the query
  2852. */
  2853. public function translatedQuery($sql)
  2854. {
  2855. $args = func_get_args();
  2856. $queries = $this->preprocess(
  2857. $sql,
  2858. array_slice($args, 1),
  2859. TRUE,
  2860. $rollback_statements
  2861. );
  2862. try {
  2863. $output = array();
  2864. $i = 0;
  2865. foreach ($queries as $key => $query) {
  2866. $result = $this->run($query, 'fResult');
  2867. if (!is_numeric($key)) {
  2868. list($number, $original_query) = explode(':', $key, 2);
  2869. $result->setUntranslatedSQL($original_query);
  2870. }
  2871. $output[] = $result;
  2872. $i++;
  2873. }
  2874. } catch (fSQLException $e) {
  2875. $this->runRollbackStatements($rollback_statements, $i);
  2876. throw $e;
  2877. }
  2878. return sizeof($output) == 1 ? $output[0] : $output;
  2879. }
  2880. /**
  2881. * Executes a single SQL statement in unbuffered mode. This is optimal for
  2882. * large results sets since it does not load the whole result set into
  2883. * memory first. The gotcha is that only one unbuffered result can exist at
  2884. * one time. If another unbuffered query is executed, the old result will
  2885. * be deleted.
  2886. *
  2887. * @param string|fStatement $statement A single SQL statement
  2888. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  2889. * @param mixed ...
  2890. * @return fUnbufferedResult The result object for the unbuffered query
  2891. */
  2892. public function unbufferedQuery($statement)
  2893. {
  2894. $args = func_get_args();
  2895. $params = array_slice($args, 1);
  2896. if (is_object($statement)) {
  2897. $result = $this->run($statement, 'fUnbufferedResult', $params);
  2898. } else {
  2899. $queries = $this->preprocess($statement, $params, FALSE);
  2900. if (sizeof($queries) > 1) {
  2901. throw new fProgrammerException(
  2902. 'Only a single unbuffered query can be run at a time, however %d were passed',
  2903. sizeof($queries)
  2904. );
  2905. }
  2906. $result = $this->run($queries[0], 'fUnbufferedResult');
  2907. }
  2908. $this->unbuffered_result = $result;
  2909. return $result;
  2910. }
  2911. /**
  2912. * Translates the SQL statement using fSQLTranslation and then executes it
  2913. * in unbuffered mode. This is optimal for large results sets since it does
  2914. * not load the whole result set into memory first. The gotcha is that only
  2915. * one unbuffered result can exist at one time. If another unbuffered query
  2916. * is executed, the old result will be deleted.
  2917. *
  2918. * @param string $sql A single SQL statement
  2919. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  2920. * @param mixed ...
  2921. * @return fUnbufferedResult The result object for the unbuffered query
  2922. */
  2923. public function unbufferedTranslatedQuery($sql)
  2924. {
  2925. if (preg_match('#^\s*(ALTER|COMMENT|CREATE|DROP)\s+#i', $sql)) {
  2926. throw new fProgrammerException(
  2927. "The SQL provided, %1$s, appears to be a DDL (data definition language) SQL statement, which can not be run via %2$s because it may result in multiple SQL statements being run. Please use %3$s instead.",
  2928. $sql,
  2929. __CLASS__ . '::unbufferedTranslatedQuery()',
  2930. __CLASS__ . '::translatedExecute()'
  2931. );
  2932. }
  2933. $args = func_get_args();
  2934. $queries = $this->preprocess(
  2935. $sql,
  2936. array_slice($args, 1),
  2937. TRUE
  2938. );
  2939. if (sizeof($queries) > 1) {
  2940. throw new fProgrammerException(
  2941. 'Only a single unbuffered query can be run at a time, however %d were passed',
  2942. sizeof($queries)
  2943. );
  2944. }
  2945. $query_keys = array_keys($queries);
  2946. $key = $query_keys[0];
  2947. list($number, $original_query) = explode(':', $key, 2);
  2948. $result = $this->run($queries[$key], 'fUnbufferedResult');
  2949. $result->setUntranslatedSQL($original_query);
  2950. $this->unbuffered_result = $result;
  2951. return $result;
  2952. }
  2953. /**
  2954. * Unescapes a value coming out of a database based on its data type
  2955. *
  2956. * The valid data types are:
  2957. *
  2958. * - `'blob'` (or `'%l'`)
  2959. * - `'boolean'` (or `'%b'`)
  2960. * - `'date'` (or `'%d'`)
  2961. * - `'float'` (or `'%f'`)
  2962. * - `'integer'` (or `'%i'`)
  2963. * - `'string'` (also `'%s'`, `'varchar'`, `'char'` or `'text'`)
  2964. * - `'time'` (or `'%t'`)
  2965. * - `'timestamp'` (or `'%p'`)
  2966. *
  2967. * @param string $data_type The data type being unescaped - see method description for valid values
  2968. * @param mixed $value The value or array of values to unescape
  2969. * @return mixed The unescaped value
  2970. */
  2971. public function unescape($data_type, $value)
  2972. {
  2973. if ($value === NULL) {
  2974. return $value;
  2975. }
  2976. $callback = NULL;
  2977. switch ($data_type) {
  2978. // Testing showed that strings tend to be most common,
  2979. // and moving this to the top of the switch statement
  2980. // improved performance on read-heavy pages
  2981. case 'string':
  2982. case 'varchar':
  2983. case 'char':
  2984. case 'text':
  2985. case '%s':
  2986. return $value;
  2987. case 'boolean':
  2988. case '%b':
  2989. $callback = $this->unescapeBoolean;
  2990. break;
  2991. case 'date':
  2992. case '%d':
  2993. $callback = $this->unescapeDate;
  2994. break;
  2995. case 'float':
  2996. case '%f':
  2997. return $value;
  2998. case 'integer':
  2999. case '%i':
  3000. return $value;
  3001. case 'time':
  3002. case '%t':
  3003. $callback = $this->unescapeTime;
  3004. break;
  3005. case 'timestamp':
  3006. case '%p':
  3007. $callback = $this->unescapeTimestamp;
  3008. break;
  3009. case 'blob':
  3010. case '%l':
  3011. $callback = $this->unescapeBlob;
  3012. break;
  3013. }
  3014. if ($callback) {
  3015. if (is_array($value)) {
  3016. return array_map($callback, $value);
  3017. }
  3018. return call_user_func($callback, $value);
  3019. }
  3020. throw new fProgrammerException(
  3021. 'Unknown data type, %1$s, specified. Must be one of: %2$s.',
  3022. $data_type,
  3023. 'blob, %l, boolean, %b, date, %d, float, %f, integer, %i, string, %s, time, %t, timestamp, %p'
  3024. );
  3025. }
  3026. /**
  3027. * Unescapes a blob coming out of the database
  3028. *
  3029. * @param string $value The value to unescape
  3030. * @return binary The binary data
  3031. */
  3032. private function unescapeBlob($value)
  3033. {
  3034. $this->connect();
  3035. if ($this->extension == 'pgsql') {
  3036. return pg_unescape_bytea($value);
  3037. } elseif ($this->extension == 'pdo' && is_resource($value)) {
  3038. return stream_get_contents($value);
  3039. } elseif ($this->extension == 'sqlite') {
  3040. return pack('H*', $value);
  3041. } else {
  3042. return $value;
  3043. }
  3044. }
  3045. /**
  3046. * Unescapes a boolean coming out of the database
  3047. *
  3048. * @param string $value The value to unescape
  3049. * @return boolean The boolean
  3050. */
  3051. private function unescapeBoolean($value)
  3052. {
  3053. return ($value === 'f' || !$value) ? FALSE : TRUE;
  3054. }
  3055. /**
  3056. * Unescapes a date coming out of the database
  3057. *
  3058. * @param string $value The value to unescape
  3059. * @return string The date in YYYY-MM-DD format
  3060. */
  3061. private function unescapeDate($value)
  3062. {
  3063. if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
  3064. return $value->format('Y-m-d');
  3065. } elseif ($this->type == 'mssql') {
  3066. $value = preg_replace('#:\d{3}#', '', $value);
  3067. }
  3068. return date('Y-m-d', strtotime($value));
  3069. }
  3070. /**
  3071. * Unescapes a time coming out of the database
  3072. *
  3073. * @param string $value The value to unescape
  3074. * @return string The time in `HH:MM:SS` format
  3075. */
  3076. private function unescapeTime($value)
  3077. {
  3078. if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
  3079. return $value->format('H:i:s');
  3080. } elseif ($this->type == 'mssql') {
  3081. $value = preg_replace('#:\d{3}#', '', $value);
  3082. }
  3083. return date('H:i:s', strtotime($value));
  3084. }
  3085. /**
  3086. * Unescapes a timestamp coming out of the database
  3087. *
  3088. * @param string $value The value to unescape
  3089. * @return string The timestamp in `YYYY-MM-DD HH:MM:SS` format
  3090. */
  3091. private function unescapeTimestamp($value)
  3092. {
  3093. if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
  3094. return $value->format('Y-m-d H:i:s');
  3095. } elseif ($this->type == 'mssql') {
  3096. $value = preg_replace('#:\d{3}#', '', $value);
  3097. }
  3098. return date('Y-m-d H:i:s', strtotime($value));
  3099. }
  3100. }
  3101. /**
  3102. * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>
  3103. *
  3104. * Permission is hereby granted, free of charge, to any person obtaining a copy
  3105. * of this software and associated documentation files (the "Software"), to deal
  3106. * in the Software without restriction, including without limitation the rights
  3107. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  3108. * copies of the Software, and to permit persons to whom the Software is
  3109. * furnished to do so, subject to the following conditions:
  3110. *
  3111. * The above copyright notice and this permission notice shall be included in
  3112. * all copies or substantial portions of the Software.
  3113. *
  3114. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  3115. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  3116. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  3117. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  3118. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  3119. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  3120. * THE SOFTWARE.
  3121. */