PageRenderTime 96ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fORMDatabase.php

https://bitbucket.org/dsqmoore/flourish
PHP | 1357 lines | 840 code | 196 blank | 321 comment | 157 complexity | ac1f3c093b0e725e85e08cda93dbc19c MD5 | raw file

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

  1. <?php
  2. /**
  3. * Holds a single instance of the fDatabase class and provides database manipulation functionality for ORM code
  4. *
  5. * @copyright Copyright (c) 2007-2011 Will Bond, others
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @author Craig Ruksznis, iMarc LLC [cr-imarc] <craigruk@imarc.net>
  8. * @license http://flourishlib.com/license
  9. *
  10. * @package Flourish
  11. * @link http://flourishlib.com/fORMDatabase
  12. *
  13. * @version 1.0.0b32
  14. * @changes 1.0.0b32 Added support to ::addWhereClause() for the `^~` and `$~` operators [wb, 2011-06-20]
  15. * @changes 1.0.0b31 Fixed a bug with ::addWhereClause() generating invalid SQL [wb, 2011-05-10]
  16. * @changes 1.0.0b30 Fixed ::insertFromAndGroupByClauses() to insert `MAX()` around columns in related tables in the `ORDER BY` clause when a `GROUP BY` is used [wb, 2011-02-03]
  17. * @changes 1.0.0b29 Added code to handle old PCRE engines that don't support unicode character properties [wb, 2010-12-06]
  18. * @changes 1.0.0b28 Fixed a bug in the fProgrammerException that is thrown when an improperly formatted OR condition is provided [wb, 2010-11-24]
  19. * @changes 1.0.0b27 Fixed ::addWhereClause() to ignore fuzzy search clauses with no values to match [wb, 2010-10-19]
  20. * @changes 1.0.0b26 Fixed ::insertFromAndGroupByClauses() to handle SQL where a table is references in more than one capitalization [wb, 2010-07-26]
  21. * @changes 1.0.0b25 Fixed ::insertFromAndGroupByClauses() to properly handle recursive relationships [wb, 2010-07-22]
  22. * @changes 1.0.0b24 Fixed ::parseSearchTerms() to work with non-ascii terms [wb, 2010-06-30]
  23. * @changes 1.0.0b23 Fixed error messages in ::retrieve() [wb, 2010-04-23]
  24. * @changes 1.0.0b22 Added support for IBM DB2, fixed an issue with building record sets or records that have recursive relationships [wb, 2010-04-13]
  25. * @changes 1.0.0b21 Changed ::injectFromAndGroupByClauses() to be able to handle table aliases that contain other aliases inside of them [wb, 2010-03-03]
  26. * @changes 1.0.0b20 Fixed a bug where joining to a table two separate ways could cause table alias issues and incorrect SQL to be generated [wb, 2009-12-16]
  27. * @changes 1.0.0b19 Added the ability to compare columns with the `=:`, `!:`, `<:`, `<=:`, `>:` and `>=:` operators [wb, 2009-12-08]
  28. * @changes 1.0.0b18 Fixed a bug affecting where conditions with columns that are not null but have a default value [wb, 2009-11-03]
  29. * @changes 1.0.0b17 Added support for multiple databases [wb, 2009-10-28]
  30. * @changes 1.0.0b16 Internal Backwards Compatibility Break - Renamed methods and significantly changed parameters and functionality for SQL statements to use value placeholders, identifier escaping and to handle schemas [wb, 2009-10-22]
  31. * @changes 1.0.0b15 Streamlined intersection operator SQL and added support for the second value being NULL [wb, 2009-09-21]
  32. * @changes 1.0.0b14 Added support for the intersection operator `><` to ::createWhereClause() [wb, 2009-07-13]
  33. * @changes 1.0.0b13 Added support for the `AND LIKE` operator `&~` to ::createWhereClause() [wb, 2009-07-09]
  34. * @changes 1.0.0b12 Added support for the `NOT LIKE` operator `!~` to ::createWhereClause() [wb, 2009-07-08]
  35. * @changes 1.0.0b11 Added support for concatenated columns to ::escapeBySchema() [cr-imarc, 2009-06-19]
  36. * @changes 1.0.0b10 Updated ::createWhereClause() to properly handle NULLs for arrays of values when doing = and != comparisons [wb, 2009-06-17]
  37. * @changes 1.0.0b9 Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
  38. * @changes 1.0.0b8 Fixed a bug with ::creatingWhereClause() where a null value would not be escaped property [wb, 2009-05-12]
  39. * @changes 1.0.0b7 Fixed a bug where an OR condition in ::createWhereClause() could not have one of the values be an array [wb, 2009-04-22]
  40. * @changes 1.0.0b6 ::insertFromAndGroupByClauses() will no longer wrap ungrouped columns if in a CAST or CASE statement for ORDER BY clauses of queries with a GROUP BY clause [wb, 2009-03-23]
  41. * @changes 1.0.0b5 Fixed ::parseSearchTerms() to include stop words when they are the only thing in the search string [wb, 2008-12-31]
  42. * @changes 1.0.0b4 Fixed a bug where loading a related record in the same table through a one-to-many relationship caused recursion [wb, 2008-12-24]
  43. * @changes 1.0.0b3 Fixed a bug from 1.0.0b2 [wb, 2008-12-05]
  44. * @changes 1.0.0b2 Added support for != and <> to ::createWhereClause() and ::createHavingClause() [wb, 2008-12-04]
  45. * @changes 1.0.0b The initial implementation [wb, 2007-08-04]
  46. */
  47. class fORMDatabase
  48. {
  49. // The following constants allow for nice looking callbacks to static methods
  50. const addHavingClause = 'fORMDatabase::addHavingClause';
  51. const addOrderByClause = 'fORMDatabase::addOrderByClause';
  52. const addPrimaryKeyWhereParams = 'fORMDatabase::addPrimaryKeyWhereParams';
  53. const addWhereClause = 'fORMDatabase::addWhereClause';
  54. const attach = 'fORMDatabase::attach';
  55. const injectFromAndGroupByClauses = 'fORMDatabase::injectFromAndGroupByClauses';
  56. const makeCondition = 'fORMDatabase::makeCondition';
  57. const parseSearchTerms = 'fORMDatabase::parseSearchTerms';
  58. const reset = 'fORMDatabase::reset';
  59. const retrieve = 'fORMDatabase::retrieve';
  60. const splitHavingConditions = 'fORMDatabase::splitHavingConditions';
  61. /**
  62. * An array of fDatabase objects
  63. *
  64. * @var array
  65. */
  66. static private $database_objects = array();
  67. /**
  68. * If the PCRE engine supports unicode character properties
  69. *
  70. * @var boolean
  71. */
  72. static private $pcre_supports_unicode_character_properties = NULL;
  73. /**
  74. * Allows attaching an fDatabase-compatible objects for by ORM code
  75. *
  76. * If a `$name` other than `default` is used, any fActiveRecord classes
  77. * that should use it will need to be configured by passing the class name
  78. * and `$name` to ::mapClassToDatabase(). The `$name` parameter should be
  79. * unique per database or database master/slave setup.
  80. *
  81. * The `$role` is used by code to allow for master/slave database setups.
  82. * There can only be one database object attached for either of the roles,
  83. * `'read'` or `'write'`. If the role `'both'` is specified, it will
  84. * be applied to both the `'read'` and `'write'` roles. Any sort of logic
  85. * for picking one out of multiple databases should be done before this
  86. * method is called.
  87. *
  88. * @param fDatabase $database An object that is compatible with fDatabase
  89. * @param string $name The name for the database instance
  90. * @param string $role If the database should be used for `'read'`, `'write'` or `'both'` operations
  91. * @return void
  92. */
  93. static public function attach($database, $name='default', $role='both')
  94. {
  95. $valid_roles = array('both', 'write', 'read');
  96. if (!in_array($role, $valid_roles)) {
  97. throw new fProgrammerException(
  98. 'The role specified, %1$s, is invalid. Must be one of: %2$s.',
  99. $role,
  100. join(', ', $valid_roles)
  101. );
  102. }
  103. if (!isset(self::$database_objects[$name])) {
  104. self::$database_objects[$name] = array();
  105. }
  106. settype($role, 'array');
  107. if ($role == array('both')) {
  108. $role = array('write', 'read');
  109. }
  110. foreach ($role as $_role) {
  111. self::$database_objects[$name][$_role] = $database;
  112. }
  113. }
  114. /**
  115. * Translated the where condition for a single column into a SQL clause
  116. *
  117. * @param fDatabase $db The database the query will be run on
  118. * @param fSchema $schema The schema for the database
  119. * @param array $params The parameters for the fDatabase::query() call
  120. * @param string $table The table to create the condition for
  121. * @param string $column The column to store the value in, may also be shorthand column name like `table.column` or `table=>related_table.column`
  122. * @param string $operator Should be `'='`, `'!='`, `'!'`, `'<>'`, `'<'`, `'<='`, `'>'`, `'>='`, `'IN'`, `'NOT IN'`
  123. * @param mixed $values The value(s) to escape
  124. * @param string $escaped_column The escaped column to use in the SQL
  125. * @param string $placeholder This allows overriding the placeholder
  126. * @return array The modified parameters
  127. */
  128. static private function addColumnCondition($db, $schema, $params, $table, $column, $operator, $values, $escaped_column=NULL, $placeholder=NULL)
  129. {
  130. // Some objects when cast to an array turn the members into array keys
  131. if (!is_object($values)) {
  132. settype($values, 'array');
  133. } else {
  134. $values = array($values);
  135. }
  136. // Make sure we have an array with something in it to compare to
  137. if (!$values) { $values = array(NULL); }
  138. // If the table and column specified are real and not some combination
  139. $real_column = $escaped_column === NULL && $placeholder === NULL;
  140. if ($escaped_column === NULL) {
  141. $escaped_column = $db->escape('%r', (strpos($column, '.') === FALSE) ? $table . '.' . $column : $column);
  142. }
  143. list($table, $column) = self::getTableAndColumn($schema, $table, $column);
  144. if ($placeholder === NULL && !in_array($operator, array('=:', '!=:', '<:', '<=:', '>:', '>=:'))) {
  145. $placeholder = $schema->getColumnInfo($table, $column, 'placeholder');
  146. }
  147. // More than one value
  148. if (sizeof($values) > 1) {
  149. switch ($operator) {
  150. case '=':
  151. $non_null_values = array();
  152. $has_null = FALSE;
  153. foreach ($values as $value) {
  154. if ($value === NULL) {
  155. $has_null = TRUE;
  156. continue;
  157. }
  158. $non_null_values[] = $value;
  159. }
  160. if ($has_null) {
  161. $params[0] .= '(' . $escaped_column . ' IS NULL OR ';
  162. }
  163. $params[0] .= $escaped_column . ' IN (' . $placeholder . ')';
  164. $params[] = $non_null_values;
  165. if ($has_null) {
  166. $params[0] .= ')';
  167. }
  168. break;
  169. case '!':
  170. $non_null_values = array();
  171. $has_null = FALSE;
  172. foreach ($values as $value) {
  173. if ($value === NULL) {
  174. $has_null = TRUE;
  175. continue;
  176. }
  177. $non_null_values[] = $value;
  178. }
  179. if ($has_null) {
  180. $params[0] .= '(' . $escaped_column . ' IS NOT NULL AND ';
  181. }
  182. $params[0] .= $escaped_column . ' NOT IN (' . $placeholder . ')';
  183. $params[] = $non_null_values;
  184. if ($has_null) {
  185. $params[0] .= ')';
  186. }
  187. break;
  188. case '~':
  189. $condition = array();
  190. foreach ($values as $value) {
  191. $condition[] = $escaped_column . ' LIKE %s';
  192. $params[] = '%' . $value . '%';
  193. }
  194. $params[0] .= '(' . join(' OR ', $condition) . ')';
  195. break;
  196. case '^~':
  197. $condition = array();
  198. foreach ($values as $value) {
  199. $condition[] = $escaped_column . ' LIKE %s';
  200. $params[] = $value . '%';
  201. }
  202. $params[0] .= '(' . join(' OR ', $condition) . ')';
  203. break;
  204. case '$~':
  205. $condition = array();
  206. foreach ($values as $value) {
  207. $condition[] = $escaped_column . ' LIKE %s';
  208. $params[] = '%' . $value;
  209. }
  210. $params[0] .= '(' . join(' OR ', $condition) . ')';
  211. break;
  212. case '&~':
  213. $condition = array();
  214. foreach ($values as $value) {
  215. $condition[] = $escaped_column . ' LIKE %s';
  216. $params[] = '%' . $value . '%';
  217. }
  218. $params[0] .= '(' . join(' AND ', $condition) . ')';
  219. break;
  220. case '!~':
  221. $condition = array();
  222. foreach ($values as $value) {
  223. $condition[] = $escaped_column . ' NOT LIKE %s';
  224. $params[] = '%' . $value . '%';
  225. }
  226. $params[0] .= '(' . join(' AND ', $condition) . ')';
  227. break;
  228. default:
  229. throw new fProgrammerException(
  230. 'An invalid array comparison operator, %s, was specified for an array of values',
  231. $operator
  232. );
  233. break;
  234. }
  235. // A single value
  236. } else {
  237. if ($values === array()) {
  238. $value = NULL;
  239. } else {
  240. $value = current($values);
  241. }
  242. switch ($operator) {
  243. case '!:':
  244. $operator = '<>:';
  245. case '=:':
  246. case '<:':
  247. case '<=:':
  248. case '>:':
  249. case '>=:':
  250. $params[0] .= $escaped_column . ' ';
  251. $params[0] .= substr($operator, 0, -1) . ' ';
  252. // If the column to match is a function, split the function
  253. // name off so we can escape the column name
  254. $prefix = '';
  255. $suffix = '';
  256. if (preg_match('#^([^(]+\()\s*([^\s]+)\s*(\))$#D', $value, $parts)) {
  257. $prefix = $parts[1];
  258. $value = $parts[2];
  259. $suffix = $parts[3];
  260. }
  261. $params[0] .= $prefix . $db->escape('%r', (strpos($value, '.') === FALSE) ? $table . '.' . $value : $value) . $suffix;
  262. break;
  263. case '=':
  264. if ($value === NULL) {
  265. $operator = 'IS';
  266. }
  267. $params[0] .= $escaped_column . ' ' . $operator . ' ' . $placeholder;
  268. $params[] = $value;
  269. break;
  270. case '!':
  271. $operator = '<>';
  272. if ($value !== NULL) {
  273. $params[0] .= '(';
  274. } else {
  275. $operator = 'IS NOT';
  276. }
  277. $params[0] .= $escaped_column . ' ' . $operator . ' ' . $placeholder;
  278. $params[] = $value;
  279. if ($value !== NULL) {
  280. $params[0] .= ' OR ' . $escaped_column . ' IS NULL)';
  281. }
  282. break;
  283. case '<':
  284. case '<=':
  285. case '>':
  286. case '>=':
  287. $params[0] .= $escaped_column . ' ' . $operator . ' ' . $placeholder;
  288. $params[] = $value;
  289. break;
  290. case '~':
  291. $params[0] .= $escaped_column . ' LIKE %s';
  292. $params[] = '%' . $value . '%';
  293. break;
  294. case '^~':
  295. $params[0] .= $escaped_column . ' LIKE %s';
  296. $params[] = $value . '%';
  297. break;
  298. case '$~':
  299. $params[0] .= $escaped_column . ' LIKE %s';
  300. $params[] = '%' . $value;
  301. break;
  302. case '!~':
  303. $params[0] .= $escaped_column . ' NOT LIKE %s';
  304. $params[] = '%' . $value . '%';
  305. break;
  306. default:
  307. throw new fProgrammerException(
  308. 'An invalid comparison operator, %s, was specified for a single value',
  309. $operator
  310. );
  311. break;
  312. }
  313. }
  314. return $params;
  315. }
  316. /**
  317. * Creates a `HAVING` clause from an array of conditions
  318. *
  319. * @internal
  320. *
  321. * @param fDatabase $db The database the query will be executed on
  322. * @param fSchema $schema The schema for the database
  323. * @param array $params The params for the fDatabase::query() call
  324. * @param string $table The table the query is being executed on
  325. * @param array $conditions The array of conditions - see fRecordSet::build() for format
  326. * @return array The params with the `HAVING` clause added
  327. */
  328. static public function addHavingClause($db, $schema, $params, $table, $conditions)
  329. {
  330. $i = 0;
  331. foreach ($conditions as $expression => $value) {
  332. if ($i) {
  333. $params[0] .= ' AND ';
  334. }
  335. // Splits the operator off of the end of the expression
  336. if (in_array(substr($expression, -3), array('!=:', '>=:', '<=:', '<>:'))) {
  337. $operator = strtr(
  338. substr($expression, -3),
  339. array('<>:' => '!:', '!=:' => '!:')
  340. );
  341. $expression = substr($expression, 0, -3);
  342. } elseif (in_array(substr($expression, -2), array('<=', '>=', '!=', '<>', '=:', '!:', '<:', '>:'))) {
  343. $operator = strtr(
  344. substr($expression, -2),
  345. array('<>' => '!', '!=' => '!')
  346. );
  347. $expression = substr($expression, 0, -2);
  348. } else {
  349. $operator = substr($expression, -1);
  350. $expression = substr($expression, 0, -1);
  351. }
  352. // Quotes the identifier in the expression
  353. preg_match('#^([^(]+\()\s*([^\s]+)\s*(\))$#D', $expression, $parts);
  354. $expression = $parts[1] . $db->escape('%r', $parts[2]) . $parts[3];
  355. // The AVG, SUM and COUNT functions all return a number
  356. $function = strtolower(substr($parts[2], 0, -1));
  357. $placeholder = (in_array($function, array('avg', 'sum', 'count'))) ? '%f' : NULL;
  358. // This removes stray quoting inside of {route} specified for shorthand column names
  359. $expression = preg_replace('#(\{\w+)"\."(\w+\})#', '\1.\2', $expression);
  360. $params = self::addColumnCondition($db, $schema, $params, $table, $parts[2], $operator, $value, $expression, $placeholder);
  361. $i++;
  362. }
  363. return $params;
  364. }
  365. /**
  366. * Adds an `ORDER BY` clause to an array of params for an fDatabase::query() call
  367. *
  368. * @internal
  369. *
  370. * @param fDatabase $db The database the query will be executed on
  371. * @param fSchema $schema The schema object for the database the query will be executed on
  372. * @param array $params The parameters for the fDatabase::query() call
  373. * @param string $table The table any ambigious column references will refer to
  374. * @param array $order_bys The array of order bys to use - see fRecordSet::build() for format
  375. * @return array The params with a SQL `ORDER BY` clause added
  376. */
  377. static public function addOrderByClause($db, $schema, $params, $table, $order_bys)
  378. {
  379. $expressions = array();
  380. foreach ($order_bys as $column => $direction) {
  381. if ((!is_string($column) && !is_object($column) && !is_numeric($column)) || !strlen(trim($column))) {
  382. throw new fProgrammerException(
  383. 'An invalid sort column, %s, was specified',
  384. $column
  385. );
  386. }
  387. $direction = strtoupper($direction);
  388. if (!in_array($direction, array('ASC', 'DESC'))) {
  389. throw new fProgrammerException(
  390. 'An invalid direction, %s, was specified',
  391. $direction
  392. );
  393. }
  394. if (preg_match('#^((?:max|min|avg|sum|count)\()?((?:(?:(?:"?\w+"?\.)?"?\w+(?:\{[\w.]+\})?"?=>)?"?(?:(?:\w+"?\."?)?\w+)(?:\{[\w.]+\})?"?\.)?"?(?:\w+)"?)(?:\))?$#D', $column, $matches)) {
  395. // Parse the expression and get a table and column to determine the data type
  396. list ($clause_table, $clause_column) = self::getTableAndColumn($schema, $table, $matches[2]);
  397. $column_type = $schema->getColumnInfo($clause_table, $clause_column, 'type');
  398. // Make sure each column is qualified with a table name
  399. if (strpos($matches[2], '.') === FALSE) {
  400. $matches[2] = $table . '.' . $matches[2];
  401. }
  402. $matches[2] = $db->escape('%r', $matches[2]);
  403. // Text columns are converted to lowercase for more accurate sorting
  404. if (in_array($column_type, array('varchar', 'char', 'text'))) {
  405. $expression = 'LOWER(' . $matches[2] . ')';
  406. } else {
  407. $expression = $matches[2];
  408. }
  409. // If the column is in an aggregate function, add the function back in
  410. if ($matches[1]) {
  411. $expression = $matches[1] . $expression . ')';
  412. }
  413. $expressions[] = $expression . ' ' . $direction;
  414. } else {
  415. $expressions[] = $column . ' ' . $direction;
  416. }
  417. }
  418. $params[0] .= join(', ', $expressions);
  419. return $params;
  420. }
  421. /**
  422. * Add the appropriate SQL and params for a `WHERE` clause condition for primary keys of the table specified
  423. *
  424. * @internal
  425. *
  426. * @param fSchema $schema The schema for the database the query will be run on
  427. * @param array $params The currently constructed params for fDatabase::query() - the first param should be a SQL statement
  428. * @param string $table The table to build the where clause for
  429. * @param string $table_alias The alias for the table
  430. * @param array &$values The values array for the fActiveRecord object
  431. * @param array &$old_values The old values array for the fActiveRecord object
  432. * @return array The params to pass to fDatabase::query(), including the new primary key where condition
  433. */
  434. static public function addPrimaryKeyWhereParams($schema, $params, $table, $table_alias, &$values, &$old_values)
  435. {
  436. $pk_columns = $schema->getKeys($table, 'primary');
  437. $column_info = $schema->getColumnInfo($table);
  438. $conditions = array();
  439. foreach ($pk_columns as $pk_column) {
  440. $value = fActiveRecord::retrieveOld($old_values, $pk_column, $values[$pk_column]);
  441. // This makes sure the query performs the way an insert will
  442. if ($value === NULL && $column_info[$pk_column]['not_null'] && $column_info[$pk_column]['default'] !== NULL) {
  443. $value = $column_info[$pk_column]['default'];
  444. }
  445. $params[] = $table_alias . '.' . $pk_column;
  446. $params[] = $value;
  447. $conditions[] = self::makeCondition($schema, $table, $pk_column, '=', $value);
  448. }
  449. $params[0] .= join(' AND ', $conditions);
  450. return $params;
  451. }
  452. /**
  453. * Adds a `WHERE` clause, from an array of conditions, to the parameters for an fDatabase::query() call
  454. *
  455. * @internal
  456. *
  457. * @param fDatabase $db The database the query will be executed on
  458. * @param fSchema $schema The schema for the database
  459. * @param array $params The parameters for the fDatabase::query() call
  460. * @param string $table The table any ambigious column references will refer to
  461. * @param array $conditions The array of conditions - see fRecordSet::build() for format
  462. * @return array The params with the SQL `WHERE` clause added
  463. */
  464. static public function addWhereClause($db, $schema, $params, $table, $conditions)
  465. {
  466. $i = 0;
  467. foreach ($conditions as $column => $values) {
  468. if ($i) {
  469. $params[0] .= ' AND ';
  470. }
  471. if (in_array(substr($column, -3), array('!=:', '>=:', '<=:', '<>:'))) {
  472. $operator = strtr(
  473. substr($column, -3),
  474. array('<>:' => '!:', '!=:' => '!:')
  475. );
  476. $column = substr($column, 0, -3);
  477. } elseif (in_array(substr($column, -2), array('<=', '>=', '!=', '<>', '!~', '&~', '^~', '$~', '><', '=:', '!:', '<:', '>:'))) {
  478. $operator = strtr(
  479. substr($column, -2),
  480. array('<>' => '!', '!=' => '!')
  481. );
  482. $column = substr($column, 0, -2);
  483. } else {
  484. $operator = substr($column, -1);
  485. $column = substr($column, 0, -1);
  486. }
  487. if (!is_object($values)) {
  488. settype($values, 'array');
  489. } else {
  490. $values = array($values);
  491. }
  492. if (!$values) { $values = array(NULL); }
  493. $new_values = array();
  494. foreach ($values as $value) {
  495. if (is_object($value) && is_callable(array($value, '__toString'))) {
  496. $value = $value->__toString();
  497. } elseif (is_object($value)) {
  498. $value = (string) $value;
  499. }
  500. $new_values[] = $value;
  501. }
  502. $values = $new_values;
  503. // Multi-column condition
  504. if (preg_match('#(?<!\|)\|(?!\|)#', $column)) {
  505. $columns = explode('|', $column);
  506. $operators = array();
  507. foreach ($columns as &$_column) {
  508. if (in_array(substr($_column, -3), array('!=:', '>=:', '<=:', '<>:'))) {
  509. $operators[] = strtr(
  510. substr($_column, -3),
  511. array('<>:' => '!:', '!=:' => '!:')
  512. );
  513. $_column = substr($_column, 0, -3);
  514. } elseif (in_array(substr($_column, -2), array('<=', '>=', '!=', '<>', '!~', '&~', '^~', '$~', '=:', '!:', '<:', '>:'))) {
  515. $operators[] = strtr(
  516. substr($_column, -2),
  517. array('<>' => '!', '!=' => '!')
  518. );
  519. $_column = substr($_column, 0, -2);
  520. } elseif (!ctype_alnum(substr($_column, -1))) {
  521. $operators[] = substr($_column, -1);
  522. $_column = substr($_column, 0, -1);
  523. }
  524. }
  525. $operators[] = $operator;
  526. if (sizeof($operators) == 1) {
  527. // Make sure every column is qualified by a table name
  528. $new_columns = array();
  529. foreach ($columns as $column) {
  530. if (strpos($column, '.') === FALSE) {
  531. $column = $table . '.' . $column;
  532. }
  533. $new_columns[] = $column;
  534. }
  535. $columns = $new_columns;
  536. // Handle fuzzy searches
  537. if ($operator == '~') {
  538. // If the value to search is a single string value, parse it for search terms
  539. if (sizeof($values) == 1 && is_string($values[0])) {
  540. $values = self::parseSearchTerms($values[0], TRUE);
  541. }
  542. // Skip fuzzy matches with no values to match
  543. if ($values === array()) {
  544. $params[0] .= ' 1 = 1 ';
  545. $i++;
  546. continue;
  547. }
  548. $condition = array();
  549. foreach ($values as $value) {
  550. $sub_condition = array();
  551. foreach ($columns as $column) {
  552. $sub_condition[] = $db->escape('%r', $column) . ' LIKE %s';
  553. $params[] = '%' . $value . '%';
  554. }
  555. $condition[] = '(' . join(' OR ', $sub_condition) . ')';
  556. }
  557. $params[0] .= ' (' . join(' AND ', $condition) . ') ';
  558. // Handle intersection
  559. } elseif ($operator == '><') {
  560. if (sizeof($columns) != 2 || sizeof($values) != 2) {
  561. throw new fProgrammerException(
  562. 'The intersection operator, %s, requires exactly two columns and two values',
  563. $operator
  564. );
  565. }
  566. $escaped_columns = array(
  567. $db->escape('%r', $columns[0]),
  568. $db->escape('%r', $columns[1])
  569. );
  570. list($column_1_table, $column_1) = self::getTableAndColumn($schema, $table, $columns[0]);
  571. list($column_2_table, $column_2) = self::getTableAndColumn($schema, $table, $columns[1]);
  572. $placeholders = array(
  573. $schema->getColumnInfo($column_1_table, $column_1, 'placeholder'),
  574. $schema->getColumnInfo($column_2_table, $column_2, 'placeholder')
  575. );
  576. if ($values[1] === NULL) {
  577. $part_1 = '(' . $escaped_columns[1] . ' IS NULL AND ' . $escaped_columns[0] . ' = ' . $placeholders[0] . ')';
  578. $part_2 = '(' . $escaped_columns[1] . ' IS NOT NULL AND ' . $escaped_columns[0] . ' <= ' . $placeholders[0] . ' AND ' . $escaped_columns[1] . ' >= ' . $placeholders[1] . ')';
  579. $params[] = $values[0];
  580. $params[] = $values[0];
  581. $params[] = $values[0];
  582. } else {
  583. $part_1 = '(' . $escaped_columns[0] . ' <= ' . $placeholders[0] . ' AND ' . $escaped_columns[1] . ' >= ' . $placeholders[1] . ')';
  584. $part_2 = '(' . $escaped_columns[0] . ' >= ' . $placeholders[0] . ' AND ' . $escaped_columns[0] . ' <= ' . $placeholders[0] . ')';
  585. $params[] = $values[0];
  586. $params[] = $values[0];
  587. $params[] = $values[0];
  588. $params[] = $values[1];
  589. }
  590. $params[0] .= ' (' . $part_1 . ' OR ' . $part_2 . ') ';
  591. } else {
  592. throw new fProgrammerException(
  593. 'An invalid comparison operator, %s, was specified for multiple columns',
  594. $operator
  595. );
  596. }
  597. // Handle OR combos
  598. } else {
  599. if (sizeof($columns) != sizeof($values)) {
  600. throw new fProgrammerException(
  601. 'When creating an %1$s where clause there must be an equal number of columns and values, however %2$s column(s) and %3$s value(s) were provided',
  602. 'OR',
  603. sizeof($columns),
  604. sizeof($values)
  605. );
  606. }
  607. if (sizeof($columns) != sizeof($operators)) {
  608. throw new fProgrammerException(
  609. 'When creating an %s where clause there must be a comparison operator for each column, however one or more is missing',
  610. 'OR'
  611. );
  612. }
  613. $params[0] .= ' (';
  614. $iterations = sizeof($columns);
  615. for ($j=0; $j<$iterations; $j++) {
  616. if ($j) {
  617. $params[0] .= ' OR ';
  618. }
  619. $params = self::addColumnCondition($db, $schema, $params, $table, $columns[$j], $operators[$j], $values[$j]);
  620. }
  621. $params[0] .= ') ';
  622. }
  623. // Concatenated columns
  624. } elseif (strpos($column, '||') !== FALSE) {
  625. $parts = explode('||', $column);
  626. $new_parts = array();
  627. foreach ($parts as $part) {
  628. $part = trim($part);
  629. if ($part[0] != "'") {
  630. $new_parts[] = $db->escape('%r', $part);
  631. } else {
  632. $new_parts[] = $part;
  633. }
  634. }
  635. $escaped_column = join('||', $new_parts);
  636. $params = self::addColumnCondition($db, $schema, $params, $table, $column, $operator, $values, $escaped_column, '%s');
  637. // Single column condition
  638. } else {
  639. $params = self::addColumnCondition($db, $schema, $params, $table, $column, $operator, $values);
  640. }
  641. $i++;
  642. }
  643. return $params;
  644. }
  645. /**
  646. * Takes a table name, cleans off quoting and removes the schema name if unambiguous
  647. *
  648. * @param fSchema $schema The schema object for the database being inspected
  649. * @param string $table The table name to be made cleaned
  650. * @return string The cleaned table name
  651. */
  652. static private function cleanTableName($schema, $table)
  653. {
  654. $table = str_replace('"', '', $table);
  655. $tables = array_flip($schema->getTables());
  656. if (!isset($tables[$table])) {
  657. $short_table = preg_replace('#^\w\.#', '', $table);
  658. if (isset($tables[$short_table])) {
  659. $table = $short_table;
  660. }
  661. }
  662. return strtolower($table);
  663. }
  664. /**
  665. * Creates a `FROM` clause from a join array
  666. *
  667. * @internal
  668. *
  669. * @param fDatabase $db The database the query will be run on
  670. * @param array $joins The joins to create the `FROM` clause out of
  671. * @return string The from clause (does not include the word `FROM`)
  672. */
  673. static private function createFromClauseFromJoins($db, $joins)
  674. {
  675. $sql = '';
  676. foreach ($joins as $join) {
  677. // Here we handle the first table in a join
  678. if ($join['join_type'] == 'none') {
  679. $sql .= $db->escape('%r', $join['table_name']);
  680. if ($join['table_alias'] != $join['table_name']) {
  681. $sql .= ' ' . $db->escape('%r', $join['table_alias']);
  682. }
  683. // Here we handle all other joins
  684. } else {
  685. $sql .= ' ' . strtoupper($join['join_type']) . ' ' . $db->escape('%r', $join['table_name']);
  686. if ($join['table_alias'] != $join['table_name']) {
  687. $sql .= ' ' . $db->escape('%r', $join['table_alias']);
  688. }
  689. if (!empty($join['on_clause_fields'])) {
  690. $sql .= ' ON ' . $db->escape('%r', $join['on_clause_fields'][0]) . ' = ' . $db->escape('%r', $join['on_clause_fields'][1]);
  691. }
  692. }
  693. }
  694. return $sql;
  695. }
  696. /**
  697. * Creates join information for the table shortcut provided
  698. *
  699. * @internal
  700. *
  701. * @param fSchema $schema The schema object for the tables/joins
  702. * @param string $table The primary table
  703. * @param string $table_alias The primary table alias
  704. * @param string $related_table The related table
  705. * @param string $route The route to the related table
  706. * @param array &$joins The names of the joins that have been created
  707. * @param array &$used_aliases The aliases that have been used
  708. * @return string The name of the significant join created
  709. */
  710. static private function createJoin($schema, $table, $table_alias, $related_table, $route, &$joins, &$used_aliases)
  711. {
  712. $routes = fORMSchema::getRoutes($schema, $table, $related_table);
  713. if (!isset($routes[$route])) {
  714. throw new fProgrammerException(
  715. 'An invalid route, %1$s, was specified for the relationship from %2$s to %3$s',
  716. $route,
  717. $table,
  718. $related_table
  719. );
  720. }
  721. if (isset($joins[$table . '_' . $related_table . '{' . $route . '}'])) {
  722. return $table . '_' . $related_table . '{' . $route . '}';
  723. }
  724. // If the route uses a join table
  725. if (isset($routes[$route]['join_table'])) {
  726. $join = array(
  727. 'join_type' => 'LEFT JOIN',
  728. 'table_name' => $routes[$route]['join_table'],
  729. 'table_alias' => self::createNewAlias($routes[$route]['join_table'], $used_aliases),
  730. 'on_clause_fields' => array()
  731. );
  732. $join2 = array(
  733. 'join_type' => 'LEFT JOIN',
  734. 'table_name' => $related_table,
  735. 'table_alias' => self::createNewAlias($related_table, $used_aliases),
  736. 'on_clause_fields' => array()
  737. );
  738. if ($table != $related_table) {
  739. $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column'];
  740. $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_column'];
  741. $join2['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_related_column'];
  742. $join2['on_clause_fields'][] = $join2['table_alias'] . '.' . $routes[$route]['related_column'];
  743. } else {
  744. $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column'];
  745. $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_related_column'];
  746. $join2['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_column'];
  747. $join2['on_clause_fields'][] = $join2['table_alias'] . '.' . $routes[$route]['related_column'];
  748. }
  749. $joins[$table . '_' . $related_table . '{' . $route . '}_join'] = $join;
  750. $joins[$table . '_' . $related_table . '{' . $route . '}'] = $join2;
  751. // If the route is a direct join
  752. } else {
  753. $join = array(
  754. 'join_type' => 'LEFT JOIN',
  755. 'table_name' => $related_table,
  756. 'table_alias' => self::createNewAlias($related_table, $used_aliases),
  757. 'on_clause_fields' => array()
  758. );
  759. if ($table != $related_table) {
  760. $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column'];
  761. $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['related_column'];
  762. } else {
  763. $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['related_column'];
  764. $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['column'];
  765. }
  766. $joins[$table . '_' . $related_table . '{' . $route . '}'] = $join;
  767. }
  768. return $table . '_' . $related_table . '{' . $route . '}';
  769. }
  770. /**
  771. * Creates a new table alias
  772. *
  773. * @internal
  774. *
  775. * @param string $table The table to create an alias for
  776. * @param array &$used_aliases The aliases that have been used
  777. * @return string The alias to use for the table
  778. */
  779. static private function createNewAlias($table, &$used_aliases)
  780. {
  781. if (!in_array($table, $used_aliases)) {
  782. $used_aliases[] = $table;
  783. return $table;
  784. }
  785. // This will strip any schema name off the beginning
  786. $table = preg_replace('#^\w+\.#', '', $table);
  787. $i = 1;
  788. while(in_array($table . $i, $used_aliases)) {
  789. $i++;
  790. }
  791. $used_aliases[] = $table . $i;
  792. return $table . $i;
  793. }
  794. /**
  795. * Gets the table and column name from a shorthand column name
  796. *
  797. * @param fSchema $schema The schema for the database
  798. * @param string $table The table to use when no table is specified in the shorthand
  799. * @param string $column The shorthand column definition - see fRecordSet::build() for possible syntaxes
  800. * @return array The $table and $column, suitable for use with fSchema
  801. */
  802. static private function getTableAndColumn($schema, $table, $column)
  803. {
  804. // Handle shorthand column names like table.column and table=>related_table.column
  805. if (preg_match('#((?:"?\w+"?\.)?"?\w+)(?:\{[\w.]+\})?"?\."?(\w+)"?$#D', $column, $match)) {
  806. $table = $match[1];
  807. $column = $match[2];
  808. }
  809. $table = self::cleanTableName($schema, $table);
  810. $column = str_replace('"', '', $column);
  811. return array($table, $column);
  812. }
  813. /**
  814. * Finds all of the table names in the SQL and creates the appropriate `FROM` and `GROUP BY` clauses with all necessary joins
  815. *
  816. * The SQL string should contain two placeholders, `:from_clause` and
  817. * `:group_by_clause`, although the later may be omitted if necessary. All
  818. * columns should be qualified with their full table name.
  819. *
  820. * Here is an example SQL string to pass in presumming that the tables
  821. * users and groups are in a relationship:
  822. *
  823. * {{{
  824. * SELECT users.* FROM :from_clause WHERE groups.group_id = 5 :group_by_clause ORDER BY lower(users.first_name) ASC
  825. * }}}
  826. *
  827. * @internal
  828. *
  829. * @param fDatabase $db The database the query is to be executed on
  830. * @param fSchema $schema The schema for the database
  831. * @param array $params The parameters for the fDatabase::query() call
  832. * @param string $table The main table to be queried
  833. * @return array The params with the SQL `FROM` and `GROUP BY` clauses injected
  834. */
  835. static public function injectFromAndGroupByClauses($db, $schema, $params, $table)
  836. {
  837. $table_with_schema = $table;
  838. $table = self::cleanTableName($schema, $table);
  839. $joins = array();
  840. if (strpos($params[0], ':from_clause') === FALSE) {
  841. throw new fProgrammerException(
  842. 'No %1$s placeholder was found in:%2$s',
  843. ':from_clause',
  844. "\n" . $params[0]
  845. );
  846. }
  847. $has_group_by_placeholder = (strpos($params[0], ':group_by_clause') !== FALSE) ? TRUE : FALSE;
  848. // Separate the SQL from quoted values
  849. preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\])*')|(?:[^']+)#", $params[0], $matches);
  850. $table_alias = $table;
  851. $used_aliases = array();
  852. $table_map = array();
  853. // If we are not passing in existing joins, start with the specified table
  854. if (!$joins) {
  855. $joins[] = array(
  856. 'join_type' => 'none',
  857. 'table_name' => $table,
  858. 'table_alias' => $table_alias
  859. );
  860. }
  861. $used_aliases[] = $table_alias;
  862. foreach ($matches[0] as $match) {
  863. if ($match[0] != "'") {
  864. // This removes quotes from around . in the {route} specified of a shorthand column name
  865. $match = preg_replace('#(\{\w+)"\."(\w+\})#', '\1.\2', $match);
  866. preg_match_all('#(?<!\w|"|=>)((?:"?((?:\w+"?\."?)?\w+)(?:\{([\w.]+)\})?"?=>)?("?(?:\w+"?\."?)?\w+)(?:\{([\w.]+)\})?"?)\."?\w+"?(?=[^\w".{])#m', $match, $table_matches, PREG_SET_ORDER);
  867. foreach ($table_matches as $table_match) {
  868. if (!isset($table_match[5])) {
  869. $table_match[5] = NULL;
  870. }
  871. if (!empty($table_match[2])) {
  872. $table_match[2] = self::cleanTableName($schema, $table_match[2]);
  873. }
  874. $table_match[4] = self::cleanTableName($schema, $table_match[4]);
  875. if (in_array($db->getType(), array('oracle', 'db2'))) {
  876. foreach (array(2, 3, 4, 5) as $subpattern) {
  877. if (isset($table_match[$subpattern])) {
  878. $table_match[$subpattern] = strtolower($table_match[$subpattern]);
  879. }
  880. }
  881. }
  882. // This is a related table that is going to join to a once-removed table
  883. if (!empty($table_match[2])) {
  884. $related_table = $table_match[2];
  885. $route = fORMSchema::getRouteName($schema, $table, $related_table, $table_match[3]);
  886. $join_name = $table . '_' . $related_table . '{' . $route . '}';
  887. $once_removed_table = $table_match[4];
  888. // Add the once removed table to the aliases in case we also join directly to it
  889. // which may cause the replacements later in this method to convert first to the
  890. // real table name and then from the real table to the real table's alias
  891. if (!in_array($once_removed_table, $used_aliases)) {
  892. $used_aliases[] = $once_removed_table;
  893. }
  894. self::createJoin($schema, $table, $table_alias, $related_table, $route, $joins, $used_aliases);
  895. $route = fORMSchema::getRouteName($schema, $related_table, $once_removed_table, $table_match[5]);
  896. $join_name = self::createJoin($schema, $related_table, $joins[$join_name]['table_alias'], $once_removed_table, $route, $joins, $used_aliases);
  897. $table_map[$table_match[1]] = $db->escape('%r', $joins[$join_name]['table_alias']);
  898. // Remove the once removed table from the aliases so we also join directly to it without an alias
  899. unset($used_aliases[array_search($once_removed_table, $used_aliases)]);
  900. // This is a related table
  901. } elseif (($table_match[4] != $table || fORMSchema::getRoutes($schema, $table, $table_match[4])) && self::cleanTableName($schema, $table_match[1]) != $table) {
  902. $related_table = $table_match[4];
  903. $route = fORMSchema::getRouteName($schema, $table, $related_table, $table_match[5]);
  904. $join_name = self::createJoin($schema, $table, $table_alias, $related_table, $route, $joins, $used_aliases);
  905. $table_map[$table_match[1]] = $db->escape('%r', $joins[$join_name]['table_alias']);
  906. }
  907. }
  908. }
  909. }
  910. // Determine if we joined a *-to-many relationship
  911. $joined_to_many = FALSE;
  912. foreach ($joins as $name => $join) {
  913. if (is_numeric($name)) {
  914. continue;
  915. }
  916. // Many-to-many uses a join table
  917. if (substr($name, -5) == '_join') {
  918. $joined_to_many = TRUE;
  919. break;
  920. }
  921. $main_table = preg_replace('#_' . $join['table_name'] . '{\w+}$#iD', '', $name);
  922. $second_table = $join['table_name'];
  923. $route = preg_replace('#^[^{]+{([\w.]+)}$#D', '\1', $name);
  924. $routes = fORMSchema::getRoutes($schema, $main_table, $second_table, '*-to-many');
  925. if (isset($routes[$route])) {
  926. $joined_to_many = TRUE;
  927. break;
  928. }
  929. }
  930. $from_clause = self::createFromClauseFromJoins($db, $joins);
  931. // If we are joining on a *-to-many relationship we need to group by the
  932. // columns in the main table to prevent duplicate entries
  933. if ($joined_to_many) {
  934. $column_info = $schema->getColumnInfo($table);
  935. $columns = array();
  936. foreach ($column_info as $column => $info) {
  937. $columns[] = $table . '.' . $column;
  938. }
  939. $group_by_columns = $db->escape('%r ', $columns);
  940. $group_by_clause = ' GROUP BY ' . $group_by_columns;
  941. } else {
  942. $group_by_clause = ' ';
  943. $group_by_columns = '';
  944. }
  945. // Put the SQL back together
  946. $new_sql = '';
  947. $preg_table_pattern = preg_quote($table_with_schema, '#') . '\.|' . preg_quote('"' . trim($table_with_schema, '"') . '"', '#') . '\.';
  948. foreach ($matches[0] as $match) {
  949. $temp_sql = $match;
  950. // Get rid of the => notation and the :from_clause placeholder
  951. if ($match[0] !== "'") {
  952. // This removes quotes from around . in the {route} specified of a shorthand column name
  953. $temp_sql = preg_replace('#(\{\w+)"\."(\w+\})#', '\1.\2', $match);
  954. foreach ($table_map as $arrow_table => $alias) {
  955. $temp_sql = preg_replace('#(?<![\w"])' . preg_quote($arrow_table, '#') . '(?!=[\w"])#', $alias, $temp_sql);
  956. }
  957. // This automatically adds max() around column from other tables when a group by is used
  958. if ($joined_to_many && preg_match('#order\s+by#i', $temp_sql)) {
  959. $order_by_found = TRUE;
  960. $parts = preg_split('#(order\s+by)#i', $temp_sql, -1, PREG_SPLIT_DELIM_CAPTURE);
  961. $parts[2] = preg_replace('#(?<!avg\(|count\(|max\(|min\(|sum\(|cast\(|case |when |"|avg\("|count\("|max\("|min\("|sum\("|cast\("|case "|when "|\{|\.)((?!' . $preg_table_pattern . ')((?:"|\b)\w+"?\.)?(?:"|\b)\w+"?\."?\w+"?)(?![\w."])#i', 'max(\1)', $parts[2]);
  962. $temp_sql = join('', $parts);
  963. }
  964. $temp_sql = str_replace(':from_clause', $from_clause, $temp_sql);
  965. if ($has_group_by_placeholder) {
  966. $temp_sql = preg_replace('#\s:group_by_clause(\s|$)#', strtr($group_by_clause, array('\\' => '\\\\', '$' => '\\$')), $temp_sql);
  967. } elseif ($group_by_columns) {
  968. $temp_sql = preg_replace('#(\sGROUP\s+BY\s((?!HAVING|ORDER\s+BY).)*)\s#i', '\1, ' . strtr($group_by_columns, array('\\' => '\\\\', '$' => '\\$')), $temp_sql);
  969. }
  970. }
  971. $new_sql .= $temp_sql;
  972. }
  973. $params[0] = $new_sql;
  974. return $params;
  975. }
  976. /**
  977. * Makes a condition for a SQL statement out of fDatabase::escape() placeholders
  978. *
  979. * @internal
  980. *
  981. * @param fSchema $schema The schema object for the database the query will be executed on
  982. * @param string $table The table to create the condition for
  983. * @param string $column The column to make the condition for
  984. * @param string $comparison_operator The comparison operator for the condition
  985. * @param mixed $value The value for the condition, which allows the $comparison_operator to be tweaked for NULL values
  986. * @return string A SQL condition using fDatabase::escape() placeholders
  987. */
  988. static public function makeCondition($schema, $table, $column, $comparison_operator, $value)
  989. {
  990. list($table, $column) = self::getTableAndColumn($schema, $table, $column);
  991. $co = strtr($comparison_operator, array('!' => '<>', '!=' => '<>'));
  992. if ($value === NULL) {
  993. if (in_array(trim($co), array('=', 'IN'))) {
  994. $co = 'IS';
  995. } elseif (in_array(trim($co), array('<>', 'NOT IN'))) {
  996. $co = 'IS NOT';
  997. }
  998. }
  999. return '%r ' . $co . ' ' . $schema->getColumnInfo($table, $column, 'placeholder');
  1000. }
  1001. /**
  1002. * Parses a search string into search terms, supports quoted phrases and removes extra punctuation
  1003. *
  1004. * @internal
  1005. *
  1006. * @param string $terms A text string from a form input to parse into search terms
  1007. * @param boolean $ignore_stop_words If stop words should be ignored, this setting will be ignored if all words are stop words
  1008. * @return void
  1009. */
  1010. static public function parseSearchTerms($terms, $ignore_stop_words=FALSE)
  1011. {
  1012. $stop_words = array(
  1013. 'i', 'a', 'an', 'are', 'as', 'at', 'be',
  1014. 'by', 'de', 'en', 'en', 'for', 'from', 'how',
  1015. 'in', 'is', 'it', 'la', 'of', 'on', 'or',
  1016. 'that', 'the', 'this', 'to', 'was', 'what', 'when',
  1017. 'where', 'who', 'will'
  1018. );
  1019. preg_match_all('#(?:"[^"]+"|[^\s]+)#', $terms, $matches);
  1020. $good_terms = array();
  1021. $ignored_terms = array();
  1022. foreach ($matches[0] as $match) {
  1023. // Remove phrases from quotes
  1024. if ($match[0] == '"' && substr($match, -1)) {
  1025. $match = substr($match, 1, -1);
  1026. // Trim any punctuation off of the beginning and end of terms
  1027. } else {
  1028. if (self::$pcre_supports_unicode_character_properties === NULL) {
  1029. fCore::startErrorCapture();
  1030. preg_match('#\pC#u', 'test');
  1031. self::$pcre_supports_unicode_character_properties = !((boolean) fCore::stopErrorCapture());
  1032. }
  1033. if (self::$pcre_supports_unicode_character_properties) {
  1034. $match = preg_replace('#(^[\pC\pC\pM\pP\pS\pZ]+|[\pC\pC\pM\pP\pS\pZ]+$)#iDu', '', $match);
  1035. } else {
  1036. // This just removes ascii non-alphanumeric characters, plus the unicode punctuation and supplemental punctuation blocks
  1037. $match = preg_replace('#(^[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F\x{2000}-\x{206F}\x{2E00}-\x{2E7F}\x{00A1}-\x{00A9}\x{00AB}-\x{00B1}\x{00B4}\x{00B6}-\x{00B8}\x{00BB}\x{00BF}\x{00D7}\x{00F7}]+|[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F\x{2000}-\x{206F}\x{2E00}-\x{2E7F}\x{00A1}-\x{00A9}\x{00AB}-\x{00B1}\x{00B4}\x{00B6}-\x{00B8}\x{00BB}\x{00BF}\x{00D7}\x{00F7}]+$)#iDu', '', $match);
  1038. }
  1039. }
  1040. if ($ignore_stop_words && in_array(strtolower($match), $stop_words)) {
  1041. $ignored_terms[] = $match;
  1042. continue;
  1043. }
  1044. $good_terms[] = $match;
  1045. }
  1046. // If no terms were parsed, that means all words were stop words
  1047. if ($ignored_terms && !$good_terms) {
  1048. $good_terms = $ignored_terms;
  1049. }
  1050. return $good_terms;
  1051. }
  1052. /**
  1053. * Resets the configuration of the class
  1054. *
  1055. * @internal
  1056. *
  1057. * @return void
  1058. */
  1059. static public function reset()
  1060. {
  1061. self::$database_objects = array();
  1062. }
  1063. /**
  1064. * Return the instance of the fDatabase class
  1065. *
  1066. * @param string $class The class to retrieve the database for - if not specified, the default database will be returned
  1067. * @param string $role If the database will be used for `'write'`, `'read'` or `'either'` operations
  1068. * @return fDatabase The database instance
  1069. */
  1070. static public function retrieve($class='fActiveRecord', $role='either')
  1071. {
  1072. if (substr($class, 0, 5) == 'name:') {
  1073. $database_name = substr($class, 5);
  1074. } else {
  1075. $database_name = fORM::getDatabaseName($class);
  1076. }
  1077. if (!isset(self::$database_objects[$database_name])) {
  1078. throw new fProgrammerException(
  1079. 'The database object named "%1$s" has not been attached via %2$s yet',
  1080. $database_name,
  1081. __CLASS__ . '::attach()'
  1082. );
  1083. }
  1084. if ($role == 'write' || $role == 'read') {
  1085. // If the user wants a read database but we are in a transaction on the write database, return
  1086. // the write database to allow for comparing data changed since the transaction started
  1087. if ($role == 'read' && isset(self::$database_objects[$database_name]['write']) && self::$database_objects[$database_name]['write']->isInsideTransaction()) {
  1088. $role = 'write';
  1089. }
  1090. if (!isset(self::$database_objects[$database_name][$role])) {
  1091. throw new fProgrammerException(
  1092. 'The database object named "%1$s" for the %s$2 role has not been attached via %3$s yet',
  1093. $database_name,
  1094. $role,
  1095. __CLASS__ . '::attach()'
  1096. );
  1097. }
  1098. return self::$database_objects[$database_name][$role];
  1099. }
  1100. if (isset(self::$database_objects[$database_name]['write'])) {
  1101. return self::$database_objects[$database_name]['write'];
  1102. } elseif (isset(self::$database_objects[$database_name]['read'])) {
  1103. return self::$database_objects[$database_name]['read'];
  1104. }
  1105. }
  1106. /**
  1107. * Removed aggregate function calls from where conditions array and puts them in a having conditions array
  1108. *
  1109. * @internal
  1110. *
  1111. * @param array &$where_conditions The where conditions to look through for aggregate functions
  1112. * @return array The conditions to be put in a `HAVING` clause
  1113. */
  1114. static public function splitHavingConditions(&$where_conditions)
  1115. {
  1116. $having_conditions = array();
  1117. foreach ($where_conditions as $column => $value) {
  1118. $column_has_aggregate = preg_match('#^(count\(|max\(|avg\(|min\(|sum\()#i', $column);
  1119. $is_column_compare_with_aggregate = substr($column, -1) == ':' && preg_match('#^(count\(|max\(|avg\(|min\(|sum\()#i', $value);
  1120. if ($column_has_aggregate || $is_column_compare_with_aggregate) {
  1121. $having_conditions[$column] = $value;
  1122. unset($where_conditions[$column]);
  1123. }
  1124. }
  1125. return $having_conditions;
  1126. }
  1127. /**
  1128. * Forces use as a static class
  1129. *
  1130. * @return fORMDatabase
  1131. */
  1132. private function __construct() { }
  1133. }
  1134. /**
  1135. * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
  1136. *
  1137. * Permission is hereby granted, free of charge, to any person obtaining a copy
  1138. * of this software and associated documentation files (the "Software"), to deal
  1139. * in the Software without restriction, including without limitation the rights
  1140. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  1141. * copies of the Software, and to permit persons to whom the Software is
  1142. * furnished to do so, subject to the following conditions:
  1143. *
  1144. * The above copyright notice and this permission notice shall be inclu…

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