PageRenderTime 82ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/classphp/flourish/fDatabase.php

https://github.com/jsuarez/Lexer
PHP | 2525 lines | 1573 code | 338 blank | 614 comment | 408 complexity | 0bb9997d5969ff9f3ae26adf735eefe6 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://microsoft.com/sql/ MSSQL]
  11. * - [http://mysql.com MySQL]
  12. * - [http://oracle.com Oracle]
  13. * - [http://postgresql.org PostgreSQL]
  14. * - [http://sqlite.org SQLite]
  15. *
  16. * The class will automatically use the first of the following extensions it finds:
  17. *
  18. * - MSSQL (via ODBC)
  19. * - [http://php.net/pdo_odbc pdo_odbc]
  20. * - [http://php.net/odbc odbc]
  21. * - MSSQL
  22. * - [http://msdn.microsoft.com/en-us/library/cc296221.aspx sqlsrv]
  23. * - [http://php.net/pdo_dblib pdo_dblib]
  24. * - [http://php.net/mssql mssql] (or [http://php.net/sybase sybase])
  25. * - MySQL
  26. * - [http://php.net/mysql mysql]
  27. * - [http://php.net/mysqli mysqli]
  28. * - [http://php.net/pdo_mysql pdo_mysql]
  29. * - Oracle (via ODBC)
  30. * - [http://php.net/pdo_odbc pdo_odbc]
  31. * - [http://php.net/odbc odbc]
  32. * - Oracle
  33. * - [http://php.net/oci8 oci8]
  34. * - [http://php.net/pdo_oci pdo_oci]
  35. * - PostgreSQL
  36. * - [http://php.net/pgsql pgsql]
  37. * - [http://php.net/pdo_pgsql pdo_pgsql]
  38. * - SQLite
  39. * - [http://php.net/pdo_sqlite pdo_sqlite] (for v3.x)
  40. * - [http://php.net/sqlite sqlite] (for v2.x)
  41. *
  42. * @copyright Copyright (c) 2007-2009 Will Bond
  43. * @author Will Bond [wb] <will@flourishlib.com>
  44. * @license http://flourishlib.com/license
  45. *
  46. * @package Flourish
  47. * @link http://flourishlib.com/fDatabase
  48. *
  49. * @version 1.0.0b18
  50. * @changes 1.0.0b18 Updated the class for the new fResult and fUnbufferedResult APIs, fixed ::unescape() to not touch NULLs [wb, 2009-08-12]
  51. * @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]
  52. * @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]
  53. * @changes 1.0.0b15 Fixed a bug where auto-incremented values would not be detected when table names were quoted [wb, 2009-07-15]
  54. * @changes 1.0.0b14 Changed ::determineExtension() and ::determineCharacterSet() to be protected instead of private [wb, 2009-07-08]
  55. * @changes 1.0.0b13 Updated ::escape() to accept arrays of values for insertion into full SQL strings [wb, 2009-07-06]
  56. * @changes 1.0.0b12 Updates to ::unescape() to improve performance [wb, 2009-06-15]
  57. * @changes 1.0.0b11 Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
  58. * @changes 1.0.0b10 Changed date/time/timestamp escaping from `strtotime()` to fDate/fTime/fTimestamp for better localization support [wb, 2009-06-01]
  59. * @changes 1.0.0b9 Fixed a bug with ::escape() where floats that start with a . were encoded as `NULL` [wb, 2009-05-09]
  60. * @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]
  61. * @changes 1.0.0b7 Updated for new fCore API [wb, 2009-02-16]
  62. * @changes 1.0.0b6 Fixed a bug with executing transaction queries when using the mysqli extension [wb, 2009-02-12]
  63. * @changes 1.0.0b5 Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
  64. * @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]
  65. * @changes 1.0.0b3 Removed some unnecessary error suppresion operators [wb, 2008-12-11]
  66. * @changes 1.0.0b2 Fixed a bug with PostgreSQL when using the PDO extension and executing an INSERT statement [wb, 2008-12-11]
  67. * @changes 1.0.0b The initial implementation [wb, 2007-09-25]
  68. */
  69. class fDatabase
  70. {
  71. /**
  72. * Composes text using fText if loaded
  73. *
  74. * @param string $message The message to compose
  75. * @param mixed $component A string or number to insert into the message
  76. * @param mixed ...
  77. * @return string The composed and possible translated message
  78. */
  79. static protected function compose($message)
  80. {
  81. $args = array_slice(func_get_args(), 1);
  82. if (class_exists('fText', FALSE)) {
  83. return call_user_func_array(
  84. array('fText', 'compose'),
  85. array($message, $args)
  86. );
  87. } else {
  88. return vsprintf($message, $args);
  89. }
  90. }
  91. /**
  92. * An fCache object to cache the schema info to
  93. *
  94. * @var fCache
  95. */
  96. private $cache;
  97. /**
  98. * Database connection resource or PDO object
  99. *
  100. * @var mixed
  101. */
  102. private $connection;
  103. /**
  104. * The database name
  105. *
  106. * @var string
  107. */
  108. private $database;
  109. /**
  110. * If debugging is enabled
  111. *
  112. * @var boolean
  113. */
  114. private $debug;
  115. /**
  116. * The extension to use for the database specified
  117. *
  118. * Options include:
  119. *
  120. * - `'mssql'`
  121. * - `'mysql'`
  122. * - `'mysqli'`
  123. * - `'oci8'`
  124. * - `'odbc'`
  125. * - `'pgsql'`
  126. * - `'sqlite'`
  127. * - `'sqlsrv'`
  128. * - `'pdo'`
  129. *
  130. * @var string
  131. */
  132. protected $extension;
  133. /**
  134. * The host the database server is located on
  135. *
  136. * @var string
  137. */
  138. private $host;
  139. /**
  140. * If a transaction is in progress
  141. *
  142. * @var boolean
  143. */
  144. private $inside_transaction;
  145. /**
  146. * The password for the user specified
  147. *
  148. * @var string
  149. */
  150. private $password;
  151. /**
  152. * The port number for the host
  153. *
  154. * @var string
  155. */
  156. private $port;
  157. /**
  158. * The total number of seconds spent executing queries
  159. *
  160. * @var float
  161. */
  162. private $query_time;
  163. /**
  164. * A cache of database-specific code
  165. *
  166. * @var array
  167. */
  168. protected $schema_info;
  169. /**
  170. * The millisecond threshold for triggering a warning about SQL performance
  171. *
  172. * @var integer
  173. */
  174. private $slow_query_threshold;
  175. /**
  176. * The fSQLTranslation object for this database
  177. *
  178. * @var object
  179. */
  180. private $translation;
  181. /**
  182. * The database type: `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, or `'sqlite'`
  183. *
  184. * @var string
  185. */
  186. private $type;
  187. /**
  188. * The unbuffered query instance
  189. *
  190. * @var fUnbufferedResult
  191. */
  192. private $unbuffered_result;
  193. /**
  194. * The user to connect to the database as
  195. *
  196. * @var string
  197. */
  198. private $username;
  199. /**
  200. * Configures the connection to a database - connection is not made until the first query is executed
  201. *
  202. * @param string $type The type of the database: `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, `'sqlite'`
  203. * @param string $database Name of the database. If an ODBC connection `'dsn:'` concatenated with the DSN, if SQLite the path to the database file.
  204. * @param string $username Database username - not used for SQLite
  205. * @param string $password The password for the username specified - not used for SQLite
  206. * @param string $host Database server host or IP, defaults to localhost - not used for SQLite or ODBC connections. MySQL socket connection can be made by entering `'sock:'` followed by the socket path. PostgreSQL socket connection can be made by passing just `'sock:'`.
  207. * @param integer $port The port to connect to, defaults to the standard port for the database type specified - not used for SQLite or ODBC connections
  208. * @return fDatabase
  209. */
  210. public function __construct($type, $database, $username=NULL, $password=NULL, $host=NULL, $port=NULL)
  211. {
  212. $valid_types = array('mssql', 'mysql', 'oracle', 'postgresql', 'sqlite');
  213. if (!in_array($type, $valid_types)) {
  214. throw new fProgrammerException(
  215. 'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
  216. $type,
  217. join(', ', $valid_types)
  218. );
  219. }
  220. if (empty($database)) {
  221. throw new fProgrammerException('No database was specified');
  222. }
  223. if ($host === NULL) {
  224. $host = 'localhost';
  225. }
  226. $this->type = $type;
  227. $this->database = $database;
  228. $this->username = $username;
  229. $this->password = $password;
  230. $this->host = $host;
  231. $this->port = $port;
  232. $this->schema_info = array();
  233. $this->determineExtension();
  234. }
  235. /**
  236. * Closes the open database connection
  237. *
  238. * @internal
  239. *
  240. * @return void
  241. */
  242. public function __destruct()
  243. {
  244. if (!$this->connection) { return; }
  245. fCore::debug('Total query time: ' . $this->query_time . ' seconds', $this->debug);
  246. if ($this->extension == 'mssql') {
  247. mssql_close($this->connection);
  248. } elseif ($this->extension == 'mysql') {
  249. mysql_close($this->connection);
  250. } elseif ($this->extension == 'mysqli') {
  251. mysqli_close($this->connection);
  252. } elseif ($this->extension == 'oci8') {
  253. oci_close($this->connection);
  254. } elseif ($this->extension == 'odbc') {
  255. odbc_close($this->connection);
  256. } elseif ($this->extension == 'pgsql') {
  257. pg_close($this->connection);
  258. } elseif ($this->extension == 'sqlite') {
  259. sqlite_close($this->connection);
  260. } elseif ($this->extension == 'sqlsrv') {
  261. sqlsrv_close($this->connection);
  262. } elseif ($this->extension == 'pdo') {
  263. // PDO objects close their own connections when destroyed
  264. }
  265. }
  266. /**
  267. * All requests that hit this method should be requests for callbacks
  268. *
  269. * @internal
  270. *
  271. * @param string $method The method to create a callback for
  272. * @return callback The callback for the method requested
  273. */
  274. public function __get($method)
  275. {
  276. return array($this, $method);
  277. }
  278. /**
  279. * Checks to see if an SQL error occured
  280. *
  281. * @param fResult|fUnbufferedResult $result The result object for the query
  282. * @param mixed $extra_info The sqlite extension will pass a string error message, the oci8 extension will pass the statement resource
  283. * @return void
  284. */
  285. private function checkForError($result, $extra_info=NULL)
  286. {
  287. if ($result->getResult() === FALSE) {
  288. if ($this->extension == 'mssql') {
  289. $message = mssql_get_last_message();
  290. } elseif ($this->extension == 'mysql') {
  291. $message = mysql_error($this->connection);
  292. } elseif ($this->extension == 'mysqli') {
  293. $message = mysqli_error($this->connection);
  294. } elseif ($this->extension == 'oci8') {
  295. $error_info = oci_error($extra_info);
  296. $message = $error_info['message'];
  297. } elseif ($this->extension == 'odbc') {
  298. $message = odbc_errormsg($this->connection);
  299. } elseif ($this->extension == 'pgsql') {
  300. $message = pg_last_error($this->connection);
  301. } elseif ($this->extension == 'sqlite') {
  302. $message = $extra_info;
  303. } elseif ($this->extension == 'sqlsrv') {
  304. $error_info = sqlsrv_errors(SQLSRV_ERR_ALL);
  305. $message = $error_info[0]['message'];
  306. } elseif ($this->extension == 'pdo') {
  307. $error_info = $this->connection->errorInfo();
  308. $message = $error_info[2];
  309. }
  310. $db_type_map = array(
  311. 'mssql' => 'MSSQL',
  312. 'mysql' => 'MySQL',
  313. 'oracle' => 'Oracle',
  314. 'postgresql' => 'PostgreSQL',
  315. 'sqlite' => 'SQLite'
  316. );
  317. throw new fSQLException(
  318. '%1$s error (%2$s) in %3$s',
  319. $db_type_map[$this->type],
  320. $message,
  321. $result->getSQL()
  322. );
  323. }
  324. }
  325. /**
  326. * Clears all of the schema info out of the object and, if set, the fCache object
  327. *
  328. * @return void
  329. */
  330. public function clearCache()
  331. {
  332. $this->schema_info = array();
  333. if ($this->cache) {
  334. $this->cache->delete($this->makeCachePrefix() . 'schema_info');
  335. }
  336. if ($this->type == 'mssql') {
  337. $this->determineCharacterSet();
  338. }
  339. }
  340. /**
  341. * Connects to the database specified if no connection exists
  342. *
  343. * @return void
  344. */
  345. private function connectToDatabase()
  346. {
  347. // Don't try to reconnect if we are already connected
  348. if ($this->connection) { return; }
  349. // Establish a connection to the database
  350. if ($this->extension == 'pdo') {
  351. $odbc = strtolower(substr($this->database, 0, 4)) == 'dsn:';
  352. if ($this->type == 'mssql') {
  353. if ($odbc) {
  354. $dsn = 'odbc:' . substr($this->database, 4);
  355. } else {
  356. $separator = (fCore::checkOS('windows')) ? ',' : ':';
  357. $port = ($this->port) ? $separator . $this->port : '';
  358. $driver = (fCore::checkOs('windows')) ? 'mssql' : 'dblib';
  359. $dsn = $driver . ':host=' . $this->host . $port . ';dbname=' . $this->database;
  360. }
  361. } elseif ($this->type == 'mysql') {
  362. if (substr($this->host, 0, 5) == 'sock:') {
  363. $dsn = 'mysql:unix_socket=' . substr($this->host, 5) . ';dbname=' . $this->database;
  364. } else {
  365. $port = ($this->port) ? ';port=' . $this->port : '';
  366. $dsn = 'mysql:host=' . $this->host . ';dbname=' . $this->database . $port;
  367. }
  368. } elseif ($this->type == 'oracle') {
  369. if ($odbc) {
  370. $dsn = 'odbc:' . substr($this->database, 4);
  371. } else {
  372. $port = ($this->port) ? ':' . $this->port : '';
  373. $dsn = 'oci:dbname=' . $this->host . $port . '/' . $this->database . ';charset=AL32UTF8';
  374. }
  375. } elseif ($this->type == 'postgresql') {
  376. $dsn = 'pgsql:dbname=' . $this->database;
  377. if ($this->host && $this->host != 'sock:') {
  378. $dsn .= ' host=' . $this->host;
  379. }
  380. if ($this->port) {
  381. $dsn .= ' port=' . $this->port;
  382. }
  383. } elseif ($this->type == 'sqlite') {
  384. $dsn = 'sqlite:' . $this->database;
  385. }
  386. try {
  387. $this->connection = new PDO($dsn, $this->username, $this->password);
  388. if ($this->type == 'mysql') {
  389. $this->connection->setAttribute(PDO::MYSQL_ATTR_DIRECT_QUERY, 1);
  390. }
  391. } catch (PDOException $e) {
  392. $this->connection = FALSE;
  393. }
  394. }
  395. if ($this->extension == 'sqlite') {
  396. $this->connection = sqlite_open($this->database);
  397. }
  398. if ($this->extension == 'mssql') {
  399. $separator = (fCore::checkOS('windows')) ? ',' : ':';
  400. $this->connection = mssql_connect(($this->port) ? $this->host . $separator . $this->port : $this->host, $this->username, $this->password);
  401. if ($this->connection !== FALSE && mssql_select_db($this->database, $this->connection) === FALSE) {
  402. $this->connection = FALSE;
  403. }
  404. }
  405. if ($this->extension == 'mysql') {
  406. if (substr($this->host, 0, 5) == 'sock:') {
  407. $host = substr($this->host, 4);
  408. } elseif ($this->port) {
  409. $host = $this->host . ':' . $this->port;
  410. } else {
  411. $host = $this->host;
  412. }
  413. $this->connection = mysql_connect($host, $this->username, $this->password);
  414. if ($this->connection !== FALSE && mysql_select_db($this->database, $this->connection) === FALSE) {
  415. $this->connection = FALSE;
  416. }
  417. }
  418. if ($this->extension == 'mysqli') {
  419. if (substr($this->host, 0, 5) == 'sock:') {
  420. $this->connection = mysqli_connect('localhost', $this->username, $this->password, $this->database, $this->port, substr($this->host, 5));
  421. } elseif ($this->port) {
  422. $this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->database, $this->port);
  423. } else {
  424. $this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->database);
  425. }
  426. }
  427. if ($this->extension == 'oci8') {
  428. $this->connection = oci_connect($this->username, $this->password, $this->host . ($this->port ? ':' . $this->port : '') . '/' . $this->database, 'AL32UTF8');
  429. }
  430. if ($this->extension == 'odbc') {
  431. $this->connection = odbc_connect(substr($this->database, 4), $this->username, $this->password);
  432. }
  433. if ($this->extension == 'pgsql') {
  434. $connection_string = "dbname='" . addslashes($this->database) . "'";
  435. if ($this->host && $this->host != 'sock:') {
  436. $connection_string .= " host='" . addslashes($this->host) . "'";
  437. }
  438. if ($this->username) {
  439. $connection_string .= " user='" . addslashes($this->username) . "'";
  440. }
  441. if ($this->password) {
  442. $connection_string .= " password='" . addslashes($this->password) . "'";
  443. }
  444. if ($this->port) {
  445. $connection_string .= " port='" . $this->port . "'";
  446. }
  447. $this->connection = pg_connect($connection_string);
  448. }
  449. if ($this->extension == 'sqlsrv') {
  450. $options = array(
  451. 'Database' => $this->database,
  452. 'UID' => $this->username,
  453. 'PWD' => $this->password
  454. );
  455. $this->connection = sqlsrv_connect($this->host, $options);
  456. }
  457. // Ensure the connection was established
  458. if ($this->connection === FALSE) {
  459. throw new fConnectivityException(
  460. 'Unable to connect to database'
  461. );
  462. }
  463. // Make MySQL act more strict and use UTF-8
  464. if ($this->type == 'mysql') {
  465. $this->query("SET SQL_MODE = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE'");
  466. $this->query("SET NAMES 'utf8'");
  467. $this->query("SET CHARACTER SET utf8");
  468. }
  469. // Make SQLite behave like other DBs for assoc arrays
  470. if ($this->type == 'sqlite') {
  471. $this->query('PRAGMA short_column_names = 1');
  472. }
  473. // Fix some issues with mssql
  474. if ($this->type == 'mssql') {
  475. if (!isset($this->schema_info['character_set'])) {
  476. $this->determineCharacterSet();
  477. }
  478. $this->query('SET TEXTSIZE 65536');
  479. }
  480. // Make PostgreSQL use UTF-8
  481. if ($this->type == 'postgresql') {
  482. $this->query("SET NAMES 'UTF8'");
  483. }
  484. // Oracle has different date and timestamp defaults
  485. if ($this->type == 'oracle') {
  486. $this->query("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'");
  487. $this->query("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'");
  488. $this->query("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZR'");
  489. $this->query("ALTER SESSION SET NLS_TIME_FORMAT = 'HH24:MI:SS'");
  490. $this->query("ALTER SESSION SET NLS_TIME_TZ_FORMAT = 'HH24:MI:SS TZR'");
  491. }
  492. }
  493. /**
  494. * Determines the character set of a SQL Server database
  495. *
  496. * @return void
  497. */
  498. protected function determineCharacterSet()
  499. {
  500. $this->schema_info['character_set'] = 'WINDOWS-1252';
  501. $this->schema_info['character_set'] = $this->query("SELECT 'WINDOWS-' + CONVERT(VARCHAR, COLLATIONPROPERTY(CONVERT(NVARCHAR, DATABASEPROPERTYEX(DB_NAME(), 'Collation')), 'CodePage')) AS charset")->fetchScalar();
  502. if ($this->cache) {
  503. $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);
  504. }
  505. }
  506. /**
  507. * Figures out which extension to use for the database type selected
  508. *
  509. * @return void
  510. */
  511. protected function determineExtension()
  512. {
  513. switch ($this->type) {
  514. case 'mssql':
  515. $odbc = strtolower(substr($this->database, 0, 4)) == 'dsn:';
  516. if ($odbc) {
  517. if (extension_loaded('odbc')) {
  518. $this->extension = 'odbc';
  519. } elseif (class_exists('PDO', FALSE) && in_array('odbc', PDO::getAvailableDrivers())) {
  520. $this->extension = 'pdo';
  521. } else {
  522. $type = 'MSSQL (ODBC)';
  523. $exts = 'odbc, pdo_odbc';
  524. }
  525. } else {
  526. if (extension_loaded('sqlsrv')) {
  527. $this->extension = 'sqlsrv';
  528. } elseif (extension_loaded('mssql')) {
  529. $this->extension = 'mssql';
  530. } elseif (class_exists('PDO', FALSE) && (in_array('dblib', PDO::getAvailableDrivers()) || in_array('mssql', PDO::getAvailableDrivers()))) {
  531. $this->extension = 'pdo';
  532. } else {
  533. $type = 'MSSQL';
  534. $exts = 'mssql, sqlsrv, pdo_dblib (linux), pdo_mssql (windows)';
  535. }
  536. }
  537. break;
  538. case 'mysql':
  539. if (extension_loaded('mysql')) {
  540. $this->extension = 'mysql';
  541. } elseif (class_exists('PDO', FALSE) && in_array('mysql', PDO::getAvailableDrivers())) {
  542. $this->extension = 'pdo';
  543. } elseif (extension_loaded('mysqli')) {
  544. $this->extension = 'mysqli';
  545. } else {
  546. $type = 'MySQL';
  547. $exts = 'mysql, pdo_mysql, mysqli';
  548. }
  549. break;
  550. case 'oracle':
  551. $odbc = strtolower(substr($this->database, 0, 4)) == 'dsn:';
  552. if ($odbc) {
  553. if (extension_loaded('odbc')) {
  554. $this->extension = 'odbc';
  555. } elseif (class_exists('PDO', FALSE) && in_array('odbc', PDO::getAvailableDrivers())) {
  556. $this->extension = 'pdo';
  557. } else {
  558. $type = 'Oracle (ODBC)';
  559. $exts = 'odbc, pdo_odbc';
  560. }
  561. } else {
  562. if (extension_loaded('oci8')) {
  563. $this->extension = 'oci8';
  564. } elseif (class_exists('PDO', FALSE) && in_array('oci', PDO::getAvailableDrivers())) {
  565. $this->extension = 'pdo';
  566. } else {
  567. $type = 'Oracle';
  568. $exts = 'oci8, pdo_oci';
  569. }
  570. }
  571. break;
  572. case 'postgresql':
  573. if (extension_loaded('pgsql')) {
  574. $this->extension = 'pgsql';
  575. } elseif (class_exists('PDO', FALSE) && in_array('pgsql', PDO::getAvailableDrivers())) {
  576. $this->extension = 'pdo';
  577. } else {
  578. $type = 'PostgreSQL';
  579. $exts = 'pgsql, pdo_pgsql';
  580. }
  581. break;
  582. case 'sqlite':
  583. $sqlite_version = 0;
  584. if (file_exists($this->database)) {
  585. $database_handle = fopen($this->database, 'r');
  586. $database_version = fread($database_handle, 64);
  587. fclose($database_handle);
  588. if (strpos($database_version, 'SQLite format 3') !== FALSE) {
  589. $sqlite_version = 3;
  590. } elseif (strpos($database_version, '** This file contains an SQLite 2.1 database **') !== FALSE) {
  591. $sqlite_version = 2;
  592. } else {
  593. throw new fConnectivityException(
  594. 'The database specified does not appear to be a valid %1$s or %2$s database',
  595. 'SQLite v2.1',
  596. 'v3'
  597. );
  598. }
  599. }
  600. if ((!$sqlite_version || $sqlite_version == 3) && class_exists('PDO', FALSE) && in_array('sqlite', PDO::getAvailableDrivers())) {
  601. $this->extension = 'pdo';
  602. } elseif ($sqlite_version == 3 && (!class_exists('PDO', FALSE) || !in_array('sqlite', PDO::getAvailableDrivers()))) {
  603. throw new fEnvironmentException(
  604. 'The database specified is an %1$s database and the %2$s extension is not installed',
  605. 'SQLite v3',
  606. 'pdo_sqlite'
  607. );
  608. } elseif ((!$sqlite_version || $sqlite_version == 2) && extension_loaded('sqlite')) {
  609. $this->extension = 'sqlite';
  610. } elseif ($sqlite_version == 2 && !extension_loaded('sqlite')) {
  611. throw new fEnvironmentException(
  612. 'The database specified is an %1$s database and the %2$s extension is not installed',
  613. 'SQLite v2.1',
  614. 'sqlite'
  615. );
  616. } else {
  617. $type = 'SQLite';
  618. $exts = 'pdo_sqlite, sqlite';
  619. }
  620. break;
  621. }
  622. if (!$this->extension) {
  623. throw new fEnvironmentException(
  624. 'The server does not have any of the following extensions for %2$s support: %2$s',
  625. $type,
  626. $exts
  627. );
  628. }
  629. }
  630. /**
  631. * Sets the schema info to be cached to the fCache object specified
  632. *
  633. * @param fCache $cache The cache to cache to
  634. * @return void
  635. */
  636. public function enableCaching($cache)
  637. {
  638. $this->cache = $cache;
  639. $this->schema_info = $this->cache->get($this->makeCachePrefix() . 'schema_info', array());
  640. }
  641. /**
  642. * Sets if debug messages should be shown
  643. *
  644. * @param boolean $flag If debugging messages should be shown
  645. * @return void
  646. */
  647. public function enableDebugging($flag)
  648. {
  649. $this->debug = (boolean) $flag;
  650. }
  651. /**
  652. * Sets a flag to trigger a PHP warning message whenever a query takes longer than the millisecond threshold specified
  653. *
  654. * It is recommended to use the error handling features of
  655. * fCore::enableErrorHandling() to log or email these warnings.
  656. *
  657. * @param integer $threshold The limit (in milliseconds) of how long an SQL query can take before a warning is triggered
  658. * @return void
  659. */
  660. public function enableSlowQueryWarnings($threshold)
  661. {
  662. $this->slow_query_threshold = (int) $threshold;
  663. }
  664. /**
  665. * Escapes a value for insertion into SQL
  666. *
  667. * The valid data types are:
  668. *
  669. * - `'blob'`
  670. * - `'boolean'`
  671. * - `'date'`
  672. * - `'float'`
  673. * - `'integer'`
  674. * - `'string'` (also varchar, char or text)
  675. * - `'varchar'`
  676. * - `'char'`
  677. * - `'text'`
  678. * - `'time'`
  679. * - `'timestamp'`
  680. *
  681. * In addition to being able to specify the data type, you can also pass
  682. * in an SQL statement with data type placeholders in the following form:
  683. *
  684. * - `%l` for a blob
  685. * - `%b` for a boolean
  686. * - `%d` for a date
  687. * - `%f` for a float
  688. * - `%i` for an integer
  689. * - `%s` for a string
  690. * - `%t` for a time
  691. * - `%p` for a timestamp
  692. *
  693. * Depending on what `$sql_or_type` and `$value` are, the output will be
  694. * slightly different. If `$sql_or_type` is a data type or a single
  695. * placeholder and `$value` is:
  696. *
  697. * - a scalar value - an escaped SQL string is returned
  698. * - an array - an array of escaped SQL strings is returned
  699. *
  700. * If `$sql_or_type` is a SQL string and `$value` is:
  701. *
  702. * - a scalar value - the escaped value is inserted into the SQL string
  703. * - an array - the escaped values are inserted into the SQL string separated by commas
  704. *
  705. * If `$sql_or_type` is a SQL string, it is also possible to pass an array
  706. * of all values as a single parameter instead of one value per parameter.
  707. * An example would look like the following:
  708. *
  709. * {{{
  710. * #!php
  711. * $db->escape(
  712. * "SELECT * FROM users WHERE status = %s AND authorization_level = %s",
  713. * array('Active', 'Admin')
  714. * );
  715. * }}}
  716. *
  717. * @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
  718. * @param mixed $value The value to escape - both single values and arrays of values are supported, see method description for details
  719. * @param mixed ...
  720. * @return mixed The escaped value/SQL or an array of the escaped values
  721. */
  722. public function escape($sql_or_type, $value)
  723. {
  724. $values = array_slice(func_get_args(), 1);
  725. if (sizeof($values) < 1) {
  726. throw new fProgrammerException(
  727. 'No value was specified to escape'
  728. );
  729. }
  730. // Convert all objects into strings
  731. $values = $this->scalarize($values);
  732. $value = array_shift($values);
  733. // Handle single value escaping
  734. $callback = NULL;
  735. switch ($sql_or_type) {
  736. case 'blob':
  737. case '%l':
  738. $callback = $this->escapeBlob;
  739. break;
  740. case 'boolean':
  741. case '%b':
  742. $callback = $this->escapeBoolean;
  743. break;
  744. case 'date':
  745. case '%d':
  746. $callback = $this->escapeDate;
  747. break;
  748. case 'float':
  749. case '%f':
  750. $callback = $this->escapeFloat;
  751. break;
  752. case 'integer':
  753. case '%i':
  754. $callback = $this->escapeInteger;
  755. break;
  756. case 'string':
  757. case 'varchar':
  758. case 'char':
  759. case 'text':
  760. case '%s':
  761. $callback = $this->escapeString;
  762. break;
  763. case 'time':
  764. case '%t':
  765. $callback = $this->escapeTime;
  766. break;
  767. case 'timestamp':
  768. case '%p':
  769. $callback = $this->escapeTimestamp;
  770. break;
  771. }
  772. if ($callback) {
  773. if (is_array($value)) {
  774. // If the values were passed as a single array, this handles that
  775. if (count($value) == 1 && is_array(current($value))) {
  776. $value = current($value);
  777. }
  778. return array_map($callback, $value);
  779. }
  780. return call_user_func($callback, $value);
  781. }
  782. // Fix \' in MySQL and PostgreSQL
  783. if(($this->type == 'mysql' || $this->type == 'postgresql') && strpos($sql_or_type, '\\') !== FALSE) {
  784. $sql_or_type = preg_replace("#(?<!\\\\)((\\\\{2})*)\\\\'#", "\\1''", $sql_or_type);
  785. }
  786. // Separate the SQL from quoted values
  787. preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql_or_type, $matches);
  788. $temp_sql = '';
  789. $strings = array();
  790. // Replace strings with a placeholder so they don't mess use the regex parsing
  791. foreach ($matches[0] as $match) {
  792. if ($match[0] == "'") {
  793. $strings[] = $match;
  794. $match = ':string_' . (sizeof($strings)-1);
  795. }
  796. $temp_sql .= $match;
  797. }
  798. $pieces = preg_split('#(%[lbdfistp])\b#', $temp_sql, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  799. $sql = '';
  800. // If the values were passed as a single array, this handles that
  801. if (count($values) == 0 && is_array($value)) {
  802. $placeholders = 0;
  803. foreach ($pieces as $piece) {
  804. if (strlen($piece) == 2 && $piece[0] == '%') {
  805. $placeholders++;
  806. }
  807. }
  808. if ($placeholders == count($value)) {
  809. $values = $value;
  810. $value = array_shift($values);
  811. }
  812. }
  813. $missing_values = -1;
  814. foreach ($pieces as $piece) {
  815. switch ($piece) {
  816. case '%l':
  817. $callback = $this->escapeBlob;
  818. break;
  819. case '%b':
  820. $callback = $this->escapeBoolean;
  821. break;
  822. case '%d':
  823. $callback = $this->escapeDate;
  824. break;
  825. case '%f':
  826. $callback = $this->escapeFloat;
  827. break;
  828. case '%i':
  829. $callback = $this->escapeInteger;
  830. break;
  831. case '%s':
  832. $callback = $this->escapeString;
  833. break;
  834. case '%t':
  835. $callback = $this->escapeTime;
  836. break;
  837. case '%p':
  838. $callback = $this->escapeTimestamp;
  839. break;
  840. default:
  841. $sql .= $piece;
  842. continue 2;
  843. }
  844. if (is_array($value)) {
  845. $sql .= join(', ', array_map($callback, $value));
  846. } else {
  847. $sql .= call_user_func($callback, $value);
  848. }
  849. if (sizeof($values)) {
  850. $value = array_shift($values);
  851. } else {
  852. $value = NULL;
  853. $missing_values++;
  854. }
  855. }
  856. if ($missing_values > 0) {
  857. throw new fProgrammerException(
  858. '%1$s value(s) are missing for the placeholders in: %2$s',
  859. $missing_values,
  860. $sql_or_type
  861. );
  862. }
  863. if (sizeof($values)) {
  864. throw new fProgrammerException(
  865. '%1$s extra value(s) were passed for the placeholders in: %2$s',
  866. sizeof($values),
  867. $sql_or_type
  868. );
  869. }
  870. $string_number = 0;
  871. foreach ($strings as $string) {
  872. $string = strtr($string, array('\\' => '\\\\', '$' => '\\$'));
  873. $sql = preg_replace('#:string_' . $string_number++ . '\b#', $string, $sql);
  874. }
  875. return $sql;
  876. }
  877. /**
  878. * Escapes a blob for use in SQL, includes surround quotes when appropriate
  879. *
  880. * A `NULL` value will be returned as `'NULL'`
  881. *
  882. * @param string $value The blob to escape
  883. * @return string The escaped blob
  884. */
  885. private function escapeBlob($value)
  886. {
  887. if ($value === NULL) {
  888. return 'NULL';
  889. }
  890. $this->connectToDatabase();
  891. if ($this->type == 'mysql') {
  892. return "x'" . bin2hex($value) . "'";
  893. } elseif ($this->type == 'postgresql') {
  894. $output = '';
  895. for ($i=0; $i<strlen($value); $i++) {
  896. $output .= '\\\\' . str_pad(decoct(ord($value[$i])), 3, '0', STR_PAD_LEFT);
  897. }
  898. return "E'" . $output . "'";
  899. } elseif ($this->extension == 'sqlite') {
  900. return "'" . bin2hex($value) . "'";
  901. } elseif ($this->type == 'sqlite') {
  902. return "X'" . bin2hex($value) . "'";
  903. } elseif ($this->type == 'mssql') {
  904. return '0x' . bin2hex($value);
  905. } elseif ($this->type == 'oracle') {
  906. return "'" . bin2hex($value) . "'";
  907. }
  908. }
  909. /**
  910. * Escapes a boolean for use in SQL, includes surround quotes when appropriate
  911. *
  912. * A `NULL` value will be returned as `'NULL'`
  913. *
  914. * @param boolean $value The boolean to escape
  915. * @return string The database equivalent of the boolean passed
  916. */
  917. private function escapeBoolean($value)
  918. {
  919. if ($value === NULL) {
  920. return 'NULL';
  921. }
  922. if (in_array($this->type, array('postgresql', 'mysql'))) {
  923. return ($value) ? 'TRUE' : 'FALSE';
  924. } elseif (in_array($this->type, array('mssql', 'sqlite'))) {
  925. return ($value) ? "'1'" : "'0'";
  926. } elseif ($this->type == 'oracle') {
  927. return ($value) ? '1' : '0';
  928. }
  929. }
  930. /**
  931. * Escapes a date for use in SQL, includes surrounding quotes
  932. *
  933. * A `NULL` or invalid value will be returned as `'NULL'`
  934. *
  935. * @param string $value The date to escape
  936. * @return string The escaped date
  937. */
  938. private function escapeDate($value)
  939. {
  940. if ($value === NULL) {
  941. return 'NULL';
  942. }
  943. try {
  944. $value = new fDate($value);
  945. return "'" . $value->format('Y-m-d') . "'";
  946. } catch (fValidationException $e) {
  947. return 'NULL';
  948. }
  949. }
  950. /**
  951. * Escapes a float for use in SQL
  952. *
  953. * A `NULL` value will be returned as `'NULL'`
  954. *
  955. * @param float $value The float to escape
  956. * @return string The escaped float
  957. */
  958. private function escapeFloat($value)
  959. {
  960. if ($value === NULL) {
  961. return 'NULL';
  962. }
  963. if (!strlen($value)) {
  964. return 'NULL';
  965. }
  966. if (!preg_match('#^[+\-]?([0-9]+(\.[0-9]+)?|(\.[0-9]+))$#D', $value)) {
  967. return 'NULL';
  968. }
  969. return (string) $value;
  970. }
  971. /**
  972. * Escapes an integer for use in SQL
  973. *
  974. * A `NULL` or invalid value will be returned as `'NULL'`
  975. *
  976. * @param integer $value The integer to escape
  977. * @return string The escaped integer
  978. */
  979. private function escapeInteger($value)
  980. {
  981. if ($value === NULL) {
  982. return 'NULL';
  983. }
  984. if (!strlen($value)) {
  985. return 'NULL';
  986. }
  987. if (!preg_match('#^[+\-]?[0-9]+$#D', $value)) {
  988. return 'NULL';
  989. }
  990. return (string) $value;
  991. }
  992. /**
  993. * Escapes a string for use in SQL, includes surrounding quotes
  994. *
  995. * A `NULL` value will be returned as `'NULL'`
  996. *
  997. * @param string $value The string to escape
  998. * @return string The escaped string
  999. */
  1000. private function escapeString($value)
  1001. {
  1002. if ($value === NULL) {
  1003. return 'NULL';
  1004. }
  1005. $this->connectToDatabase();
  1006. if ($this->extension == 'mysql') {
  1007. return "'" . mysql_real_escape_string($value, $this->connection) . "'";
  1008. } elseif ($this->extension == 'mysqli') {
  1009. return "'" . mysqli_real_escape_string($this->connection, $value) . "'";
  1010. } elseif ($this->extension == 'pgsql') {
  1011. return "'" . pg_escape_string($value) . "'";
  1012. } elseif ($this->extension == 'sqlite') {
  1013. return "'" . sqlite_escape_string($value) . "'";
  1014. } elseif ($this->type == 'oracle') {
  1015. return "'" . str_replace("'", "''", $value) . "'";
  1016. } elseif ($this->type == 'mssql') {
  1017. // If there are any non-ASCII characters, we need to escape
  1018. if (preg_match('#[^\x00-\x7F]#', $value)) {
  1019. preg_match_all('#.|^\z#us', $value, $characters);
  1020. $output = "";
  1021. $last_type = NULL;
  1022. foreach ($characters[0] as $character) {
  1023. if (strlen($character) > 1) {
  1024. $b = array_map('ord', str_split($character));
  1025. switch (strlen($character)) {
  1026. case 2:
  1027. $bin = substr(decbin($b[0]), 3) .
  1028. substr(decbin($b[1]), 2);
  1029. break;
  1030. case 3:
  1031. $bin = substr(decbin($b[0]), 4) .
  1032. substr(decbin($b[1]), 2) .
  1033. substr(decbin($b[2]), 2);
  1034. break;
  1035. // If it is a 4-byte character, MSSQL can't store it
  1036. // so instead store a ?
  1037. default:
  1038. $output .= '?';
  1039. continue;
  1040. }
  1041. if ($last_type == 'nchar') {
  1042. $output .= '+';
  1043. } elseif ($last_type == 'char') {
  1044. $output .= "'+";
  1045. }
  1046. $output .= "NCHAR(" . bindec($bin) . ")";
  1047. $last_type = 'nchar';
  1048. } else {
  1049. if (!$last_type) {
  1050. $output .= "'";
  1051. } elseif ($last_type == 'nchar') {
  1052. $output .= "+'";
  1053. }
  1054. $output .= $character;
  1055. // Escape single quotes
  1056. if ($character == "'") {
  1057. $output .= "'";
  1058. }
  1059. $last_type = 'char';
  1060. }
  1061. }
  1062. if ($last_type == 'char') {
  1063. $output .= "'";
  1064. } elseif (!$last_type) {
  1065. $output .= "''";
  1066. }
  1067. // ASCII text is normal
  1068. } else {
  1069. $output = "'" . str_replace("'", "''", $value) . "'";
  1070. }
  1071. # a \ before a \r\n has to be escaped with another \
  1072. return preg_replace('#(?<!\\\\)\\\\(?=\r\n)#', '\\\\\\\\', $output);
  1073. } elseif ($this->extension == 'pdo') {
  1074. return $this->connection->quote($value);
  1075. }
  1076. }
  1077. /**
  1078. * Escapes a time for use in SQL, includes surrounding quotes
  1079. *
  1080. * A `NULL` or invalid value will be returned as `'NULL'`
  1081. *
  1082. * @param string $value The time to escape
  1083. * @return string The escaped time
  1084. */
  1085. private function escapeTime($value)
  1086. {
  1087. if ($value === NULL) {
  1088. return 'NULL';
  1089. }
  1090. try {
  1091. $value = new fTime($value);
  1092. if ($this->type == 'mssql' || $this->type == 'oracle') {
  1093. return "'" . $value->format('1970-01-01 H:i:s') . "'";
  1094. }
  1095. return "'" . $value->format('H:i:s') . "'";
  1096. } catch (fValidationException $e) {
  1097. return 'NULL';
  1098. }
  1099. }
  1100. /**
  1101. * Escapes a timestamp for use in SQL, includes surrounding quotes
  1102. *
  1103. * A `NULL` or invalid value will be returned as `'NULL'`
  1104. *
  1105. * @param string $value The timestamp to escape
  1106. * @return string The escaped timestamp
  1107. */
  1108. private function escapeTimestamp($value)
  1109. {
  1110. if ($value === NULL) {
  1111. return 'NULL';
  1112. }
  1113. try {
  1114. $value = new fTimestamp($value);
  1115. return "'" . $value->format('Y-m-d H:i:s') . "'";
  1116. } catch (fValidationException $e) {
  1117. return 'NULL';
  1118. }
  1119. }
  1120. /**
  1121. * Executes an SQL query
  1122. *
  1123. * @param fResult $result The result object for the query
  1124. * @return void
  1125. */
  1126. private function executeQuery($result)
  1127. {
  1128. // We don't want errors and an exception
  1129. $old_level = error_reporting(error_reporting() & ~E_WARNING);
  1130. if ($this->extension == 'mssql') {
  1131. $result->setResult(mssql_query($result->getSQL(), $this->connection));
  1132. } elseif ($this->extension == 'mysql') {
  1133. $result->setResult(mysql_query($result->getSQL(), $this->connection));
  1134. } elseif ($this->extension == 'mysqli') {
  1135. $result->setResult(mysqli_query($this->connection, $result->getSQL()));
  1136. } elseif ($this->extension == 'oci8') {
  1137. $oci_statement = oci_parse($this->connection, $result->getSQL());
  1138. if (oci_execute($oci_statement, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS)) {
  1139. oci_fetch_all($oci_statement, $rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW + OCI_ASSOC);
  1140. $result->setResult($rows);
  1141. unset($rows);
  1142. } else {
  1143. $result->setResult(FALSE);
  1144. }
  1145. } elseif ($this->extension == 'odbc') {
  1146. $resource = odbc_exec($this->connection, $result->getSQL());
  1147. if (is_resource($resource)) {
  1148. $rows = array();
  1149. // Allow up to 1MB of binary data
  1150. odbc_longreadlen($resource, 1048576);
  1151. odbc_binmode($resource, ODBC_BINMODE_CONVERT);
  1152. while ($row = odbc_fetch_array($resource)) {
  1153. $rows[] = $row;
  1154. }
  1155. $result->setResult($rows);
  1156. unset($rows);
  1157. } else {
  1158. $result->setResult($resource);
  1159. }
  1160. } elseif ($this->extension == 'pgsql') {
  1161. $result->setResult(pg_query($this->connection, $result->getSQL()));
  1162. } elseif ($this->extension == 'sqlite') {
  1163. $result->setResult(sqlite_query($this->connection, $result->getSQL(), SQLITE_ASSOC, $sqlite_error_message));
  1164. } elseif ($this->extension == 'sqlsrv') {
  1165. $resource = sqlsrv_query($this->connection, $result->getSQL());
  1166. if (is_resource($resource)) {
  1167. $rows = array();
  1168. while ($row = sqlsrv_fetch_array($resource, SQLSRV_FETCH_ASSOC)) {
  1169. $rows[] = $row;
  1170. }
  1171. $result->setResult($rows);
  1172. unset($rows);
  1173. } else {
  1174. $result->setResult($resource);
  1175. }
  1176. } elseif ($this->extension == 'pdo') {
  1177. if (preg_match('#^\s*CREATE(\s+OR\s+REPLACE)?\s+TRIGGER#i', $result->getSQL())) {
  1178. $this->connection->exec($result->getSQL());
  1179. $pdo_statement = FALSE;
  1180. $returned_rows = array();
  1181. } else {
  1182. $pdo_statement = $this->connection->query($result->getSQL());
  1183. $returned_rows = (is_object($pdo_statement)) ? $pdo_statement->fetchAll(PDO::FETCH_ASSOC) : $pdo_statement;
  1184. // The pdo_pgsql driver likes to return empty rows equal to the number of affected rows for insert and deletes
  1185. if ($this->type == 'postgresql' && $returned_rows && $returned_rows[0] == array()) {
  1186. $returned_rows = array();
  1187. }
  1188. }
  1189. $result->setResult($returned_rows);
  1190. }
  1191. error_reporting($old_level);
  1192. if ($this->extension == 'sqlite') {
  1193. $this->checkForError($result, $sqlite_error_message);
  1194. } elseif ($this->extension == 'oci8') {
  1195. $this->checkForError($result, $oci_statement);
  1196. } else {
  1197. $this->checkForError($result);
  1198. }
  1199. if ($this->extension == 'pdo') {
  1200. $this->setAffectedRows($result, $pdo_statement);
  1201. if ($pdo_statement) {
  1202. $pdo_statement->closeCursor();
  1203. }
  1204. unset($pdo_statement);
  1205. } elseif ($this->extension == 'oci8') {
  1206. $this->setAffectedRows($result, $oci_statement);
  1207. oci_free_statement($oci_statement);
  1208. } elseif ($this->extension == 'odbc') {
  1209. $this->setAffectedRows($result, $resource);
  1210. odbc_free_result($resource);
  1211. } elseif ($this->extension == 'sqlsrv') {
  1212. $this->setAffectedRows($result, $resource);
  1213. sqlsrv_free_stmt($resource);
  1214. } else {
  1215. $this->setAffectedRows($result);
  1216. }
  1217. $this->setReturnedRows($result);
  1218. $this->handleAutoIncrementedValue($result);
  1219. }
  1220. /**
  1221. * Executes an unbuffered SQL query
  1222. *
  1223. * @param fUnbufferedResult $result The result object for the query
  1224. * @return void
  1225. */
  1226. private function executeUnbufferedQuery($result)
  1227. {
  1228. $old_level = error_reporting(error_reporting() & ~E_WARNING);
  1229. if ($this->extension == 'mssql') {
  1230. $result->setResult(mssql_query($result->getSQL(), $this->connection, 20));
  1231. } elseif ($this->extension == 'mysql') {
  1232. $result->setResult(mysql_unbuffered_query($result->getSQL(), $this->connection));
  1233. } elseif ($this->extension == 'mysqli') {
  1234. $result->setResult(mysqli_query($this->connection, $result->getSQL(), MYSQLI_USE_RESULT));
  1235. } elseif ($this->extension == 'oci8') {
  1236. $oci_statement = oci_parse($this->connection, $result->getSQL());
  1237. $result->setResult(oci_execute($oci_statement, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS) ? $oci_statement : FALSE);
  1238. } elseif ($this->extension == 'odbc') {
  1239. $result->setResult(odbc_exec($this->connection, $result->getSQL()));
  1240. } elseif ($this->extension == 'pgsql') {
  1241. $result->setResult(pg_query($this->connection, $result->getSQL()));
  1242. } elseif ($this->extension == 'sqlite') {
  1243. $result->setResult(sqlite_unbuffered_query($this->connection, $result->getSQL(), SQLITE_ASSOC, $sqlite_error_message));
  1244. } elseif ($this->extension == 'sqlsrv') {
  1245. $result->setResult(sqlsrv_query($this->connection, $result->getSQL()));
  1246. } elseif ($this->extension == 'pdo') {
  1247. $result->setResult($this->connection->query($result->getSQL()));
  1248. }
  1249. error_reporting($old_level);
  1250. if ($this->extension == 'sqlite') {
  1251. $this->checkForError($result, $sqlite_error_message);
  1252. } elseif ($this->extension == 'oci8') {
  1253. $this->checkForError($result, $oci_statement);
  1254. } else {
  1255. $this->checkForError($result);
  1256. }
  1257. }
  1258. /**
  1259. * Takes in a string of SQL that contains multiple queries and returns any array of them
  1260. *
  1261. * @param string $sql The string of SQL to parse for queries
  1262. * @return array The individual SQL queries
  1263. */
  1264. private function explodeQueries($sql)
  1265. {
  1266. $sql_queries = array();
  1267. // Separate the SQL from quoted values
  1268. preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql, $matches);
  1269. $cur_sql = '';
  1270. foreach ($matches[0] as $match) {
  1271. // This is a quoted string value, don't do anything to it
  1272. if ($match[0] == "'") {
  1273. $cur_sql .= $match;
  1274. // Handle the SQL, exploding on any ; that isn't escaped with a \
  1275. } else {
  1276. $sql_strings = preg_split('#(?<!\\\\);#', $match);
  1277. $cur_sql .= $sql_strings[0];
  1278. for ($i=1; $i < sizeof($sql_strings); $i++) {
  1279. $cur_sql = trim($cur_sql);
  1280. if ($cur_sql) {
  1281. $sql_queries[] = $cur_sql;
  1282. }
  1283. $cur_sql = $sql_strings[$i];
  1284. }
  1285. }
  1286. }
  1287. if (trim($cur_sql)) {
  1288. $sql_queries[] = $cur_sql;
  1289. }
  1290. return $sql_queries;
  1291. }
  1292. /**
  1293. * Returns the database connection resource or object
  1294. *
  1295. * @return mixed The database connection
  1296. */
  1297. public function getConnection()
  1298. {
  1299. $this->connectToDatabase();
  1300. return $this->connection;
  1301. }
  1302. /**
  1303. * Gets the name of the database currently connected to
  1304. *
  1305. * @return string The name of the database currently connected to
  1306. */
  1307. public function getDatabase()
  1308. {
  1309. return $this->database;
  1310. }
  1311. /**
  1312. * Gets the php extension being used
  1313. *
  1314. * @internal
  1315. *
  1316. * @return string The php extension used for database interaction
  1317. */
  1318. public function getExtension()
  1319. {
  1320. return $this->extension;
  1321. }
  1322. /**
  1323. * Gets the host for this database
  1324. *
  1325. * @return string The host
  1326. */
  1327. public function getHost()
  1328. {
  1329. return $this->host;
  1330. }
  1331. /**
  1332. * Gets the port for this database
  1333. *
  1334. * @return string The port
  1335. */
  1336. public function getPort()
  1337. {
  1338. return $this->port;
  1339. }
  1340. /**
  1341. * Gets the fSQLTranslation object used for translated queries
  1342. *
  1343. * @return fSQLTranslation The SQL translation object
  1344. */
  1345. public function getSQLTranslation()
  1346. {
  1347. if (!$this->translation) { new fSQLTranslation($this); }
  1348. return $this->translation;
  1349. }
  1350. /**
  1351. * Gets the database type
  1352. *
  1353. * @return string The database type: `'mssql'`, `'mysql'`, `'postgresql'` or `'sqlite'`
  1354. */
  1355. public function getType()
  1356. {
  1357. return $this->type;
  1358. }
  1359. /**
  1360. * Gets the username for this database
  1361. *
  1362. * @return string The username
  1363. */
  1364. public function getUsername()
  1365. {
  1366. return $this->username;
  1367. }
  1368. /**
  1369. * Will grab the auto incremented value from the last query (if one exists)
  1370. *
  1371. * @param fResult $result The result object for the query
  1372. * @return void
  1373. */
  1374. private function handleAutoIncrementedValue($result)
  1375. {
  1376. if (!preg_match('#^\s*INSERT\s+INTO\s+(?:`|"|\[)?(\w+)(?:`|"|\])?#i', $result->getSQL(), $table_match)) {
  1377. $result->setAutoIncrementedValue(NULL);
  1378. return;
  1379. }
  1380. $table = strtolower($table_match[1]);
  1381. $insert_id = NULL;
  1382. if ($this->type == 'oracle') {
  1383. if (!isset($this->schema_info['sequences'])) {
  1384. $sql = "SELECT
  1385. TABLE_NAME,
  1386. TRIGGER_BODY
  1387. FROM
  1388. USER_TRIGGERS
  1389. WHERE
  1390. TRIGGERING_EVENT = 'INSERT' AND
  1391. STATUS = 'ENABLED' AND
  1392. TRIGGER_NAME NOT LIKE 'BIN\$%'";
  1393. $this->schema_info['sequences'] = array();
  1394. foreach ($this->query($sql) as $row) {
  1395. if (preg_match('#SELECT\s+(\w+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) {
  1396. $this->schema_info['sequences'][strtolower($row['table_name'])] = array('sequence' => $matches[1], 'column' => $matches[2]);
  1397. }
  1398. }
  1399. if ($this->cache) {
  1400. $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);
  1401. }
  1402. }
  1403. if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($table, '#') . '\s+\([^\)]*?\b' . preg_quote($this->schema_info['sequences'][$table]['column'], '#') . '\b#i', $result->getSQL())) {
  1404. return;
  1405. }
  1406. $insert_id_sql = "SELECT " . $this->schema_info['sequences'][$table]['sequence'] . ".currval AS INSERT_ID FROM dual";
  1407. }
  1408. if ($this->type == 'postgresql') {
  1409. if (!isset($this->schema_info['sequences'])) {
  1410. $sql = "SELECT
  1411. pg_class.relname AS table_name,
  1412. pg_attribute.attname AS column
  1413. FROM
  1414. pg_attribute INNER JOIN
  1415. pg_class ON pg_attribute.attrelid = pg_class.oid INNER JOIN
  1416. pg_attrdef ON pg_class.oid = pg_attrdef.adrelid AND pg_attribute.attnum = pg_attrdef.adnum
  1417. WHERE
  1418. NOT pg_attribute.attisdropped AND
  1419. pg_attrdef.adsrc LIKE 'nextval(%'";
  1420. $this->schema_info['sequences'] = array();
  1421. foreach ($this->query($sql) as $row) {
  1422. $this->schema_info['sequences'][strtolower($row['table_name'])] = $row['column'];
  1423. }
  1424. if ($this->cache) {
  1425. $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);
  1426. }
  1427. }
  1428. if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($table, '#') . '\s+\([^\)]*?\b' . preg_quote($this->schema_info['sequences'][$table], '#') . '\b#i', $result->getSQL())) {
  1429. return;
  1430. }
  1431. }
  1432. if ($this->extension == 'mssql') {
  1433. $insert_id_res = mssql_query("SELECT @@IDENTITY AS insert_id", $this->connection);
  1434. $insert_id = mssql_result($insert_id_res, 0, 'insert_id');
  1435. mssql_free_result($insert_id_res);
  1436. } elseif ($this->extension == 'mysql') {
  1437. $insert_id = mysql_insert_id($this->connection);
  1438. } elseif ($this->extension == 'mysqli') {
  1439. $insert_id = mysqli_insert_id($this->connection);
  1440. } elseif ($this->extension == 'oci8') {
  1441. $oci_statement = oci_parse($this->connection, $insert_id_sql);
  1442. oci_execute($oci_statement);
  1443. $insert_id_row = oci_fetch_array($oci_statement, OCI_ASSOC);
  1444. $insert_id = $insert_id_row['INSERT_ID'];
  1445. oci_free_statement($oci_statement);
  1446. } elseif ($this->extension == 'odbc' && $this->type == 'mssql') {
  1447. $insert_id_res = odbc_exec($this->connection, "SELECT @@IDENTITY AS insert_id");
  1448. $insert_id = odbc_result($insert_id_res, 'insert_id');
  1449. odbc_free_result($insert_id_res);
  1450. } elseif ($this->extension == 'odbc' && $this->type == 'oracle') {
  1451. $insert_id_res = odbc_exec($this->connection, $insert_id_sql);
  1452. $insert_id = odbc_result($insert_id_res, 'insert_id');
  1453. odbc_free_result($insert_id_res);
  1454. } elseif ($this->extension == 'pgsql') {
  1455. $insert_id_res = pg_query($this->connection, "SELECT lastval()");
  1456. $insert_id_row = pg_fetch_assoc($insert_id_res);
  1457. $insert_id = array_shift($insert_id_row);
  1458. pg_free_result($insert_id_res);
  1459. } elseif ($this->extension == 'sqlite') {
  1460. $insert_id = sqlite_last_insert_rowid($this->connection);
  1461. } elseif ($this->extension == 'sqlsrv') {
  1462. $insert_id_res = sqlsrv_query($this->connection, "SELECT @@IDENTITY AS insert_id");
  1463. $insert_id_row = sqlsrv_fetch_array($insert_id_res, SQLSRV_FETCH_ASSOC);
  1464. $insert_id = $insert_id_row['insert_id'];
  1465. sqlsrv_free_stmt($insert_id_res);
  1466. } elseif ($this->extension == 'pdo') {
  1467. switch ($this->type) {
  1468. case 'mssql':
  1469. try {
  1470. $insert_id_statement = $this->connection->query("SELECT @@IDENTITY AS insert_id");
  1471. if (!$insert_id_statement) {
  1472. throw new Exception();
  1473. }
  1474. $insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
  1475. $insert_id = array_shift($insert_id_row);
  1476. } catch (Exception $e) {
  1477. // If there was an error we don't have an insert id
  1478. }
  1479. break;
  1480. case 'oracle':
  1481. try {
  1482. $insert_id_statement = $this->connection->query($insert_id_sql);
  1483. if (!$insert_id_statement) {
  1484. throw new Exception();
  1485. }
  1486. $insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
  1487. $insert_id = array_shift($insert_id_row);
  1488. } catch (Exception $e) {
  1489. // If there was an error we don't have an insert id
  1490. }
  1491. break;
  1492. case 'postgresql':
  1493. $insert_id_statement = $this->connection->query("SELECT lastval()");
  1494. $insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
  1495. $insert_id = array_shift($insert_id_row);
  1496. $insert_id_statement->closeCursor();
  1497. unset($insert_id_statement);
  1498. break;
  1499. case 'mysql':
  1500. $insert_id = $this->connection->lastInsertId();
  1501. break;
  1502. case 'sqlite':
  1503. $insert_id = $this->connection->lastInsertId();
  1504. break;
  1505. }
  1506. }
  1507. $result->setAutoIncrementedValue($insert_id);
  1508. }
  1509. /**
  1510. * Makes sure each database and extension handles BEGIN, COMMIT and ROLLBACK
  1511. *
  1512. * @param string &$sql The SQL to check for a transaction query
  1513. * @param string $result_class The type of result object to create
  1514. * @return mixed `FALSE` if normal processing should continue, otherwise an object of the type $result_class
  1515. */
  1516. private function handleTransactionQueries(&$sql, $result_class)
  1517. {
  1518. // SQL Server supports transactions, but starts then with BEGIN TRANSACTION
  1519. if ($this->type == 'mssql' && preg_match('#^\s*(begin|start(\s+transaction)?)\s*#i', $sql)) {
  1520. $sql = 'BEGIN TRANSACTION';
  1521. }
  1522. $begin = FALSE;
  1523. $commit = FALSE;
  1524. $rollback = FALSE;
  1525. // Track transactions since most databases don't support nesting
  1526. if (preg_match('#^\s*(begin|start)(\s+(transaction|work))?\s*$#iD', $sql)) {
  1527. if ($this->inside_transaction) {
  1528. throw new fProgrammerException('A transaction is already in progress');
  1529. }
  1530. $this->inside_transaction = TRUE;
  1531. $begin = TRUE;
  1532. } elseif (preg_match('#^\s*(commit)(\s+(transaction|work))?\s*$#iD', $sql)) {
  1533. if (!$this->inside_transaction) {
  1534. throw new fProgrammerException('There is no transaction in progress');
  1535. }
  1536. $this->inside_transaction = FALSE;
  1537. $commit = TRUE;
  1538. } elseif (preg_match('#^\s*(rollback)(\s+(transaction|work))?\s*$#iD', $sql)) {
  1539. if (!$this->inside_transaction) {
  1540. throw new fProgrammerException('There is no transaction in progress');
  1541. }
  1542. $this->inside_transaction = FALSE;
  1543. $rollback = TRUE;
  1544. }
  1545. if (!$begin && !$commit && !$rollback) {
  1546. return FALSE;
  1547. }
  1548. // The PDO, OCI8, ODBC and SQLSRV extensions require special handling through methods and functions
  1549. $is_pdo = $this->extension == 'pdo';
  1550. $is_oci = $this->extension == 'oci8';
  1551. $is_odbc = $this->extension == 'odbc';
  1552. $is_sqlsrv = $this->extension == 'sqlsrv';
  1553. if (!$is_pdo && !$is_oci && !$is_odbc && !$is_sqlsrv) {
  1554. return FALSE;
  1555. }
  1556. // PDO seems to act weird if you try to start transactions through a normal query call
  1557. if ($is_pdo) {
  1558. try {
  1559. $is_mssql = $this->type == 'mssql' && substr($this->database, 0, 4) != 'dsn:';
  1560. $is_oracle = $this->type == 'oracle' && substr($this->database, 0, 4) != 'dsn:';
  1561. if ($begin) {
  1562. // The SQL Server PDO object hasn't implemented transactions
  1563. if ($is_mssql) {
  1564. $this->connection->exec('BEGIN TRANSACTION');
  1565. } elseif ($is_oracle) {
  1566. $this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE);
  1567. } else {
  1568. $this->connection->beginTransaction();
  1569. }
  1570. } elseif ($commit) {
  1571. if ($is_mssql) {
  1572. $this->connection->exec('COMMIT');
  1573. } elseif ($is_oracle) {
  1574. $this->connection->exec('COMMIT');
  1575. $this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
  1576. } else {
  1577. $this->connection->commit();
  1578. }
  1579. } elseif ($rollback) {
  1580. if ($is_mssql) {
  1581. $this->connection->exec('ROLLBACK');
  1582. } elseif ($is_oracle) {
  1583. $this->connection->exec('ROLLBACK');
  1584. $this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
  1585. } else {
  1586. $this->connection->rollBack();
  1587. }
  1588. }
  1589. } catch (Exception $e) {
  1590. $db_type_map = array(
  1591. 'mssql' => 'MSSQL',
  1592. 'mysql' => 'MySQL',
  1593. 'oracle' => 'Oracle',
  1594. 'postgresql' => 'PostgreSQL',
  1595. 'sqlite' => 'SQLite'
  1596. );
  1597. throw new fSQLException(
  1598. '%1$s error (%2$s) in %3$s',
  1599. $db_type_map[$this->type],
  1600. $e->getMessage(),
  1601. $sql
  1602. );
  1603. }
  1604. } elseif ($is_oci) {
  1605. if ($commit) {
  1606. oci_commit($this->connection);
  1607. } elseif ($rollback) {
  1608. oci_rollback($this->connection);
  1609. }
  1610. } elseif ($is_odbc) {
  1611. if ($begin) {
  1612. odbc_autocommit($this->connection, FALSE);
  1613. } elseif ($commit) {
  1614. odbc_commit($this->connection);
  1615. odbc_autocommit($this->connection, TRUE);
  1616. } elseif ($rollback) {
  1617. odbc_rollback($this->connection);
  1618. odbc_autocommit($this->connection, TRUE);
  1619. }
  1620. } elseif ($is_sqlsrv) {
  1621. if ($begin) {
  1622. sqlsrv_begin_transaction($this->connection);
  1623. } elseif ($commit) {
  1624. sqlsrv_commit($this->connection);
  1625. } elseif ($rollback) {
  1626. sqlsrv_rollback($this->connection);
  1627. }
  1628. }
  1629. $result = new $result_class($this);
  1630. $result->setSQL($sql);
  1631. $result->setResult(TRUE);
  1632. return $result;
  1633. }
  1634. /**
  1635. * Injects an fSQLTranslation object to handle translation
  1636. *
  1637. * @internal
  1638. *
  1639. * @param fSQLTranslation $sql_translation The SQL translation object
  1640. * @return void
  1641. */
  1642. public function inject($sql_translation)
  1643. {
  1644. $this->translation = $sql_translation;
  1645. }
  1646. /**
  1647. * Will indicate if a transaction is currently in progress
  1648. *
  1649. * @return boolean If a transaction has been started and not yet rolled back or committed
  1650. */
  1651. public function isInsideTransaction()
  1652. {
  1653. return $this->inside_transaction;
  1654. }
  1655. /**
  1656. * Creates a unique cache prefix to help prevent cache conflicts
  1657. *
  1658. * @return void
  1659. */
  1660. private function makeCachePrefix()
  1661. {
  1662. $prefix = 'fDatabase::' . $this->type . '::';
  1663. if ($this->host) {
  1664. $prefix .= $this->host . '::';
  1665. }
  1666. if ($this->port) {
  1667. $prefix .= $this->port . '::';
  1668. }
  1669. $prefix .= $this->database . '::';
  1670. if ($this->username) {
  1671. $prefix .= $this->username . '::';
  1672. }
  1673. return $prefix;
  1674. }
  1675. /**
  1676. * Prepares the SQL by escaping values, spliting queries, cleaning escaped semicolons, fixing backslashed single quotes and translating
  1677. *
  1678. * @param string $sql The SQL to prepare
  1679. * @param array $values Literal values to escape into the SQL
  1680. * @param boolean $translate If the SQL should be translated
  1681. * @return array The split out SQL queries, queries that have been translated will have a string key of the original SQL, non-translated SQL will have a numeric key
  1682. */
  1683. private function prepareSQL($sql, $values, $translate)
  1684. {
  1685. $this->connectToDatabase();
  1686. // Ensure an SQL statement was passed
  1687. if (empty($sql)) {
  1688. throw new fProgrammerException('No SQL statement passed');
  1689. }
  1690. if ($values) {
  1691. $sql = call_user_func_array(
  1692. $this->escape,
  1693. array_merge(array($sql), $values)
  1694. );
  1695. }
  1696. // Fix \' in MySQL and PostgreSQL
  1697. if(($this->type == 'mysql' || $this->type == 'postgresql') && strpos($sql, '\\') !== FALSE) {
  1698. $sql = preg_replace("#(?<!\\\\)((\\\\{2})*)\\\\'#", "\\1''", $sql);
  1699. }
  1700. $strings = array(array());
  1701. $queries = array('');
  1702. $number = 0;
  1703. // Separate the SQL from quoted values
  1704. preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql, $matches);
  1705. foreach ($matches[0] as $match) {
  1706. if ($match[0] == "'") {
  1707. $queries[$number] .= ':string_' . sizeof($strings[$number]);
  1708. $strings[$number][] = $match;
  1709. } else {
  1710. $split_queries = preg_split('#(?<!\\\\);#', $match);
  1711. $queries[$number] .= $split_queries[0];
  1712. for ($i=1; $i < sizeof($split_queries); $i++) {
  1713. $queries[$number] = trim($queries[$number]);
  1714. $number++;
  1715. $strings[$number] = array();
  1716. $queries[$number] = $split_queries[$i];
  1717. }
  1718. }
  1719. }
  1720. if (!trim($queries[$number])) {
  1721. unset($queries[$number]);
  1722. unset($strings[$number]);
  1723. } else {
  1724. $queries[$number] = trim($queries[$number]);
  1725. }
  1726. // Translate the SQL queries, this takes care of unescaping and reinserting strings
  1727. if ($translate) {
  1728. $output = $this->getSQLTranslation()->translate($queries, $strings);
  1729. // For untranslated queries we need to unescape and reinsert strings
  1730. } else {
  1731. $output = array();
  1732. foreach ($queries as $number => $query) {
  1733. // Unescape literal semicolons in the queries
  1734. $query = preg_replace('#(?<!\\\\)\\\\;#', ';', $query);
  1735. // Put the strings back into the SQL
  1736. foreach ($strings[$number] as $index => $string) {
  1737. $string = strtr($string, array('\\' => '\\\\', '$' => '\\$'));
  1738. $query = preg_replace('#:string_' . $index . '\b#', $string, $query, 1);
  1739. }
  1740. $output[] = $query;
  1741. }
  1742. }
  1743. return $output;
  1744. }
  1745. /**
  1746. * Executes one or more SQL queries
  1747. *
  1748. * @param string $sql One or more SQL statements
  1749. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  1750. * @param mixed ...
  1751. * @return fResult|array The fResult object(s) for the query
  1752. */
  1753. public function query($sql)
  1754. {
  1755. $args = func_get_args();
  1756. $queries = $this->prepareSQL(
  1757. $sql,
  1758. array_slice($args, 1),
  1759. FALSE
  1760. );
  1761. $output = array();
  1762. foreach ($queries as $query) {
  1763. $output[] = $this->runQuery($query, 'fResult');
  1764. }
  1765. return sizeof($output) == 1 ? $output[0] : $output;
  1766. }
  1767. /**
  1768. * Runs a single query and times it, removes any old unbuffered queries before starting
  1769. *
  1770. * @param string $sql The SQL statement to execute
  1771. * @param string $result_type The type of result object to return, fResult or fUnbufferedResult
  1772. * @return fResult|fUnbufferedResult The result for the query
  1773. */
  1774. private function runQuery($sql, $result_type)
  1775. {
  1776. if ($this->unbuffered_result) {
  1777. $this->unbuffered_result->__destruct();
  1778. $this->unbuffered_result = NULL;
  1779. }
  1780. $start_time = microtime(TRUE);
  1781. if (!$result = $this->handleTransactionQueries($sql, $result_type)) {
  1782. $result = new $result_type($this, $this->type == 'mssql' ? $this->schema_info['character_set'] : NULL);
  1783. $result->setSQL($sql);
  1784. if ($result_type == 'fResult') {
  1785. $this->executeQuery($result);
  1786. } else {
  1787. $this->executeUnbufferedQuery($result);
  1788. }
  1789. }
  1790. // Write some debugging info
  1791. $query_time = microtime(TRUE) - $start_time;
  1792. $this->query_time += $query_time;
  1793. fCore::debug(
  1794. self::compose(
  1795. 'Query time was %1$s seconds for:%2$s',
  1796. $query_time,
  1797. "\n" . $result->getSQL()
  1798. ),
  1799. $this->debug
  1800. );
  1801. if ($this->slow_query_threshold && $query_time > $this->slow_query_threshold) {
  1802. trigger_error(
  1803. self::compose(
  1804. 'The following query took %1$s milliseconds, which is above the slow query threshold of %2$s:%3$s',
  1805. $query_time,
  1806. $this->slow_query_threshold,
  1807. "\n" . $result->getSQL()
  1808. ),
  1809. E_USER_WARNING
  1810. );
  1811. }
  1812. return $result;
  1813. }
  1814. /**
  1815. * Turns an array possibly containing objects into an array of all strings
  1816. *
  1817. * @param array $values The array of values to scalarize
  1818. * @return array The scalarized values
  1819. */
  1820. private function scalarize($values)
  1821. {
  1822. $new_values = array();
  1823. foreach ($values as $value) {
  1824. if (is_object($value) && is_callable(array($value, '__toString'))) {
  1825. $value = $value->__toString();
  1826. } elseif (is_object($value)) {
  1827. $value = (string) $value;
  1828. } elseif (is_array($value)) {
  1829. $value = $this->scalarize($value);
  1830. }
  1831. $new_values[] = $value;
  1832. }
  1833. return $new_values;
  1834. }
  1835. /**
  1836. * Sets the number of rows affected by the query
  1837. *
  1838. * @param fResult $result The result object for the query
  1839. * @param mixed $resource Only applicable for `pdo`, `oci8`, `odbc` and `sqlsrv` extentions, this is either the `PDOStatement` object or the `oci8`, `odbc` or `sqlsrv` resource
  1840. * @return void
  1841. */
  1842. private function setAffectedRows($result, $resource=NULL)
  1843. {
  1844. if ($this->extension == 'mssql') {
  1845. $affected_rows_result = mssql_query('SELECT @@ROWCOUNT AS rows', $this->connection);
  1846. $result->setAffectedRows((int) mssql_result($affected_rows_result, 0, 'rows'));
  1847. } elseif ($this->extension == 'mysql') {
  1848. $result->setAffectedRows(mysql_affected_rows($this->connection));
  1849. } elseif ($this->extension == 'mysqli') {
  1850. $result->setAffectedRows(mysqli_affected_rows($this->connection));
  1851. } elseif ($this->extension == 'oci8') {
  1852. $result->setAffectedRows(oci_num_rows($resource));
  1853. } elseif ($this->extension == 'odbc') {
  1854. $result->setAffectedRows(odbc_num_rows($resource));
  1855. } elseif ($this->extension == 'pgsql') {
  1856. $result->setAffectedRows(pg_affected_rows($result->getResult()));
  1857. } elseif ($this->extension == 'sqlite') {
  1858. $result->setAffectedRows(sqlite_changes($this->connection));
  1859. } elseif ($this->extension == 'sqlsrv') {
  1860. $result->setAffectedRows(sqlsrv_rows_affected($resource));
  1861. } elseif ($this->extension == 'pdo') {
  1862. // This fixes the fact that rowCount is not reset for non INSERT/UPDATE/DELETE statements
  1863. try {
  1864. if (!$resource || !$resource->fetch()) {
  1865. throw new PDOException();
  1866. }
  1867. $result->setAffectedRows(0);
  1868. } catch (PDOException $e) {
  1869. // The SQLite PDO driver seems to return 1 when no rows are returned from a SELECT statement
  1870. if ($this->type == 'sqlite' && $this->extension == 'pdo' && preg_match('#^\s*SELECT#i', $result->getSQL())) {
  1871. $result->setAffectedRows(0);
  1872. } elseif (!$resource) {
  1873. $result->setAffectedRows(0);
  1874. } else {
  1875. $result->setAffectedRows($resource->rowCount());
  1876. }
  1877. }
  1878. }
  1879. }
  1880. /**
  1881. * Sets the number of rows returned by the query
  1882. *
  1883. * @param fResult $result The result object for the query
  1884. * @return void
  1885. */
  1886. private function setReturnedRows($result)
  1887. {
  1888. if (is_resource($result->getResult()) || is_object($result->getResult())) {
  1889. if ($this->extension == 'mssql') {
  1890. $result->setReturnedRows(mssql_num_rows($result->getResult()));
  1891. } elseif ($this->extension == 'mysql') {
  1892. $result->setReturnedRows(mysql_num_rows($result->getResult()));
  1893. } elseif ($this->extension == 'mysqli') {
  1894. $result->setReturnedRows(mysqli_num_rows($result->getResult()));
  1895. } elseif ($this->extension == 'pgsql') {
  1896. $result->setReturnedRows(pg_num_rows($result->getResult()));
  1897. } elseif ($this->extension == 'sqlite') {
  1898. $result->setReturnedRows(sqlite_num_rows($result->getResult()));
  1899. }
  1900. } elseif (is_array($result->getResult())) {
  1901. $result->setReturnedRows(sizeof($result->getResult()));
  1902. }
  1903. }
  1904. /**
  1905. * Translates the SQL statement using fSQLTranslation and executes it
  1906. *
  1907. * @param string $sql One or more SQL statements
  1908. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  1909. * @param mixed ...
  1910. * @return fResult|array The fResult object(s) for the query
  1911. */
  1912. public function translatedQuery($sql)
  1913. {
  1914. $args = func_get_args();
  1915. $queries = $this->prepareSQL(
  1916. $sql,
  1917. array_slice($args, 1),
  1918. TRUE
  1919. );
  1920. $output = array();
  1921. foreach ($queries as $original_query => $query) {
  1922. $result = $this->runQuery($query, 'fResult');
  1923. if (!is_numeric($original_query)) {
  1924. $result->setUntranslatedSQL($original_query);
  1925. }
  1926. $output[] = $result;
  1927. }
  1928. return sizeof($output) == 1 ? $output[0] : $output;
  1929. }
  1930. /**
  1931. * Executes a single SQL statement in unbuffered mode. This is optimal for
  1932. * large results sets since it does not load the whole result set into
  1933. * memory first. The gotcha is that only one unbuffered result can exist at
  1934. * one time. If another unbuffered query is executed, the old result will
  1935. * be deleted.
  1936. *
  1937. * @param string $sql A single SQL statement
  1938. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  1939. * @param mixed ...
  1940. * @return fUnbufferedResult The result object for the unbuffered query
  1941. */
  1942. public function unbufferedQuery($sql)
  1943. {
  1944. $args = func_get_args();
  1945. $queries = $this->prepareSQL(
  1946. $sql,
  1947. array_slice($args, 1),
  1948. FALSE
  1949. );
  1950. if (sizeof($queries) > 1) {
  1951. throw new fProgrammerException(
  1952. 'Only a single unbuffered query can be run at a time, however %d were passed',
  1953. sizeof($queries)
  1954. );
  1955. }
  1956. $result = $this->runQuery($queries[0], 'fUnbufferedResult');
  1957. $this->unbuffered_result = $result;
  1958. return $result;
  1959. }
  1960. /**
  1961. * Translates the SQL statement using fSQLTranslation and then executes it
  1962. * in unbuffered mode. This is optimal for large results sets since it does
  1963. * not load the whole result set into memory first. The gotcha is that only
  1964. * one unbuffered result can exist at one time. If another unbuffered query
  1965. * is executed, the old result will be deleted.
  1966. *
  1967. * @param string $sql A single SQL statement
  1968. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  1969. * @param mixed ...
  1970. * @return fUnbufferedResult The result object for the unbuffered query
  1971. */
  1972. public function unbufferedTranslatedQuery($sql)
  1973. {
  1974. $args = func_get_args();
  1975. $queries = $this->prepareSQL(
  1976. $sql,
  1977. array_slice($args, 1),
  1978. TRUE
  1979. );
  1980. if (sizeof($queries) > 1) {
  1981. throw new fProgrammerException(
  1982. 'Only a single unbuffered query can be run at a time, however %d were passed',
  1983. sizeof($queries)
  1984. );
  1985. }
  1986. $query_keys = array_keys($queries);
  1987. $original_query = $query_keys[0];
  1988. $result = $this->runQuery($queries[$original_query], 'fUnbufferedResult');
  1989. $result->setUntranslatedSQL($original_query);
  1990. $this->unbuffered_result = $result;
  1991. return $result;
  1992. }
  1993. /**
  1994. * Unescapes a value coming out of a database based on its data type
  1995. *
  1996. * The valid data types are:
  1997. *
  1998. * - `'blob'` (or `'%l'`)
  1999. * - `'boolean'` (or `'%b'`)
  2000. * - `'date'` (or `'%d'`)
  2001. * - `'float'` (or `'%f'`)
  2002. * - `'integer'` (or `'%i'`)
  2003. * - `'string'` (also `'%s'`, `'varchar'`, `'char'` or `'text'`)
  2004. * - `'time'` (or `'%t'`)
  2005. * - `'timestamp'` (or `'%p'`)
  2006. *
  2007. * @param string $data_type The data type being unescaped - see method description for valid values
  2008. * @param mixed $value The value or array of values to unescape
  2009. * @return mixed The unescaped value
  2010. */
  2011. public function unescape($data_type, $value)
  2012. {
  2013. if ($value === NULL) {
  2014. return $value;
  2015. }
  2016. $callback = NULL;
  2017. switch ($data_type) {
  2018. // Testing showed that strings tend to be most common,
  2019. // and moving this to the top of the switch statement
  2020. // improved performance on read-heavy pages
  2021. case 'string':
  2022. case 'varchar':
  2023. case 'char':
  2024. case 'text':
  2025. case '%s':
  2026. return $value;
  2027. case 'boolean':
  2028. case '%b':
  2029. $callback = $this->unescapeBoolean;
  2030. break;
  2031. case 'date':
  2032. case '%d':
  2033. $callback = $this->unescapeDate;
  2034. break;
  2035. case 'float':
  2036. case '%f':
  2037. return $value;
  2038. case 'integer':
  2039. case '%i':
  2040. return $value;
  2041. case 'time':
  2042. case '%t':
  2043. $callback = $this->unescapeTime;
  2044. break;
  2045. case 'timestamp':
  2046. case '%p':
  2047. $callback = $this->unescapeTimestamp;
  2048. break;
  2049. case 'blob':
  2050. case '%l':
  2051. $callback = $this->unescapeBlob;
  2052. break;
  2053. }
  2054. if ($callback) {
  2055. if (is_array($value)) {
  2056. return array_map($callback, $value);
  2057. }
  2058. return call_user_func($callback, $value);
  2059. }
  2060. throw new fProgrammerException(
  2061. 'Unknown data type, %1$s, specified. Must be one of: %2$s.',
  2062. $data_type,
  2063. 'blob, %l, boolean, %b, date, %d, float, %f, integer, %i, string, %s, time, %t, timestamp, %p'
  2064. );
  2065. }
  2066. /**
  2067. * Unescapes a blob coming out of the database
  2068. *
  2069. * @param string $value The value to unescape
  2070. * @return binary The binary data
  2071. */
  2072. private function unescapeBlob($value)
  2073. {
  2074. $this->connectToDatabase();
  2075. if ($this->extension == 'pgsql') {
  2076. return pg_unescape_bytea($value);
  2077. } elseif ($this->extension == 'pdo' && is_resource($value)) {
  2078. return stream_get_contents($value);
  2079. } elseif ($this->type == 'mssql' && (substr($this->database, 0, 4) == 'dsn:')) {
  2080. return pack('H*', $value);
  2081. } elseif ($this->extension == 'sqlite') {
  2082. return pack('H*', $value);
  2083. } else {
  2084. return $value;
  2085. }
  2086. }
  2087. /**
  2088. * Unescapes a boolean coming out of the database
  2089. *
  2090. * @param string $value The value to unescape
  2091. * @return boolean The boolean
  2092. */
  2093. private function unescapeBoolean($value)
  2094. {
  2095. return ($value === 'f' || !$value) ? FALSE : TRUE;
  2096. }
  2097. /**
  2098. * Unescapes a date coming out of the database
  2099. *
  2100. * @param string $value The value to unescape
  2101. * @return string The date in YYYY-MM-DD format
  2102. */
  2103. private function unescapeDate($value)
  2104. {
  2105. if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
  2106. return $value->format('Y-m-d');
  2107. } elseif ($this->type == 'mssql') {
  2108. $value = preg_replace('#:\d{3}#', '', $value);
  2109. }
  2110. return date('Y-m-d', strtotime($value));
  2111. }
  2112. /**
  2113. * Unescapes a time coming out of the database
  2114. *
  2115. * @param string $value The value to unescape
  2116. * @return string The time in `HH:MM:SS` format
  2117. */
  2118. private function unescapeTime($value)
  2119. {
  2120. if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
  2121. return $value->format('H:i:s');
  2122. } elseif ($this->type == 'mssql') {
  2123. $value = preg_replace('#:\d{3}#', '', $value);
  2124. }
  2125. return date('H:i:s', strtotime($value));
  2126. }
  2127. /**
  2128. * Unescapes a timestamp coming out of the database
  2129. *
  2130. * @param string $value The value to unescape
  2131. * @return string The timestamp in `YYYY-MM-DD HH:MM:SS` format
  2132. */
  2133. private function unescapeTimestamp($value)
  2134. {
  2135. if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
  2136. return $value->format('Y-m-d H:i:s');
  2137. } elseif ($this->type == 'mssql') {
  2138. $value = preg_replace('#:\d{3}#', '', $value);
  2139. }
  2140. return date('Y-m-d H:i:s', strtotime($value));
  2141. }
  2142. }
  2143. /**
  2144. * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
  2145. *
  2146. * Permission is hereby granted, free of charge, to any person obtaining a copy
  2147. * of this software and associated documentation files (the "Software"), to deal
  2148. * in the Software without restriction, including without limitation the rights
  2149. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  2150. * copies of the Software, and to permit persons to whom the Software is
  2151. * furnished to do so, subject to the following conditions:
  2152. *
  2153. * The above copyright notice and this permission notice shall be included in
  2154. * all copies or substantial portions of the Software.
  2155. *
  2156. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  2157. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  2158. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  2159. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  2160. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  2161. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  2162. * THE SOFTWARE.
  2163. */