PageRenderTime 58ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/classes/fDatabase.php

https://bitbucket.org/dsqmoore/flourish
PHP | 3624 lines | 2274 code | 487 blank | 863 comment | 586 complexity | 613c04e1a5b7df0b3bb7b4be610fa0b4 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Provides a common API for different databases - will automatically use any installed extension
  4. *
  5. * This class is implemented to use the UTF-8 character encoding. Please see
  6. * http://flourishlib.com/docs/UTF-8 for more information.
  7. *
  8. * The following databases are supported:
  9. *
  10. * - [http://ibm.com/db2 DB2]
  11. * - [http://microsoft.com/sql/ MSSQL]
  12. * - [http://mysql.com MySQL]
  13. * - [http://oracle.com Oracle]
  14. * - [http://postgresql.org PostgreSQL]
  15. * - [http://sqlite.org SQLite]
  16. *
  17. * The class will automatically use the first of the following extensions it finds:
  18. *
  19. * - DB2
  20. * - [http://php.net/ibm_db2 ibm_db2]
  21. * - [http://php.net/pdo_ibm pdo_ibm]
  22. * - MSSQL
  23. * - [http://msdn.microsoft.com/en-us/library/cc296221.aspx sqlsrv]
  24. * - [http://php.net/pdo_dblib pdo_dblib]
  25. * - [http://php.net/mssql mssql] (or [http://php.net/sybase sybase])
  26. * - MySQL
  27. * - [http://php.net/mysql mysql]
  28. * - [http://php.net/mysqli mysqli]
  29. * - [http://php.net/pdo_mysql pdo_mysql]
  30. * - Oracle
  31. * - [http://php.net/oci8 oci8]
  32. * - [http://php.net/pdo_oci pdo_oci]
  33. * - PostgreSQL
  34. * - [http://php.net/pgsql pgsql]
  35. * - [http://php.net/pdo_pgsql pdo_pgsql]
  36. * - SQLite
  37. * - [http://php.net/pdo_sqlite pdo_sqlite] (for v3.x)
  38. * - [http://php.net/sqlite sqlite] (for v2.x)
  39. *
  40. * The `odbc` and `pdo_odbc` extensions are not supported due to character
  41. * encoding and stability issues on Windows, and functionality on non-Windows
  42. * operating systems.
  43. *
  44. * @copyright Copyright (c) 2007-2011 Will Bond
  45. * @author Will Bond [wb] <will@flourishlib.com>
  46. * @license http://flourishlib.com/license
  47. *
  48. * @package Flourish
  49. * @link http://flourishlib.com/fDatabase
  50. *
  51. * @version 1.0.0b40
  52. * @changes 1.0.0b40 Fixed a bug with notices being triggered when failing to connect to a SQLite database [wb, 2011-06-20]
  53. * @changes 1.0.0b39 Fixed a bug with detecting some MySQL database version numbers [wb, 2011-05-24]
  54. * @changes 1.0.0b38 Backwards Compatibility Break - callbacks registered to the `extracted` hook via ::registerHookCallback() no longer receive the `$strings` parameter, instead all strings are added into the `$values` parameter - added ::getVersion(), fixed a bug with SQLite messaging, fixed a bug with ::__destruct(), improved handling of transactional queries, added ::close(), enhanced class to throw four different exceptions for different connection errors, silenced PHP warnings upon connection error [wb, 2011-05-09]
  55. * @changes 1.0.0b37 Fixed usage of the mysqli extension to only call mysqli_set_charset() if it exists [wb, 2011-03-04]
  56. * @changes 1.0.0b36 Updated ::escape() and methods that use ::escape() to handle float values that don't contain a digit before or after the . [wb, 2011-02-01]
  57. * @changes 1.0.0b35 Updated the class to replace `LIMIT` and `OFFSET` value placeholders in the SQL with their values before translating since most databases that translate `LIMIT` statements need to move or add values together [wb, 2011-01-11]
  58. * @changes 1.0.0b34 Fixed a bug with creating translated prepared statements [wb, 2011-01-09]
  59. * @changes 1.0.0b33 Added code to explicitly set the connection encoding for the mysql and mysqli extensions since some PHP installs don't see to fully respect `SET NAMES` [wb, 2010-12-06]
  60. * @changes 1.0.0b32 Fixed handling auto-incrementing values for Oracle when the trigger was on `INSERT OR UPDATE` instead of just `INSERT` [wb, 2010-12-04]
  61. * @changes 1.0.0b31 Fixed handling auto-incrementing values for MySQL when the `INTO` keyword is left out of an `INSERT` statement [wb, 2010-11-04]
  62. * @changes 1.0.0b30 Fixed the pgsql, mssql and mysql extensions to force a new connection instead of reusing an existing one [wb, 2010-08-17]
  63. * @changes 1.0.0b29 Backwards Compatibility Break - removed ::enableSlowQueryWarnings(), added ability to replicate via ::registerHookCallback() [wb, 2010-08-10]
  64. * @changes 1.0.0b28 Backwards Compatibility Break - removed ODBC support. Added support for the `pdo_ibm` extension. [wb, 2010-07-31]
  65. * @changes 1.0.0b27 Fixed a bug with running multiple copies of a SQL statement with string values through a single ::translatedQuery() call [wb, 2010-07-14]
  66. * @changes 1.0.0b26 Updated the class to use new fCore functionality [wb, 2010-07-05]
  67. * @changes 1.0.0b25 Added IBM DB2 support [wb, 2010-04-13]
  68. * @changes 1.0.0b24 Fixed an auto-incrementing transaction bug with Oracle and debugging issues with all databases [wb, 2010-03-17]
  69. * @changes 1.0.0b23 Resolved another bug with capturing auto-incrementing values for PostgreSQL and Oracle [wb, 2010-03-15]
  70. * @changes 1.0.0b22 Changed ::clearCache() to also clear the cache on the fSQLTranslation [wb, 2010-03-09]
  71. * @changes 1.0.0b21 Added ::execute() for result-less SQL queries, ::prepare() and ::translatedPrepare() to create fStatement objects for prepared statements, support for prepared statements in ::query() and ::unbufferedQuery(), fixed default caching key for ::enableCaching() [wb, 2010-03-02]
  72. * @changes 1.0.0b20 Added a parameter to ::enableCaching() to provide a key token that will allow cached values to be shared between multiple databases with the same schema [wb, 2009-10-28]
  73. * @changes 1.0.0b19 Added support for escaping identifiers (column and table names) to ::escape(), added support for database schemas, rewrote internal SQL string spliting [wb, 2009-10-22]
  74. * @changes 1.0.0b18 Updated the class for the new fResult and fUnbufferedResult APIs, fixed ::unescape() to not touch NULLs [wb, 2009-08-12]
  75. * @changes 1.0.0b17 Added the ability to pass an array of all values as a single parameter to ::escape() instead of one value per parameter [wb, 2009-08-11]
  76. * @changes 1.0.0b16 Fixed PostgreSQL and Oracle from trying to get auto-incrementing values on inserts when explicit values were given [wb, 2009-08-06]
  77. * @changes 1.0.0b15 Fixed a bug where auto-incremented values would not be detected when table names were quoted [wb, 2009-07-15]
  78. * @changes 1.0.0b14 Changed ::determineExtension() and ::determineCharacterSet() to be protected instead of private [wb, 2009-07-08]
  79. * @changes 1.0.0b13 Updated ::escape() to accept arrays of values for insertion into full SQL strings [wb, 2009-07-06]
  80. * @changes 1.0.0b12 Updates to ::unescape() to improve performance [wb, 2009-06-15]
  81. * @changes 1.0.0b11 Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
  82. * @changes 1.0.0b10 Changed date/time/timestamp escaping from `strtotime()` to fDate/fTime/fTimestamp for better localization support [wb, 2009-06-01]
  83. * @changes 1.0.0b9 Fixed a bug with ::escape() where floats that start with a . were encoded as `NULL` [wb, 2009-05-09]
  84. * @changes 1.0.0b8 Added Oracle support, change PostgreSQL code to no longer cause lastval() warnings, added support for arrays of values to ::escape() [wb, 2009-05-03]
  85. * @changes 1.0.0b7 Updated for new fCore API [wb, 2009-02-16]
  86. * @changes 1.0.0b6 Fixed a bug with executing transaction queries when using the mysqli extension [wb, 2009-02-12]
  87. * @changes 1.0.0b5 Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
  88. * @changes 1.0.0b4 Added a few error suppression operators back in so that developers don't get errors and exceptions [wb, 2009-01-14]
  89. * @changes 1.0.0b3 Removed some unnecessary error suppresion operators [wb, 2008-12-11]
  90. * @changes 1.0.0b2 Fixed a bug with PostgreSQL when using the PDO extension and executing an INSERT statement [wb, 2008-12-11]
  91. * @changes 1.0.0b The initial implementation [wb, 2007-09-25]
  92. */
  93. class fDatabase
  94. {
  95. /**
  96. * Composes text using fText if loaded
  97. *
  98. * @param string $message The message to compose
  99. * @param mixed $component A string or number to insert into the message
  100. * @param mixed ...
  101. * @return string The composed and possible translated message
  102. */
  103. static protected function compose($message)
  104. {
  105. $args = array_slice(func_get_args(), 1);
  106. if (class_exists('fText', FALSE)) {
  107. return call_user_func_array(
  108. array('fText', 'compose'),
  109. array($message, $args)
  110. );
  111. } else {
  112. return vsprintf($message, $args);
  113. }
  114. }
  115. /**
  116. * An fCache object to cache the schema info to
  117. *
  118. * @var fCache
  119. */
  120. private $cache;
  121. /**
  122. * The cache prefix to use for cache entries
  123. *
  124. * @var string
  125. */
  126. private $cache_prefix;
  127. /**
  128. * Database connection resource or PDO object
  129. *
  130. * @var mixed
  131. */
  132. private $connection;
  133. /**
  134. * The database name
  135. *
  136. * @var string
  137. */
  138. private $database;
  139. /**
  140. * If debugging is enabled
  141. *
  142. * @var boolean
  143. */
  144. private $debug;
  145. /**
  146. * A temporary error holder for the mssql extension
  147. *
  148. * @var string
  149. */
  150. private $error;
  151. /**
  152. * The extension to use for the database specified
  153. *
  154. * Options include:
  155. *
  156. * - `'ibm_db2'`
  157. * - `'mssql'`
  158. * - `'mysql'`
  159. * - `'mysqli'`
  160. * - `'oci8'`
  161. * - `'pgsql'`
  162. * - `'sqlite'`
  163. * - `'sqlsrv'`
  164. * - `'pdo'`
  165. *
  166. * @var string
  167. */
  168. protected $extension;
  169. /**
  170. * Hooks callbacks to be used for accessing and modifying queries
  171. *
  172. * This array will have the structure:
  173. *
  174. * {{{
  175. * array(
  176. * 'unmodified' => array({callbacks}),
  177. * 'extracted' => array({callbacks}),
  178. * 'run' => array({callbacks})
  179. * )
  180. * }}}
  181. *
  182. * @var array
  183. */
  184. private $hook_callbacks;
  185. /**
  186. * The host the database server is located on
  187. *
  188. * @var string
  189. */
  190. private $host;
  191. /**
  192. * If a transaction is in progress
  193. *
  194. * @var boolean
  195. */
  196. private $inside_transaction;
  197. /**
  198. * The password for the user specified
  199. *
  200. * @var string
  201. */
  202. private $password;
  203. /**
  204. * The port number for the host
  205. *
  206. * @var string
  207. */
  208. private $port;
  209. /**
  210. * The total number of seconds spent executing queries
  211. *
  212. * @var float
  213. */
  214. private $query_time;
  215. /**
  216. * A cache of database-specific code
  217. *
  218. * @var array
  219. */
  220. protected $schema_info;
  221. /**
  222. * The last executed fStatement object
  223. *
  224. * @var fStatement
  225. */
  226. private $statement;
  227. /**
  228. * The timeout for the database connection
  229. *
  230. * @var integer
  231. */
  232. private $timeout;
  233. /**
  234. * The fSQLTranslation object for this database
  235. *
  236. * @var object
  237. */
  238. private $translation;
  239. /**
  240. * The database type: `'db2'`, `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, or `'sqlite'`
  241. *
  242. * @var string
  243. */
  244. private $type;
  245. /**
  246. * The unbuffered query instance
  247. *
  248. * @var fUnbufferedResult
  249. */
  250. private $unbuffered_result;
  251. /**
  252. * The user to connect to the database as
  253. *
  254. * @var string
  255. */
  256. private $username;
  257. /**
  258. * Configures the connection to a database - connection is not made until the first query is executed
  259. *
  260. * Passing `NULL` to any parameter other than `$type` and `$database` will
  261. * cause the default value to be used.
  262. *
  263. * @param string $type The type of the database: `'db2'`, `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, `'sqlite'`
  264. * @param string $database Name of the database. If SQLite the path to the database file.
  265. * @param string $username Database username - not used for SQLite
  266. * @param string $password The password for the username specified - not used for SQLite
  267. * @param string $host Database server host or IP, defaults to localhost - not used for SQLite. MySQL socket connection can be made by entering `'sock:'` followed by the socket path. PostgreSQL socket connection can be made by passing just `'sock:'`.
  268. * @param integer $port The port to connect to, defaults to the standard port for the database type specified - not used for SQLite
  269. * @param integer $timeout The number of seconds to timeout after if a connection can not be made - not used for SQLite
  270. * @return fDatabase
  271. */
  272. public function __construct($type, $database, $username=NULL, $password=NULL, $host=NULL, $port=NULL, $timeout=NULL)
  273. {
  274. $valid_types = array('db2', 'mssql', 'mysql', 'oracle', 'postgresql', 'sqlite');
  275. if (!in_array($type, $valid_types)) {
  276. throw new fProgrammerException(
  277. 'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
  278. $type,
  279. join(', ', $valid_types)
  280. );
  281. }
  282. if (empty($database)) {
  283. throw new fProgrammerException('No database was specified');
  284. }
  285. if ($host === NULL) {
  286. $host = 'localhost';
  287. }
  288. $this->type = $type;
  289. $this->database = $database;
  290. $this->username = $username;
  291. $this->password = $password;
  292. $this->host = $host;
  293. $this->port = $port;
  294. $this->timeout = $timeout;
  295. $this->hook_callbacks = array(
  296. 'unmodified' => array(),
  297. 'extracted' => array(),
  298. 'run' => array()
  299. );
  300. $this->schema_info = array();
  301. $this->determineExtension();
  302. }
  303. /**
  304. * Closes the open database connection
  305. *
  306. * @internal
  307. *
  308. * @return void
  309. */
  310. public function __destruct()
  311. {
  312. if (!$this->connection) { return; }
  313. fCore::debug('Total query time: ' . $this->query_time . ' seconds', $this->debug);
  314. if ($this->extension == 'ibm_db2') {
  315. db2_close($this->connection);
  316. } elseif ($this->extension == 'mssql') {
  317. mssql_close($this->connection);
  318. } elseif ($this->extension == 'mysql') {
  319. mysql_close($this->connection);
  320. } elseif ($this->extension == 'mysqli') {
  321. // Before 5.2.0 the destructor order would cause mysqli to
  322. // close itself which would make this call trigger a warning
  323. if (fCore::checkVersion('5.2.0')) {
  324. mysqli_close($this->connection);
  325. }
  326. } elseif ($this->extension == 'oci8') {
  327. oci_close($this->connection);
  328. } elseif ($this->extension == 'pgsql') {
  329. pg_close($this->connection);
  330. } elseif ($this->extension == 'sqlite') {
  331. sqlite_close($this->connection);
  332. } elseif ($this->extension == 'sqlsrv') {
  333. sqlsrv_close($this->connection);
  334. } elseif ($this->extension == 'pdo') {
  335. // PDO objects close their own connections when destroyed
  336. }
  337. $this->connection = FALSE;
  338. }
  339. /**
  340. * All requests that hit this method should be requests for callbacks
  341. *
  342. * @internal
  343. *
  344. * @param string $method The method to create a callback for
  345. * @return callback The callback for the method requested
  346. */
  347. public function __get($method)
  348. {
  349. return array($this, $method);
  350. }
  351. /**
  352. * Checks to see if an SQL error occured
  353. *
  354. * @param fResult|fUnbufferedResult|boolean $result The result object for the query
  355. * @param mixed $extra_info The sqlite extension will pass a string error message, the oci8 extension will pass the statement resource
  356. * @param string $sql The SQL that was executed
  357. * @return void
  358. */
  359. private function checkForError($result, $extra_info=NULL, $sql=NULL)
  360. {
  361. if ($result === FALSE || $result->getResult() === FALSE) {
  362. if ($this->extension == 'ibm_db2') {
  363. if (is_resource($extra_info)) {
  364. $message = db2_stmt_errormsg($extra_info);
  365. } else {
  366. $message = db2_stmt_errormsg();
  367. }
  368. } elseif ($this->extension == 'mssql') {
  369. $message = $this->error;
  370. $this->error = '';
  371. } elseif ($this->extension == 'mysql') {
  372. $message = mysql_error($this->connection);
  373. } elseif ($this->extension == 'mysqli') {
  374. if (is_object($extra_info)) {
  375. $message = $extra_info->error;
  376. } else {
  377. $message = mysqli_error($this->connection);
  378. }
  379. } elseif ($this->extension == 'oci8') {
  380. $error_info = oci_error($extra_info ? $extra_info : $this->connection);
  381. $message = $error_info['message'];
  382. } elseif ($this->extension == 'pgsql') {
  383. $message = pg_last_error($this->connection);
  384. } elseif ($this->extension == 'sqlite') {
  385. if ($extra_info === NULL) {
  386. $message = sqlite_error_string(sqlite_last_error($this->connection));
  387. } else {
  388. $message = $extra_info;
  389. }
  390. } elseif ($this->extension == 'sqlsrv') {
  391. $error_info = sqlsrv_errors(SQLSRV_ERR_ALL);
  392. $message = $error_info[0]['message'];
  393. } elseif ($this->extension == 'pdo') {
  394. if ($extra_info instanceof PDOStatement) {
  395. $error_info = $extra_info->errorInfo();
  396. } else {
  397. $error_info = $this->connection->errorInfo();
  398. }
  399. if (empty($error_info[2])) {
  400. $error_info[2] = 'Unknown error - this usually indicates a bug in the PDO driver';
  401. }
  402. $message = $error_info[2];
  403. }
  404. $db_type_map = array(
  405. 'db2' => 'DB2',
  406. 'mssql' => 'MSSQL',
  407. 'mysql' => 'MySQL',
  408. 'oracle' => 'Oracle',
  409. 'postgresql' => 'PostgreSQL',
  410. 'sqlite' => 'SQLite'
  411. );
  412. throw new fSQLException(
  413. '%1$s error (%2$s) in %3$s',
  414. $db_type_map[$this->type],
  415. $message,
  416. is_object($result) ? $result->getSQL() : $sql
  417. );
  418. }
  419. }
  420. /**
  421. * Clears all of the schema info out of the object and, if set, the fCache object
  422. *
  423. * @return void
  424. */
  425. public function clearCache()
  426. {
  427. $this->schema_info = array();
  428. if ($this->cache) {
  429. $this->cache->delete($this->makeCachePrefix() . 'schema_info');
  430. }
  431. if ($this->type == 'mssql') {
  432. $this->determineCharacterSet();
  433. }
  434. if ($this->translation) {
  435. $this->translation->clearCache();
  436. }
  437. }
  438. /**
  439. * Closes the database connection
  440. *
  441. * @return void
  442. */
  443. public function close()
  444. {
  445. $this->__destruct();
  446. }
  447. /**
  448. * Connects to the database specified, if no connection exists
  449. *
  450. * This method is only intended to force a connection, all operations that
  451. * require a database connection will automatically call this method.
  452. *
  453. * @throws fAuthorizationException When the username and password are not accepted
  454. *
  455. * @return void
  456. */
  457. public function connect()
  458. {
  459. // Don't try to reconnect if we are already connected
  460. if ($this->connection) { return; }
  461. $connection_error = FALSE;
  462. $authentication_error = FALSE;
  463. $database_error = FALSE;
  464. $errors = NULL;
  465. // Establish a connection to the database
  466. if ($this->extension == 'pdo') {
  467. $username = $this->username;
  468. $password = $this->password;
  469. $options = array();
  470. if ($this->timeout !== NULL && $this->type != 'sqlite' && $this->type != 'mssql') {
  471. $options[PDO::ATTR_TIMEOUT] = $this->timeout;
  472. }
  473. if ($this->type == 'db2') {
  474. if ($this->host === NULL && $this->port === NULL) {
  475. $dsn = 'ibm:DSN:' . $this->database;
  476. } else {
  477. $dsn = 'ibm:DRIVER={IBM DB2 ODBC DRIVER};DATABASE=' . $this->database . ';HOSTNAME=' . $this->host . ';';
  478. $dsn .= 'PORT=' . ($this->port ? $this->port : 60000) . ';';
  479. $dsn .= 'PROTOCOL=TCPIP;UID=' . $username . ';PWD=' . $password . ';';
  480. if ($this->timeout !== NULL) {
  481. $dsn .= 'CONNECTTIMEOUT=' . $this->timeout . ';';
  482. }
  483. $username = NULL;
  484. $password = NULL;
  485. }
  486. } elseif ($this->type == 'mssql') {
  487. $separator = (fCore::checkOS('windows')) ? ',' : ':';
  488. $port = ($this->port) ? $separator . $this->port : '';
  489. $driver = (fCore::checkOs('windows')) ? 'mssql' : 'dblib';
  490. $dsn = $driver . ':host=' . $this->host . $port . ';dbname=' . $this->database;
  491. // This driver does not support timeouts so we fake it here
  492. if ($this->timeout !== NULL) {
  493. fCore::startErrorCapture();
  494. $resource = fsockopen($this->host, $this->port ? $this->port : 1433, $errno, $errstr, $this->timeout);
  495. $errors = fCore::stopErrorCapture();
  496. if ($resource !== FALSE) {
  497. fclose($resource);
  498. }
  499. }
  500. } elseif ($this->type == 'mysql') {
  501. if (substr($this->host, 0, 5) == 'sock:') {
  502. $dsn = 'mysql:unix_socket=' . substr($this->host, 5) . ';dbname=' . $this->database;
  503. } else {
  504. $port = ($this->port) ? ';port=' . $this->port : '';
  505. $dsn = 'mysql:host=' . $this->host . ';dbname=' . $this->database . $port;
  506. }
  507. } elseif ($this->type == 'oracle') {
  508. $port = ($this->port) ? ':' . $this->port : '';
  509. $dsn = 'oci:dbname=' . $this->host . $port . '/' . $this->database . ';charset=AL32UTF8';
  510. // This driver does not support timeouts so we fake it here
  511. if ($this->timeout !== NULL) {
  512. fCore::startErrorCapture();
  513. $resource = fsockopen($this->host, $this->port ? $this->port : 1521, $errno, $errstr, $this->timeout);
  514. $errors = fCore::stopErrorCapture();
  515. if ($resource !== FALSE) {
  516. fclose($resource);
  517. }
  518. }
  519. } elseif ($this->type == 'postgresql') {
  520. $dsn = 'pgsql:dbname=' . $this->database;
  521. if ($this->host && $this->host != 'sock:') {
  522. $dsn .= ' host=' . $this->host;
  523. }
  524. if ($this->port) {
  525. $dsn .= ' port=' . $this->port;
  526. }
  527. } elseif ($this->type == 'sqlite') {
  528. $dsn = 'sqlite:' . $this->database;
  529. }
  530. try {
  531. if ($errors) {
  532. $this->connection = FALSE;
  533. } else {
  534. $this->connection = new PDO($dsn, $username, $password, $options);
  535. if ($this->type == 'mysql') {
  536. $this->connection->setAttribute(PDO::MYSQL_ATTR_DIRECT_QUERY, 1);
  537. }
  538. }
  539. } catch (PDOException $e) {
  540. $this->connection = FALSE;
  541. $errors = $e->getMessage();
  542. }
  543. }
  544. if ($this->extension == 'sqlite') {
  545. $this->connection = sqlite_open($this->database);
  546. }
  547. if ($this->extension == 'ibm_db2') {
  548. $username = $this->username;
  549. $password = $this->password;
  550. if ($this->host === NULL && $this->port === NULL && $this->timeout === NULL) {
  551. $connection_string = $this->database;
  552. } else {
  553. $connection_string = 'DATABASE=' . $this->database . ';HOSTNAME=' . $this->host . ';';
  554. $connection_string .= 'PORT=' . ($this->port ? $this->port : 60000) . ';';
  555. $connection_string .= 'PROTOCOL=TCPIP;UID=' . $this->username . ';PWD=' . $this->password . ';';
  556. if ($this->timeout !== NULL) {
  557. $connection_string .= 'CONNECTTIMEOUT=' . $this->timeout . ';';
  558. }
  559. $username = NULL;
  560. $password = NULL;
  561. }
  562. $options = array(
  563. 'autocommit' => DB2_AUTOCOMMIT_ON,
  564. 'DB2_ATTR_CASE' => DB2_CASE_LOWER
  565. );
  566. $this->connection = db2_connect($connection_string, $username, $password, $options);
  567. if ($this->connection === FALSE) {
  568. $errors = db2_conn_errormsg();
  569. }
  570. }
  571. if ($this->extension == 'mssql') {
  572. if ($this->timeout !== NULL) {
  573. $old_timeout = ini_get('mssql.connect_timeout');
  574. ini_set('mssql.connect_timeout', $this->timeout);
  575. }
  576. fCore::startErrorCapture();
  577. $separator = (fCore::checkOS('windows')) ? ',' : ':';
  578. $this->connection = mssql_connect(($this->port) ? $this->host . $separator . $this->port : $this->host, $this->username, $this->password, TRUE);
  579. if ($this->connection !== FALSE && mssql_select_db($this->database, $this->connection) === FALSE) {
  580. $this->connection = FALSE;
  581. }
  582. $errors = fCore::stopErrorCapture();
  583. if ($this->timeout !== NULL) {
  584. ini_set('mssql.connect_timeout', $old_timeout);
  585. }
  586. }
  587. if ($this->extension == 'mysql') {
  588. if ($this->timeout !== NULL) {
  589. $old_timeout = ini_get('mysql.connect_timeout');
  590. ini_set('mysql.connect_timeout', $this->timeout);
  591. }
  592. if (substr($this->host, 0, 5) == 'sock:') {
  593. $host = substr($this->host, 4);
  594. } elseif ($this->port) {
  595. $host = $this->host . ':' . $this->port;
  596. } else {
  597. $host = $this->host;
  598. }
  599. fCore::startErrorCapture();
  600. $this->connection = mysql_connect($host, $this->username, $this->password, TRUE);
  601. $errors = fCore::stopErrorCapture();
  602. if ($this->connection !== FALSE && mysql_select_db($this->database, $this->connection) === FALSE) {
  603. $errors = 'Unknown database';
  604. $this->connection = FALSE;
  605. }
  606. if ($this->connection && function_exists('mysql_set_charset') && !mysql_set_charset('utf8', $this->connection)) {
  607. throw new fConnectivityException(
  608. 'There was an error setting the database connection to use UTF-8'
  609. );
  610. }
  611. if ($this->timeout !== NULL) {
  612. ini_set('mysql.connect_timeout', $old_timeout);
  613. }
  614. }
  615. if ($this->extension == 'mysqli') {
  616. $this->connection = mysqli_init();
  617. if ($this->timeout !== NULL) {
  618. mysqli_options($this->connection, MYSQLI_OPT_CONNECT_TIMEOUT, $this->timeout);
  619. }
  620. fCore::startErrorCapture();
  621. if (substr($this->host, 0, 5) == 'sock:') {
  622. $result = mysqli_real_connect($this->connection, 'localhost', $this->username, $this->password, $this->database, $this->port, substr($this->host, 5));
  623. } elseif ($this->port) {
  624. $result = mysqli_real_connect($this->connection, $this->host, $this->username, $this->password, $this->database, $this->port);
  625. } else {
  626. $result = mysqli_real_connect($this->connection, $this->host, $this->username, $this->password, $this->database);
  627. }
  628. if (!$result) {
  629. $this->connection = FALSE;
  630. }
  631. $errors = fCore::stopErrorCapture();
  632. if ($this->connection && function_exists('mysqli_set_charset') && !mysqli_set_charset($this->connection, 'utf8')) {
  633. throw new fConnectivityException(
  634. 'There was an error setting the database connection to use UTF-8'
  635. );
  636. }
  637. }
  638. if ($this->extension == 'oci8') {
  639. fCore::startErrorCapture();
  640. $resource = TRUE;
  641. // This driver does not support timeouts so we fake it here
  642. if ($this->timeout !== NULL) {
  643. $resource = fsockopen($this->host, $this->port ? $this->port : 1521, $errno, $errstr, $this->timeout);
  644. if ($resource !== FALSE) {
  645. fclose($resource);
  646. $resource = TRUE;
  647. } else {
  648. $this->connection = FALSE;
  649. }
  650. }
  651. if ($resource) {
  652. $this->connection = oci_connect($this->username, $this->password, $this->host . ($this->port ? ':' . $this->port : '') . '/' . $this->database, 'AL32UTF8');
  653. }
  654. $errors = fCore::stopErrorCapture();
  655. }
  656. if ($this->extension == 'pgsql') {
  657. $connection_string = "dbname='" . addslashes($this->database) . "'";
  658. if ($this->host && $this->host != 'sock:') {
  659. $connection_string .= " host='" . addslashes($this->host) . "'";
  660. }
  661. if ($this->username) {
  662. $connection_string .= " user='" . addslashes($this->username) . "'";
  663. }
  664. if ($this->password) {
  665. $connection_string .= " password='" . addslashes($this->password) . "'";
  666. }
  667. if ($this->port) {
  668. $connection_string .= " port='" . $this->port . "'";
  669. }
  670. if ($this->timeout !== NULL) {
  671. $connection_string .= " connect_timeout='" . $this->timeout . "'";
  672. }
  673. fCore::startErrorCapture();
  674. $this->connection = pg_connect($connection_string, PGSQL_CONNECT_FORCE_NEW);
  675. $errors = fCore::stopErrorCapture();
  676. }
  677. if ($this->extension == 'sqlsrv') {
  678. $options = array(
  679. 'Database' => $this->database
  680. );
  681. if ($this->username !== NULL) {
  682. $options['UID'] = $this->username;
  683. }
  684. if ($this->password !== NULL) {
  685. $options['PWD'] = $this->password;
  686. }
  687. if ($this->timeout !== NULL) {
  688. $options['LoginTimeout'] = $this->timeout;
  689. }
  690. $this->connection = sqlsrv_connect($this->host . ',' . $this->port, $options);
  691. if ($this->connection === FALSE) {
  692. $errors = sqlsrv_errors();
  693. }
  694. sqlsrv_configure('WarningsReturnAsErrors', 0);
  695. }
  696. // Ensure the connection was established
  697. if ($this->connection === FALSE) {
  698. $this->handleConnectionErrors($errors);
  699. }
  700. // Make MySQL act more strict and use UTF-8
  701. if ($this->type == 'mysql') {
  702. $this->execute("SET SQL_MODE = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE'");
  703. $this->execute("SET NAMES 'utf8'");
  704. $this->execute("SET CHARACTER SET utf8");
  705. }
  706. // Make SQLite behave like other DBs for assoc arrays
  707. if ($this->type == 'sqlite') {
  708. $this->execute('PRAGMA short_column_names = 1');
  709. }
  710. // Fix some issues with mssql
  711. if ($this->type == 'mssql') {
  712. if (!isset($this->schema_info['character_set'])) {
  713. $this->determineCharacterSet();
  714. }
  715. $this->execute('SET TEXTSIZE 65536');
  716. $this->execute('SET QUOTED_IDENTIFIER ON');
  717. }
  718. // Make PostgreSQL use UTF-8
  719. if ($this->type == 'postgresql') {
  720. $this->execute("SET NAMES 'UTF8'");
  721. }
  722. // Oracle has different date and timestamp defaults
  723. if ($this->type == 'oracle') {
  724. $this->execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'");
  725. $this->execute("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'");
  726. $this->execute("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZR'");
  727. $this->execute("ALTER SESSION SET NLS_TIME_FORMAT = 'HH24:MI:SS'");
  728. $this->execute("ALTER SESSION SET NLS_TIME_TZ_FORMAT = 'HH24:MI:SS TZR'");
  729. }
  730. }
  731. /**
  732. * Determines the character set of a SQL Server database
  733. *
  734. * @return void
  735. */
  736. protected function determineCharacterSet()
  737. {
  738. $this->schema_info['character_set'] = 'WINDOWS-1252';
  739. $this->schema_info['character_set'] = $this->query("SELECT 'WINDOWS-' + CONVERT(VARCHAR, COLLATIONPROPERTY(CONVERT(NVARCHAR, DATABASEPROPERTYEX(DB_NAME(), 'Collation')), 'CodePage')) AS charset")->fetchScalar();
  740. if ($this->cache) {
  741. $this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);
  742. }
  743. }
  744. /**
  745. * Figures out which extension to use for the database type selected
  746. *
  747. * @return void
  748. */
  749. protected function determineExtension()
  750. {
  751. switch ($this->type) {
  752. case 'db2':
  753. if (extension_loaded('ibm_db2')) {
  754. $this->extension = 'ibm_db2';
  755. } elseif (class_exists('PDO', FALSE) && in_array('ibm', PDO::getAvailableDrivers())) {
  756. $this->extension = 'pdo';
  757. } else {
  758. $type = 'DB2';
  759. $exts = 'ibm_db2, pdo_ibm';
  760. }
  761. break;
  762. case 'mssql':
  763. if (extension_loaded('sqlsrv')) {
  764. $this->extension = 'sqlsrv';
  765. } elseif (extension_loaded('mssql')) {
  766. $this->extension = 'mssql';
  767. } elseif (class_exists('PDO', FALSE) && (in_array('dblib', PDO::getAvailableDrivers()) || in_array('mssql', PDO::getAvailableDrivers()))) {
  768. $this->extension = 'pdo';
  769. } else {
  770. $type = 'MSSQL';
  771. $exts = 'mssql, sqlsrv, pdo_dblib (linux), pdo_mssql (windows)';
  772. }
  773. break;
  774. case 'mysql':
  775. if (extension_loaded('mysqli')) {
  776. $this->extension = 'mysqli';
  777. } elseif (class_exists('PDO', FALSE) && in_array('mysql', PDO::getAvailableDrivers())) {
  778. $this->extension = 'pdo';
  779. } elseif (extension_loaded('mysql')) {
  780. $this->extension = 'mysql';
  781. } else {
  782. $type = 'MySQL';
  783. $exts = 'mysql, pdo_mysql, mysqli';
  784. }
  785. break;
  786. case 'oracle':
  787. if (extension_loaded('oci8')) {
  788. $this->extension = 'oci8';
  789. } elseif (class_exists('PDO', FALSE) && in_array('oci', PDO::getAvailableDrivers())) {
  790. $this->extension = 'pdo';
  791. } else {
  792. $type = 'Oracle';
  793. $exts = 'oci8, pdo_oci';
  794. }
  795. break;
  796. case 'postgresql':
  797. if (extension_loaded('pgsql')) {
  798. $this->extension = 'pgsql';
  799. } elseif (class_exists('PDO', FALSE) && in_array('pgsql', PDO::getAvailableDrivers())) {
  800. $this->extension = 'pdo';
  801. } else {
  802. $type = 'PostgreSQL';
  803. $exts = 'pgsql, pdo_pgsql';
  804. }
  805. break;
  806. case 'sqlite':
  807. $sqlite_version = 0;
  808. if (file_exists($this->database)) {
  809. $database_handle = fopen($this->database, 'r');
  810. $database_version = fread($database_handle, 64);
  811. fclose($database_handle);
  812. if (strpos($database_version, 'SQLite format 3') !== FALSE) {
  813. $sqlite_version = 3;
  814. } elseif (strpos($database_version, '** This file contains an SQLite 2.1 database **') !== FALSE) {
  815. $sqlite_version = 2;
  816. } else {
  817. throw new fConnectivityException(
  818. 'The database specified does not appear to be a valid %1$s or %2$s database',
  819. 'SQLite v2.1',
  820. 'v3'
  821. );
  822. }
  823. }
  824. if ((!$sqlite_version || $sqlite_version == 3) && class_exists('PDO', FALSE) && in_array('sqlite', PDO::getAvailableDrivers())) {
  825. $this->extension = 'pdo';
  826. } elseif ($sqlite_version == 3 && (!class_exists('PDO', FALSE) || !in_array('sqlite', PDO::getAvailableDrivers()))) {
  827. throw new fEnvironmentException(
  828. 'The database specified is an %1$s database and the %2$s extension is not installed',
  829. 'SQLite v3',
  830. 'pdo_sqlite'
  831. );
  832. } elseif ((!$sqlite_version || $sqlite_version == 2) && extension_loaded('sqlite')) {
  833. $this->extension = 'sqlite';
  834. } elseif ($sqlite_version == 2 && !extension_loaded('sqlite')) {
  835. throw new fEnvironmentException(
  836. 'The database specified is an %1$s database and the %2$s extension is not installed',
  837. 'SQLite v2.1',
  838. 'sqlite'
  839. );
  840. } else {
  841. $type = 'SQLite';
  842. $exts = 'pdo_sqlite, sqlite';
  843. }
  844. break;
  845. }
  846. if (!$this->extension) {
  847. throw new fEnvironmentException(
  848. 'The server does not have any of the following extensions for %2$s support: %2$s',
  849. $type,
  850. $exts
  851. );
  852. }
  853. }
  854. /**
  855. * Sets the schema info to be cached to the fCache object specified
  856. *
  857. * @param fCache $cache The cache to cache to
  858. * @param string $key_token Internal use only! (this will be used in the cache key to uniquely identify the cache for this fDatabase object)
  859. * @return void
  860. */
  861. public function enableCaching($cache, $key_token=NULL)
  862. {
  863. $this->cache = $cache;
  864. if ($key_token !== NULL) {
  865. $this->cache_prefix = 'fDatabase::' . $this->type . '::' . $key_token . '::';
  866. }
  867. $this->schema_info = $this->cache->get($this->makeCachePrefix() . 'schema_info', array());
  868. }
  869. /**
  870. * Sets if debug messages should be shown
  871. *
  872. * @param boolean $flag If debugging messages should be shown
  873. * @return void
  874. */
  875. public function enableDebugging($flag)
  876. {
  877. $this->debug = (boolean) $flag;
  878. }
  879. /**
  880. * Escapes a value for insertion into SQL
  881. *
  882. * The valid data types are:
  883. *
  884. * - `'blob'`
  885. * - `'boolean'`
  886. * - `'date'`
  887. * - `'float'`
  888. * - `'identifier'`
  889. * - `'integer'`
  890. * - `'string'` (also varchar, char or text)
  891. * - `'varchar'`
  892. * - `'char'`
  893. * - `'text'`
  894. * - `'time'`
  895. * - `'timestamp'`
  896. *
  897. * In addition to being able to specify the data type, you can also pass
  898. * in an SQL statement with data type placeholders in the following form:
  899. *
  900. * - `%l` for a blob
  901. * - `%b` for a boolean
  902. * - `%d` for a date
  903. * - `%f` for a float
  904. * - `%r` for an indentifier (table or column name)
  905. * - `%i` for an integer
  906. * - `%s` for a string
  907. * - `%t` for a time
  908. * - `%p` for a timestamp
  909. *
  910. * Depending on what `$sql_or_type` and `$value` are, the output will be
  911. * slightly different. If `$sql_or_type` is a data type or a single
  912. * placeholder and `$value` is:
  913. *
  914. * - a scalar value - an escaped SQL string is returned
  915. * - an array - an array of escaped SQL strings is returned
  916. *
  917. * If `$sql_or_type` is a SQL string and `$value` is:
  918. *
  919. * - a scalar value - the escaped value is inserted into the SQL string
  920. * - an array - the escaped values are inserted into the SQL string separated by commas
  921. *
  922. * If `$sql_or_type` is a SQL string, it is also possible to pass an array
  923. * of all values as a single parameter instead of one value per parameter.
  924. * An example would look like the following:
  925. *
  926. * {{{
  927. * #!php
  928. * $db->escape(
  929. * "SELECT * FROM users WHERE status = %s AND authorization_level = %s",
  930. * array('Active', 'Admin')
  931. * );
  932. * }}}
  933. *
  934. * @param string $sql_or_type This can either be the data type to escape or an SQL string with a data type placeholder - see method description
  935. * @param mixed $value The value to escape - both single values and arrays of values are supported, see method description for details
  936. * @param mixed ...
  937. * @return mixed The escaped value/SQL or an array of the escaped values
  938. */
  939. public function escape($sql_or_type, $value)
  940. {
  941. $values = array_slice(func_get_args(), 1);
  942. if (sizeof($values) < 1) {
  943. throw new fProgrammerException(
  944. 'No value was specified to escape'
  945. );
  946. }
  947. // Convert all objects into strings
  948. $values = $this->scalarize($values);
  949. $value = array_shift($values);
  950. // Handle single value escaping
  951. $callback = NULL;
  952. switch ($sql_or_type) {
  953. case 'blob':
  954. case '%l':
  955. $callback = $this->escapeBlob;
  956. break;
  957. case 'boolean':
  958. case '%b':
  959. $callback = $this->escapeBoolean;
  960. break;
  961. case 'date':
  962. case '%d':
  963. $callback = $this->escapeDate;
  964. break;
  965. case 'float':
  966. case '%f':
  967. $callback = $this->escapeFloat;
  968. break;
  969. case 'identifier':
  970. case '%r':
  971. $callback = $this->escapeIdentifier;
  972. break;
  973. case 'integer':
  974. case '%i':
  975. $callback = $this->escapeInteger;
  976. break;
  977. case 'string':
  978. case 'varchar':
  979. case 'char':
  980. case 'text':
  981. case '%s':
  982. $callback = $this->escapeString;
  983. break;
  984. case 'time':
  985. case '%t':
  986. $callback = $this->escapeTime;
  987. break;
  988. case 'timestamp':
  989. case '%p':
  990. $callback = $this->escapeTimestamp;
  991. break;
  992. }
  993. if ($callback) {
  994. if (is_array($value)) {
  995. // If the values were passed as a single array, this handles that
  996. if (count($value) == 1 && is_array(current($value))) {
  997. $value = current($value);
  998. }
  999. return array_map($callback, $value);
  1000. }
  1001. return call_user_func($callback, $value);
  1002. }
  1003. // Separate the SQL from quoted values
  1004. $parts = $this->splitSQL($sql_or_type, $placeholders);
  1005. // If the values were passed as a single array, this handles that
  1006. if (count($values) == 0 && is_array($value) && count($value) == $placeholders) {
  1007. $values = $value;
  1008. $value = array_shift($values);
  1009. }
  1010. array_unshift($values, $value);
  1011. $sql = $this->extractStrings($parts, $values);
  1012. return $this->escapeSQL($sql, $values, FALSE);
  1013. }
  1014. /**
  1015. * Escapes a blob for use in SQL, includes surround quotes when appropriate
  1016. *
  1017. * A `NULL` value will be returned as `'NULL'`
  1018. *
  1019. * @param string $value The blob to escape
  1020. * @return string The escaped blob
  1021. */
  1022. private function escapeBlob($value)
  1023. {
  1024. if ($value === NULL) {
  1025. return 'NULL';
  1026. }
  1027. $this->connect();
  1028. if ($this->type == 'db2') {
  1029. return "BLOB(X'" . bin2hex($value) . "')";
  1030. } elseif ($this->type == 'mysql') {
  1031. return "x'" . bin2hex($value) . "'";
  1032. } elseif ($this->type == 'postgresql') {
  1033. $output = '';
  1034. for ($i=0; $i<strlen($value); $i++) {
  1035. $output .= '\\\\' . str_pad(decoct(ord($value[$i])), 3, '0', STR_PAD_LEFT);
  1036. }
  1037. return "E'" . $output . "'";
  1038. } elseif ($this->extension == 'sqlite') {
  1039. return "'" . bin2hex($value) . "'";
  1040. } elseif ($this->type == 'sqlite') {
  1041. return "X'" . bin2hex($value) . "'";
  1042. } elseif ($this->type == 'mssql') {
  1043. return '0x' . bin2hex($value);
  1044. } elseif ($this->type == 'oracle') {
  1045. return "'" . bin2hex($value) . "'";
  1046. }
  1047. }
  1048. /**
  1049. * Escapes a boolean for use in SQL, includes surround quotes when appropriate
  1050. *
  1051. * A `NULL` value will be returned as `'NULL'`
  1052. *
  1053. * @param boolean $value The boolean to escape
  1054. * @return string The database equivalent of the boolean passed
  1055. */
  1056. private function escapeBoolean($value)
  1057. {
  1058. if ($value === NULL) {
  1059. return 'NULL';
  1060. }
  1061. if (in_array($this->type, array('postgresql', 'mysql'))) {
  1062. return ($value) ? 'TRUE' : 'FALSE';
  1063. } elseif (in_array($this->type, array('mssql', 'sqlite', 'db2'))) {
  1064. return ($value) ? "'1'" : "'0'";
  1065. } elseif ($this->type == 'oracle') {
  1066. return ($value) ? '1' : '0';
  1067. }
  1068. }
  1069. /**
  1070. * Escapes a date for use in SQL, includes surrounding quotes
  1071. *
  1072. * A `NULL` or invalid value will be returned as `'NULL'`
  1073. *
  1074. * @param string $value The date to escape
  1075. * @return string The escaped date
  1076. */
  1077. private function escapeDate($value)
  1078. {
  1079. if ($value === NULL) {
  1080. return 'NULL';
  1081. }
  1082. try {
  1083. $value = new fDate($value);
  1084. return "'" . $value->format('Y-m-d') . "'";
  1085. } catch (fValidationException $e) {
  1086. return 'NULL';
  1087. }
  1088. }
  1089. /**
  1090. * Escapes a float for use in SQL
  1091. *
  1092. * A `NULL` value will be returned as `'NULL'`
  1093. *
  1094. * @param float $value The float to escape
  1095. * @return string The escaped float
  1096. */
  1097. private function escapeFloat($value)
  1098. {
  1099. if ($value === NULL) {
  1100. return 'NULL';
  1101. }
  1102. if (!strlen($value)) {
  1103. return 'NULL';
  1104. }
  1105. if (!preg_match('#^[+\-]?([0-9]+(\.([0-9]+)?)?|(\.[0-9]+))$#D', $value)) {
  1106. return 'NULL';
  1107. }
  1108. $value = rtrim($value, '.');
  1109. $value = preg_replace('#(?<![0-9])\.#', '0.', $value);
  1110. return (string) $value;
  1111. }
  1112. /**
  1113. * Escapes an identifier for use in SQL, necessary for reserved words
  1114. *
  1115. * @param string $value The identifier to escape
  1116. * @return string The escaped identifier
  1117. */
  1118. private function escapeIdentifier($value)
  1119. {
  1120. $value = '"' . str_replace(
  1121. array('"', '.'),
  1122. array('', '"."'),
  1123. $value
  1124. ) . '"';
  1125. if (in_array($this->type, array('oracle', 'db2'))) {
  1126. $value = strtoupper($value);
  1127. }
  1128. return $value;
  1129. }
  1130. /**
  1131. * Escapes an integer for use in SQL
  1132. *
  1133. * A `NULL` or invalid value will be returned as `'NULL'`
  1134. *
  1135. * @param integer $value The integer to escape
  1136. * @return string The escaped integer
  1137. */
  1138. private function escapeInteger($value)
  1139. {
  1140. if ($value === NULL) {
  1141. return 'NULL';
  1142. }
  1143. if (!strlen($value)) {
  1144. return 'NULL';
  1145. }
  1146. if (!preg_match('#^([+\-]?[0-9]+)(\.[0-9]*)?$#D', $value, $matches)) {
  1147. return 'NULL';
  1148. }
  1149. return str_replace('+', '', $matches[1]);
  1150. }
  1151. /**
  1152. * Escapes a string for use in SQL, includes surrounding quotes
  1153. *
  1154. * A `NULL` value will be returned as `'NULL'`.
  1155. *
  1156. * @param string $value The string to escape
  1157. * @return string The escaped string
  1158. */
  1159. private function escapeString($value)
  1160. {
  1161. if ($value === NULL) {
  1162. return 'NULL';
  1163. }
  1164. $this->connect();
  1165. if ($this->type == 'db2') {
  1166. return "'" . str_replace("'", "''", $value) . "'";
  1167. } elseif ($this->extension == 'mysql') {
  1168. return "'" . mysql_real_escape_string($value, $this->connection) . "'";
  1169. } elseif ($this->extension == 'mysqli') {
  1170. return "'" . mysqli_real_escape_string($this->connection, $value) . "'";
  1171. } elseif ($this->extension == 'pgsql') {
  1172. return "'" . pg_escape_string($value) . "'";
  1173. } elseif ($this->extension == 'sqlite') {
  1174. return "'" . sqlite_escape_string($value) . "'";
  1175. } elseif ($this->type == 'oracle') {
  1176. return "'" . str_replace("'", "''", $value) . "'";
  1177. } elseif ($this->type == 'mssql') {
  1178. // If there are any non-ASCII characters, we need to escape
  1179. if (preg_match('#[^\x00-\x7F]#', $value)) {
  1180. preg_match_all('#.|^\z#us', $value, $characters);
  1181. $output = "";
  1182. $last_type = NULL;
  1183. foreach ($characters[0] as $character) {
  1184. if (strlen($character) > 1) {
  1185. $b = array_map('ord', str_split($character));
  1186. switch (strlen($character)) {
  1187. case 2:
  1188. $bin = substr(decbin($b[0]), 3) .
  1189. substr(decbin($b[1]), 2);
  1190. break;
  1191. case 3:
  1192. $bin = substr(decbin($b[0]), 4) .
  1193. substr(decbin($b[1]), 2) .
  1194. substr(decbin($b[2]), 2);
  1195. break;
  1196. // If it is a 4-byte character, MSSQL can't store it
  1197. // so instead store a ?
  1198. default:
  1199. $output .= '?';
  1200. continue;
  1201. }
  1202. if ($last_type == 'nchar') {
  1203. $output .= '+';
  1204. } elseif ($last_type == 'char') {
  1205. $output .= "'+";
  1206. }
  1207. $output .= "NCHAR(" . bindec($bin) . ")";
  1208. $last_type = 'nchar';
  1209. } else {
  1210. if (!$last_type) {
  1211. $output .= "'";
  1212. } elseif ($last_type == 'nchar') {
  1213. $output .= "+'";
  1214. }
  1215. $output .= $character;
  1216. // Escape single quotes
  1217. if ($character == "'") {
  1218. $output .= "'";
  1219. }
  1220. $last_type = 'char';
  1221. }
  1222. }
  1223. if ($last_type == 'char') {
  1224. $output .= "'";
  1225. } elseif (!$last_type) {
  1226. $output .= "''";
  1227. }
  1228. // ASCII text is normal
  1229. } else {
  1230. $output = "'" . str_replace("'", "''", $value) . "'";
  1231. }
  1232. # a \ before a \r\n has to be escaped with another \
  1233. return preg_replace('#(?<!\\\\)\\\\(?=\r\n)#', '\\\\\\\\', $output);
  1234. } elseif ($this->extension == 'pdo') {
  1235. return $this->connection->quote($value);
  1236. }
  1237. }
  1238. /**
  1239. * Takes a SQL string and an array of values and replaces the placeholders with the value
  1240. *
  1241. * @param string $sql The SQL string containing placeholders
  1242. * @param array $values An array of values to escape into the SQL
  1243. * @param boolean $unescape_percent If %% should be translated to % - this should only be done once processing of the string is done
  1244. * @return string The SQL with the values escaped into it
  1245. */
  1246. private function escapeSQL($sql, $values, $unescape_percent)
  1247. {
  1248. $original_sql = $sql;
  1249. $pieces = preg_split('#(?<!%)(%[lbdfristp])\b#', $sql, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  1250. $sql = '';
  1251. $value = array_shift($values);
  1252. $missing_values = -1;
  1253. foreach ($pieces as $piece) {
  1254. switch ($piece) {
  1255. case '%l':
  1256. $callback = $this->escapeBlob;
  1257. break;
  1258. case '%b':
  1259. $callback = $this->escapeBoolean;
  1260. break;
  1261. case '%d':
  1262. $callback = $this->escapeDate;
  1263. break;
  1264. case '%f':
  1265. $callback = $this->escapeFloat;
  1266. break;
  1267. case '%r':
  1268. $callback = $this->escapeIdentifier;
  1269. break;
  1270. case '%i':
  1271. $callback = $this->escapeInteger;
  1272. break;
  1273. case '%s':
  1274. $callback = $this->escapeString;
  1275. break;
  1276. case '%t':
  1277. $callback = $this->escapeTime;
  1278. break;
  1279. case '%p':
  1280. $callback = $this->escapeTimestamp;
  1281. break;
  1282. default:
  1283. if ($unescape_percent) {
  1284. $piece = str_replace('%%', '%', $piece);
  1285. }
  1286. $sql .= $piece;
  1287. continue 2;
  1288. }
  1289. if (is_array($value)) {
  1290. $sql .= join(', ', array_map($callback, $value));
  1291. } else {
  1292. $sql .= call_user_func($callback, $value);
  1293. }
  1294. if (sizeof($values)) {
  1295. $value = array_shift($values);
  1296. } else {
  1297. $value = NULL;
  1298. $missing_values++;
  1299. }
  1300. }
  1301. if ($missing_values > 0) {
  1302. throw new fProgrammerException(
  1303. '%1$s value(s) are missing for the placeholders in: %2$s',
  1304. $missing_values,
  1305. $original_sql
  1306. );
  1307. }
  1308. if (sizeof($values)) {
  1309. throw new fProgrammerException(
  1310. '%1$s extra value(s) were passed for the placeholders in: %2$s',
  1311. sizeof($values),
  1312. $original_sql
  1313. );
  1314. }
  1315. return $sql;
  1316. }
  1317. /**
  1318. * Escapes a time for use in SQL, includes surrounding quotes
  1319. *
  1320. * A `NULL` or invalid value will be returned as `'NULL'`
  1321. *
  1322. * @param string $value The time to escape
  1323. * @return string The escaped time
  1324. */
  1325. private function escapeTime($value)
  1326. {
  1327. if ($value === NULL) {
  1328. return 'NULL';
  1329. }
  1330. try {
  1331. $value = new fTime($value);
  1332. if ($this->type == 'mssql' || $this->type == 'oracle') {
  1333. return "'" . $value->format('1970-01-01 H:i:s') . "'";
  1334. }
  1335. return "'" . $value->format('H:i:s') . "'";
  1336. } catch (fValidationException $e) {
  1337. return 'NULL';
  1338. }
  1339. }
  1340. /**
  1341. * Escapes a timestamp for use in SQL, includes surrounding quotes
  1342. *
  1343. * A `NULL` or invalid value will be returned as `'NULL'`
  1344. *
  1345. * @param string $value The timestamp to escape
  1346. * @return string The escaped timestamp
  1347. */
  1348. private function escapeTimestamp($value)
  1349. {
  1350. if ($value === NULL) {
  1351. return 'NULL';
  1352. }
  1353. try {
  1354. $value = new fTimestamp($value);
  1355. return "'" . $value->format('Y-m-d H:i:s') . "'";
  1356. } catch (fValidationException $e) {
  1357. return 'NULL';
  1358. }
  1359. }
  1360. /**
  1361. * Executes one or more SQL queries without returning any results
  1362. *
  1363. * @param string|fStatement $statement One or more SQL statements in a string or an fStatement prepared statement
  1364. * @param mixed $value The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
  1365. * @param mixed ...
  1366. * @return void
  1367. */
  1368. public function execute($statement)
  1369. {
  1370. $args = func_get_args();
  1371. $params = array_slice($args, 1);
  1372. if (is_object($statement)) {
  1373. return $this->run($statement, NULL, $params);
  1374. }
  1375. $queries = $this->preprocess($statement, $params, FALSE);
  1376. $output = array();
  1377. foreach ($queries as $query) {
  1378. $this->run($query);
  1379. }
  1380. }
  1381. /**
  1382. * Pulls quoted strings out into the values array for simpler processing
  1383. *
  1384. * @param array $parts The parts of the SQL - alternating SQL and quoted strings
  1385. * @param array &$values The value to be escaped into the SQL
  1386. * @return string The SQL with all quoted string values extracted into the `$values` array
  1387. */
  1388. private function extractStrings($parts, &$values)
  1389. {
  1390. $sql = '';
  1391. $value_number = 0;
  1392. foreach ($parts as $part) {
  1393. // We leave blank strings in because Oracle treats them like NULL
  1394. if ($part[0] == "'" && $part != "''") {
  1395. $sql .= '%s';
  1396. $value = str_replace("''", "'", substr($part, 1, -1));
  1397. if ($this->type == 'postgresql') {
  1398. $value = str_replace('\\\\', '\\', $value);
  1399. }
  1400. $values = array_merge(
  1401. array_slice($values, 0, $value_number),
  1402. array($value),
  1403. array_slice($values, $value_number)
  1404. );
  1405. $value_number++;
  1406. } else {
  1407. $value_number += preg_match_all('#(?<!%)%[lbdfristp]\b#', $part, $trash);
  1408. unset($trash);
  1409. $sql .= $part;
  1410. }
  1411. }
  1412. return $sql;
  1413. }
  1414. /**
  1415. * Returns the database connection resource or object
  1416. *
  1417. * @return mixed The database connection
  1418. */
  1419. public function getConnection()
  1420. {
  1421. $this->connect();
  1422. return $this->connection;
  1423. }
  1424. /**
  1425. * Gets the name of the database currently connected to
  1426. *
  1427. * @return string The name of the database currently connected to
  1428. */
  1429. public function getDatabase()
  1430. {
  1431. return $this->database;
  1432. }
  1433. /**
  1434. * Gets the php extension being used
  1435. *
  1436. * @internal
  1437. *
  1438. * @return string The php extension used for database interaction
  1439. */
  1440. public function getExtension()
  1441. {
  1442. return $this->extension;
  1443. }
  1444. /**
  1445. * Gets the host for this database
  1446. *
  1447. * @return string The host
  1448. */
  1449. public function getHost()
  1450. {
  1451. return $this->host;
  1452. }
  1453. /**
  1454. * Gets the por…

Large files files are truncated, but you can click here to view the full file