PageRenderTime 72ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/base/lib/flourishlib/fDatabase.php

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