PageRenderTime 64ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/base/lib/flourishlib/fORMDatabase.php

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