Test Setup Failed
Push — 17.1 ( 80400e...1bcf53 )
by Ralf
10:23
created

Storage::search2criteria()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 5
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware generalized SQL Storage Object with build in custom field support
4
 *
5
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
6
 * @package api
7
 * @subpackage storage
8
 * @link http://www.egroupware.org
9
 * @author Ralf Becker <[email protected]>
10
 * @copyright 2009-16 by [email protected]
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api;
15
16
/**
17
 * Generalized SQL Storage Object with build in custom field support
18
 *
19
 * This class allows to display, search, order and filter by custom fields simply by replacing Storage\Base
20
 * by it and adding custom field widgets to the eTemplates of an applications.
21
 * It's inspired by the code from Klaus Leithoff, which does the same thing limited to addressbook.
22
 *
23
 * The schema of the custom fields table should be like (the lenght of the cf name is nowhere enfored and
24
 * varies throughout eGW from 40-255, the value column from varchar(255) to longtext!):
25
 *
26
 * 'egw_app_extra' => array(
27
 * 	'fd' => array(
28
 * 		'prefix_id' => array('type' => 'int','precision' => '4','nullable' => False),
29
 * 		'prefix_name' => array('type' => 'string','precision' => '64','nullable' => False),
30
 * 		'prefix_value' => array('type' => 'text'),
31
 * 	),
32
 *  'pk' => array('prefix_id','prefix_name'),
33
 *	'fk' => array(),
34
 *	'ix' => array(),
35
 *	'uc' => array()
36
 * )
37
 *
38
 * @package etemplate
39
 * @subpackage api
40
 * @author RalfBecker-AT-outdoor-training.de
41
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
42
 */
43
class Storage extends Storage\Base
44
{
45
	/**
46
	 * Prefix used by the class
47
	 */
48
	const CF_PREFIX = '#';
49
50
	/**
51
	 * name of customefields table
52
	 *
53
	 * @var string
54
	 */
55
	var $extra_table;
56
57
	/**
58
	 * name of id column, defaults to the regular tables auto id
59
	 *
60
	 * @var string
61
	 */
62
	var $extra_id = '_id';
63
64
	/**
65
	 * Name of key (cf name) column or just a postfix added to the table prefix
66
	 *
67
	 * @var string
68
	 */
69
	var $extra_key = '_name';
70
71
	/**
72
	 * Name of value column or just a postfix added to the table prefix
73
	 *
74
	 * @var string
75
	 */
76
	var $extra_value = '_value';
77
78
	var $extra_join;
79
	var $extra_join_order;
80
	var $extra_join_filter;
81
82
	/**
83
	 * Does extra table has a unique index (over id and name)
84
	 *
85
	 * @var boolean
86
	 */
87
	var $extra_has_unique_index;
88
89
	/**
90
	 * Custom fields of $app, read by the constructor
91
	 *
92
	 * @var array
93
	 */
94
	var $customfields;
95
96
	/**
97
	 * Do we allow AND store multiple values for a cf (1:N) relations
98
	 *
99
	 * @var boolean
100
	 */
101
	var $allow_multiple_values = false;
102
103
	/**
104
	 * constructor of the class
105
	 *
106
	 * Please note the different params compared to Storage\Base!
107
	 *
108
	 * @param string $app application name to load table schemas
109
	 * @param string $table name of the table to use
110
	 * @param string $extra_table name of the custom field table
111
	 * @param string $column_prefix ='' column prefix to automatic remove from the column-name, if the column name starts with it
112
	 * @param string $extra_key ='_name' column name for cf name column (will be prefixed with colum prefix, if starting with _)
113
	 * @param string $extra_value ='_value' column name for cf value column (will be prefixed with colum prefix, if starting with _)
114
	 * @param string $extra_id ='_id' column name for cf id column (will be prefixed with colum prefix, if starting with _)
115
	 * @param Db $db =null database object, if not the one in $GLOBALS['egw']->db should be used, eg. for an other database
116
	 * @param boolean $no_clone =true can we avoid to clone the db-object, default yes (different from Storage\Base!)
117
	 * 	new code using appnames and foreach(select(...,$app) can set it to avoid an extra instance of the db object
118
	 * @param boolean $allow_multiple_values =false should we allow AND store multiple values (1:N relations)
119
	 * @param string $timestamp_type =null default null=leave them as is, 'ts'|'integer' use integer unix timestamps, 'object' use DateTime objects
120
	 */
121
	function __construct($app,$table,$extra_table,$column_prefix='',
122
		$extra_key='_name',$extra_value='_value',$extra_id='_id',
123
		Db $db=null,$no_clone=true,$allow_multiple_values=false,$timestamp_type=null)
124
	{
125
		// calling the Storage\Base constructor
126
		parent::__construct($app,$table,$db,$column_prefix,$no_clone,$timestamp_type);
127
128
		$this->allow_multiple_values = $allow_multiple_values;
129
		$this->extra_table = $extra_table;
130
		if (!$this->extra_id) $this->extra_id = $this->autoinc_id;	// default to auto id of regular table
131
132
		// if names from columns of extra table are only postfixes (starting with _), prepend column prefix
133
		if (!($prefix=$column_prefix))
134
		{
135
			list($prefix) = explode('_',$this->autoinc_id);
136
		}
137
		elseif(substr($prefix,-1) == '_')
138
		{
139
			$prefix = substr($prefix,0,-1);	// remove trailing underscore from column prefix parameter
140
		}
141
		foreach(array(
142
			'extra_id' => $extra_id,
143
			'extra_key' => $extra_key,
144
			'extra_value' => $extra_value
145
		) as $col => $val)
146
		{
147
			$this->$col = $col_name = $val;
148
			if ($col_name[0] == '_') $this->$col = $prefix . $val;
149
		}
150
		// some sanity checks, maybe they should be active only for development
151
		if (!($extra_defs = $this->db->get_table_definitions($app,$extra_table)))
152
		{
153
			throw new Exception\WrongParameter("extra table $extra_table is NOT defined!");
154
		}
155
		foreach(array('extra_id','extra_key','extra_value') as $col)
156
		{
157
			if (!$this->$col || !isset($extra_defs['fd'][$this->$col]))
158
			{
159
				throw new Exception\WrongParameter("$col column $extra_table.{$this->$col} is NOT defined!");
160
			}
161
		}
162
		// check if our extra table has a unique index (if not we have to delete the old values, as replacing does not work!)
163
		$this->extra_has_unique_index = $extra_defs['pk'] || $extra_defs['uc'];
164
165
		// setting up our extra joins, now we know table and column names
166
		$this->extra_join = " LEFT JOIN $extra_table ON $table.$this->autoinc_id=$extra_table.$this->extra_id";
167
		$this->extra_join_order = " LEFT JOIN $extra_table extra_order ON $table.$this->autoinc_id=extra_order.$this->extra_id";
168
		$this->extra_join_filter = " JOIN $extra_table extra_filter ON $table.$this->autoinc_id=extra_filter.$this->extra_id";
169
170
		$this->customfields = Storage\Customfields::get($app, false, null, $db);
171
	}
172
173
	/**
174
	 * Read all customfields of the given id's
175
	 *
176
	 * @param int|array $ids one ore more id's
177
	 * @param array $field_names =null custom fields to read, default all
178
	 * @return array id => $this->cf_field(name) => value
179
	 */
180
	function read_customfields($ids,$field_names=null)
181
	{
182
		if (is_null($field_names)) $field_names = array_keys($this->customfields);
183
184
		foreach((array)$ids as $key => $id)
185
		{
186
			if (!(int)$id && is_array($ids)) unset($ids[$key]);
187
		}
188
		if (!$ids || !$field_names) return array();	// nothing to do
189
190
		$entries = array();
191
		foreach($this->db->select($this->extra_table,'*',array(
192
			$this->extra_id => $ids,
193
			$this->extra_key => $field_names,
194
		),__LINE__,__FILE__,false,'',$this->app) as $row)
195
		{
196
			$entry =& $entries[$row[$this->extra_id]];
197
			if (!is_array($entry)) $entry = array();
198
			$field = $this->get_cf_field($row[$this->extra_key]);
199
200
			if ($this->allow_multiple_values && $this->is_multiple($row[$this->extra_key]))
201
			{
202
				$entry[$field][] = $row[$this->extra_value];
203
			}
204
			else
205
			{
206
				$entry[$field] = $row[$this->extra_value];
207
			}
208
		}
209
		return $entries;
210
	}
211
212
	/**
213
	* saves custom field data
214
	*
215
	* @param array $data data to save (cf's have to be prefixed with self::CF_PREFIX = #)
216
	* @param array $extra_cols =array() extra-data to be saved
217
	* @return bool false on success, errornumber on failure
218
	*/
219
	function save_customfields(&$data, array $extra_cols=array())
220
	{
221
		$id = isset($data[$this->autoinc_id]) ? $data[$this->autoinc_id] : $data[$this->db_key_cols[$this->autoinc_id]];
222
223
		\EGroupware\Api\Storage\Customfields::handle_files($this->app, $id, $data, $this->customfields);
224
225
		foreach (array_keys((array)$this->customfields) as $name)
226
		{
227
			if (!isset($data[$field = $this->get_cf_field($name)])) continue;
228
229
			$where = array(
230
				$this->extra_id    => $id,
231
				$this->extra_key   => $name,
232
			);
233
			$is_multiple = $this->is_multiple($name);
234
235
			// we explicitly need to delete fields, if value is empty or field allows multiple values or we have no unique index
236
			if(empty($data[$field]) || $is_multiple || !$this->extra_has_unique_index)
237
			{
238
				$this->db->delete($this->extra_table,$where,__LINE__,__FILE__,$this->app);
239
				if (empty($data[$field])) continue;	// nothing else to do for empty values
240
			}
241
			foreach($is_multiple && !is_array($data[$field]) ? explode(',',$data[$field]) :
242
				// regular custom fields (!$is_multiple) eg. addressbook store multiple values comma-separated
243
				(array)(!$is_multiple && is_array($data[$field]) ? implode(',', $data[$field]) : $data[$field]) as $value)
244
			{
245
				if (!$this->db->insert($this->extra_table,array($this->extra_value => $value)+$extra_cols,$where,__LINE__,__FILE__,$this->app))
246
				{
247
					return $this->db->Errno;
248
				}
249
			}
250
		}
251
		return false;	// no error
252
	}
253
254
	/**
255
	 * merges in new values from the given new data-array
256
	 *
257
	 * reimplemented to also merge the customfields
258
	 *
259
	 * @param $new array in form col => new_value with values to set
260
	 */
261
	function data_merge($new)
262
	{
263
		parent::data_merge($new);
264
265
		if ($this->customfields)
266
		{
267
			foreach(array_keys($this->customfields) as $name)
268
			{
269
				if (isset($new[$field = $this->get_cf_field($name)]))
270
				{
271
					$this->data[$field] = $new[$field];
272
				}
273
			}
274
		}
275
	}
276
277
	/**
278
	 * reads row matched by key and puts all cols in the data array
279
	 *
280
	 * reimplented to also read the custom fields
281
	 *
282
	 * @param array $keys array with keys in form internalName => value, may be a scalar value if only one key
283
	 * @param string|array $extra_cols string or array of strings to be added to the SELECT, eg. "count(*) as num"
284
	 * @param string $join sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
285
	 * @return array|boolean data if row could be retrived else False
286
	 */
287
	function read($keys,$extra_cols='',$join='')
288
	{
289
		if (!parent::read($keys,$extra_cols,$join))
290
		{
291
			return false;
292
		}
293
		if (($id = (int)$this->data[$this->db_key_cols[$this->autoinc_id]]) && $this->customfields &&
294
			($cfs = $this->read_customfields($id)))
295
		{
296
			$this->data = array_merge($this->data,$cfs[$id]);
297
		}
298
		return $this->data;
299
	}
300
301
	/**
302
	 * saves the content of data to the db
303
	 *
304
	 * reimplented to also save the custom fields
305
	 *
306
	 * @param array $keys if given $keys are copied to data before saveing => allows a save as
307
	 * @param string|array $extra_where =null extra where clause, eg. to check an etag, returns true if no affected rows!
308
	 * @return int|boolean 0 on success, or errno != 0 on error, or true if $extra_where is given and no rows affected
309
	 */
310
	function save($keys=null,$extra_where=null)
311
	{
312
		if (is_array($keys) && count($keys) && !isset($keys[0]))	// allow to use an etag, eg array('etag=etag+1')
313
		{
314
			$this->data_merge($keys);
315
			$keys = null;
316
		}
317
		$ret = parent::save($keys,$extra_where);
318
319
		if ($ret == 0 && $this->customfields)
320
		{
321
			$this->save_customfields($this->data);
322
		}
323
		return $ret;
324
	}
325
326
	/**
327
	 * deletes row representing keys in internal data or the supplied $keys if != null
328
	 *
329
	 * reimplented to also delete the custom fields
330
	 *
331
	 * @param array|int $keys =null if given array with col => value pairs to characterise the rows to delete, or integer autoinc id
332
	 * @param boolean $only_return_ids =false return $ids of delete call to db object, but not run it (can be used by extending classes!)
333
	 * @return int|array affected rows, should be 1 if ok, 0 if an error or array with id's if $only_return_ids
334
	 */
335
	function delete($keys=null,$only_return_ids=false)
336
	{
337
		if ($this->customfields || $only_return_ids)
338
		{
339
			$query = parent::delete($keys,true);
340
			// check if query contains more then the id's
341
			if (!isset($query[$this->autoinc_id]) || count($query) != 1)
342
			{
343
				foreach($this->db->select($this->table_name,$this->autoinc_id,$query,__LINE__,__FILE__,false,'',$this->app) as $row)
344
				{
345
					$ids[] = $row[$this->autoinc_id];
346
				}
347
				if (!$ids) return 0;	// no rows affected
348
			}
349
			else
350
			{
351
				$ids = (array)$query[$this->autoinc_id];
352
			}
353
			if ($only_return_ids) return $ids;
354
			$this->db->delete($this->extra_table,array($this->extra_id => $ids),__LINE__,__FILE__);
355
		}
356
		return parent::delete($keys);
357
	}
358
359
	/**
360
	 * query rows for the nextmatch widget
361
	 *
362
	 * Reimplemented to also read the custom fields (if enabled via $query['selectcols']).
363
	 *
364
	 * Please note: the name of the nextmatch-customfields has to be 'customfields'!
365
	 *
366
	 * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter'
367
	 *	For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class.
368
	 * @param array &$rows returned rows/competitions
369
	 * @param array &$readonlys eg. to disable buttons based on acl, not use here, maybe in a derived class
370
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
371
	 *	"LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
372
	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
373
	 * @param mixed $only_keys =false, see search
374
	 * @param string|array $extra_cols =array()
375
	 * @return int total number of rows
376
	 */
377
	function get_rows($query,&$rows,&$readonlys,$join='',$need_full_no_count=false,$only_keys=false,$extra_cols=array())
378
	{
379
		parent::get_rows($query,$rows,$readonlys,$join,$need_full_no_count,$only_keys,$extra_cols);
380
381
		$selectcols = $query['selectcols'] ? explode(',',$query['selectcols']) : array();
382
383
		if ($rows && $this->customfields && (!$selectcols || in_array('customfields',$selectcols)))
384
		{
385
			$id2keys = array();
386
			foreach($rows as $key => $row)
387
			{
388
				$id2keys[$row[$this->db_key_cols[$this->autoinc_id]]] = $key;
389
			}
390
			// check if only certain cf's to show
391
			if (!in_array('customfields', $selectcols))
392
			{
393
				foreach($selectcols as $col)
394
				{
395
					if ($this->is_cf($col)) $fields[] = $this->get_cf_name($col);
396
				}
397
			}
398
			if (($cfs = $this->read_customfields(array_keys($id2keys),$fields)))
399
			{
400
				foreach($cfs as $id => $data)
401
				{
402
					$rows[$id2keys[$id]] = array_merge($rows[$id2keys[$id]],$data);
403
				}
404
			}
405
		}
406
		return $this->total;
407
	}
408
409
	/**
410
	 * Return criteria array for a given search pattern
411
	 *
412
	 * Reimplemented to handle search in custom-fields by ORing with a sub-query
413
	 * returning all auto-ids of custom-fields matching the search-criteria
414
	 *
415
	 * @param string $_pattern search pattern incl. * or ? as wildcard, if no wildcards used we append and prepend one!
416
	 * @param string &$wildcard ='' on return wildcard char to use, if pattern does not already contain wildcards!
417
	 * @param string &$op ='AND' on return boolean operation to use, if pattern does not start with ! we use OR else AND
418
	 * @param string $extra_col =null extra column to search
419
	 * @param array $search_cols =array() List of columns to search.  If not provided, all columns in $this->db_cols will be considered
420
	 * @return array or column => value pairs
421
	 */
422
	public function search2criteria($_pattern,&$wildcard='',&$op='AND',$extra_col=null, $search_cols = array())
423
	{
424
		$pattern = $wildcard.$_pattern.$wildcard;
425
426
		$criteria = parent::search2criteria($_pattern, $wildcard, $op, $extra_col, $search_cols);
427
428
		$criteria[0] = '('.$criteria[0].' OR '.
429
			$this->table_name.'.'.$this->autoinc_id.' IN (SELECT '.$this->autoinc_id.
430
			' FROM '.$this->extra_table.' WHERE '.$this->extra_value.' '.
431
			$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.
432
			$GLOBALS['egw']->db->quote($pattern).'))';
433
434
		return $criteria;
435
	}
436
437
	/**
438
	 * searches db for rows matching searchcriteria
439
	 *
440
	 * Reimplemented to search, order and filter by custom fields
441
	 *
442
	 * @param array|string $criteria array of key and data cols, OR string with search pattern (incl. * or ? as wildcards)
443
	 * @param boolean|string/array $only_keys =true True returns only keys, False returns all cols. or
444
	 *	comma seperated list or array of columns to return
445
	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
446
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
447
	 * @param string $wildcard ='' appended befor and after each criteria
448
	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
449
	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
450
	 * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num), or 'UNION' for a part of a union query
451
	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
452
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. "JOIN table2 ON x=y" or
453
	 *	"LEFT JOIN table2 ON (x=y AND z=o)", Note: there's no quoting done on $join, you are responsible for it!!!
454
	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
455
	 * @return array|NULL array of matching rows (the row is an array of the cols) or NULL
456
	 */
457
	function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='',$need_full_no_count=false)
458
	{
459
		//error_log(__METHOD__.'('.array2string(array_combine(array_slice(array('criteria','only_keys','order_by','extra_cols','wildcard','empty','op','start','filter','join','need_full_no_count'), 0, count(func_get_args())), func_get_args())).')');
460
461
		// if no CFs are defined OR used and became unavailable (deleted or permissions changed)
462
		if (!$this->customfields && strpos($order_by, self::CF_PREFIX) === false &&
463
			(empty($filter) || strpos(implode(',', array_keys($filter)), self::CF_PREFIX) === false))
464
		{
465
			return parent::search($criteria,$only_keys,$order_by,$extra_cols,$wildcard,$empty,$op,$start,$filter,$join,$need_full_no_count);
466
		}
467
		if ($only_keys === false)
468
		{
469
			$only_keys = $this->table_name.'.*';
470
		}
471
		$extra_join_added = $join && strpos($join, $this->extra_join) !== false;
472
		if ($criteria && is_string($criteria))
473
		{
474
			$extra_join_added = true;	// we have NOT added the join, as we use a sub-query and therefore not need it
475
476
			$criteria = $this->search2criteria($criteria, $wildcard, $op);
477
		}
478
		if ($criteria && is_array($criteria))
479
		{
480
			// check if we search in the custom fields
481
			if (isset($criteria[$this->extra_value]))
482
			{
483
				// we should check if the CF is (still) available, but that makes the slow search even slower :(
484
				if (($negate = $criteria[$this->extra_value][0] === '!'))
485
				{
486
					$criteria[$this->extra_value] = substr($criteria[$this->extra_value],1);
487
				}
488
				$criteria[] = $this->extra_table.'.'.$this->extra_value . ' ' .($negate ? 'NOT ' : '').
489
					$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE]. ' ' .
490
					$this->db->quote($wildcard.$criteria[$this->extra_value].$wildcard);
491
				unset($criteria[$this->extra_value]);
492
			}
493
			// replace ambiguous auto-id with (an exact match of) table_name.autoid
494
			if (isset($criteria[$this->autoinc_id]))
495
			{
496 View Code Duplication
				if ($criteria[$this->autoinc_id])
497
				{
498
					$criteria[] = $this->db->expression($this->table_name,$this->table_name.'.',
499
						array($this->autoinc_id => $criteria[$this->autoinc_id]));
500
				}
501
				unset($criteria[$this->autoinc_id]);
502
			}
503
			// replace ambiguous column with (an exact match of) table_name.column
504
			foreach($criteria as $name => $val)
505
			{
506
				// only add extra_join, if we really need it
507
				if (!$extra_join_added && (
508
					is_int($name) && strpos($val, $this->extra_value) !== false ||
509
					is_string($name) && $this->is_cf($name)
510
				))
511
				{
512
					$join .= $this->extra_join;
513
					$extra_join_added = true;
514
				}
515
				$extra_columns = $this->db->get_table_definitions($this->app, $this->extra_table);
516
				if(is_string($name) && $extra_columns['fd'][array_search($name, $this->db_cols)])
517
				{
518
					$criteria[] = $this->db->expression($this->table_name,$this->table_name.'.',array(
519
						array_search($name, $this->db_cols) => $val,
520
					));
521
					unset($criteria[$name]);
522
				}
523
				elseif (is_string($name) && $this->is_cf($name))
524
				{
525
					if ($op != 'AND')
526
					{
527
						$name = substr($name, 1);
528
						if (($negate = $criteria[$name][0] === '!'))
529
						{
530
							$val = substr($val,1);
531
						}
532
						$cfcriteria[] = '(' . $this->extra_table.'.'.$this->extra_value . ' ' .($negate ? 'NOT ' : '').
533
							$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE]. ' ' .
534
							$this->db->quote($wildcard.$val.$wildcard) . ' AND ' .
535
							$this->extra_table.'.'.$this->extra_key . ' = ' . $this->db->quote($name) .
536
							')';
537
						unset($criteria[self::CF_PREFIX.$name]);
538
					}
539
					else
540
					{
541
						// criteria operator is AND we remap the criteria to be transformed to filters
542
						$filter[$name] = $val;
543
						unset($criteria[$name]);
544
					}
545
				}
546
			}
547
			if ($cfcriteria && $op =='OR') $criteria[] = implode(' OR ',$cfcriteria);
548
		}
549
		if($only_keys === true)
550
		{
551
			// Expand to keys here, so table_name can be prepended below
552
			$only_keys = array_values($this->db_key_cols);
553
		}
554
		// replace ambiguous column with (an exact match of) table_name.column
555
		if(is_array($only_keys))
556
		{
557
			foreach($only_keys as $key => &$col)
558
			{
559
				if(is_numeric($key) && in_array($col, $this->db_cols, true))
560
				{
561
					$col = $this->table_name .'.'.array_search($col, $this->db_cols).' AS '.$col;
562
				}
563
			}
564
		}
565
		// check if we order by a custom field --> join cf table for given cf and order by it's value
566
		if (strpos($order_by,self::CF_PREFIX) !== false)
567
		{
568
			// if $order_by contains more then order by columns (eg. group by) split it off before
569
			if (($pos = stripos($order_by, 'order by')) !== false)
570
			{
571
				$group_by = substr($order_by, 0, $pos+9);
572
				$order_by = substr($order_by, $pos+9);
573
			}
574
			// fields to order by, as cutomfields may have names with spaces, we examine each order by criteria
575
			$fields2order = explode(',',$order_by);
576
			foreach($fields2order as $v)
577
			{
578
				if (strpos($v,self::CF_PREFIX) !== false)
579
				{
580
					// we found a customfield, so we split that part by space char in order to get Sorting Direction and Fieldname
581
					$buff = explode(' ',trim($v));
582
					$orderDir = array_pop($buff);
583
					$key = substr(trim(implode(' ',$buff)), 1);
584
					if (!isset($this->customfields[$key]))
585
					{
586
						$order_by = preg_replace('/'.preg_quote($v, '/').',?/', '', $order_by);
587
						continue;	// ignore unavaiable CF
588
					}
589
					switch($this->customfields[$key]['type'])
590
					{
591 View Code Duplication
						case 'int':
592
							$order_by = str_replace($v, 'extra_order.'.$this->extra_value.' IS NULL,'.
593
								$this->db->to_int('extra_order.'.$this->extra_value).' '.$orderDir, $order_by);
594
							break;
595 View Code Duplication
						case 'float':
596
							$order_by = str_replace($v, 'extra_order.'.$this->extra_value.' IS NULL,'.
597
								$this->db->to_double('extra_order.'.$this->extra_value).' '.$orderDir, $order_by);
598
							break;
599
						default:
600
							$order_by = str_replace($v, 'extra_order.'.$this->extra_value.' IS NULL,extra_order.'.
601
								$this->extra_value.' '.$orderDir, $order_by);
602
					}
603
					// postgres requires that expressions in order by appear in the columns of a distinct select
604
					if ($this->db->Type != 'mysql')
605
					{
606
						if (!is_array($extra_cols))
607
						{
608
							$extra_cols = $extra_cols ? explode(',', $extra_cols) : array();
609
						}
610
						$extra_cols[] = 'extra_order.'.$this->extra_value;
611
						$extra_cols[] = 'extra_order.'.$this->extra_value.' IS NULL';
612
					}
613
					$join .= $this->extra_join_order.' AND extra_order.'.$this->extra_key.'='.$this->db->quote($key);
614
				}
615
			}
616
			// add group by again
617
			if (isset($group_by))
618
			{
619
				$order_by = $group_by.$order_by;
620
			}
621
		}
622
		// check if we filter by a custom field
623
		if (is_array($filter))
624
		{
625
			$_cfnames = array_keys($this->customfields);
626
			$extra_filter = null;
627
			foreach($filter as $name => $val)
628
			{
629
				// replace ambiguous auto-id with (an exact match of) table_name.autoid
630
				if (is_string($name) && $name == $this->autoinc_id)
631
				{
632 View Code Duplication
					if ((int)$filter[$this->autoinc_id])
633
					{
634
						$filter[] = $this->db->expression($this->table_name,$this->table_name.'.',array(
635
							$this->autoinc_id => $filter[$this->autoinc_id],
636
						));
637
					}
638
					unset($filter[$this->autoinc_id]);
639
				}
640
				// replace ambiguous column with (an exact match of) table_name.column
641
				elseif (is_string($name) && $val!=null && in_array($name, $this->db_cols))
642
				{
643
					$extra_columns = $this->db->get_table_definitions($this->app, $this->extra_table);
644
					if ($extra_columns['fd'][array_search($name, $this->db_cols)])
645
					{
646
						$filter[] = $this->db->expression($this->table_name,$this->table_name.'.',array(
647
							array_search($name, $this->db_cols) => $val,
648
						));
649
						unset($filter[$name]);
650
					}
651
				}
652
				elseif (is_string($name) && $this->is_cf($name))
653
				{
654
					$cf_name = $this->get_cf_name($name);
655
					if (!isset($this->customfields[$cf_name]))
656
					{
657
						unset($filter[$name]);
658
						continue;	// ignore unavailable CF
659
					}
660
					if (!empty($val))	// empty -> dont filter
661
					{
662
						if ($val[0] === '!')	// negative filter
663
						{
664
							$sql_filter = 'extra_filter.'.$this->extra_value.'!='.$this->db->quote(substr($val,1));
665
						}
666
						else	// using Db::expression to allow to use array() with possible values or NULL
667
						{
668
							if($this->customfields[$cf_name]['type'] == 'select' &&
669
								$this->customfields[$cf_name]['rows'] > 1)
670
							{
671
								// Multi-select - any entry with the filter value selected matches
672
								$sql_filter = str_replace($this->extra_value,'extra_filter.'.
673
									$this->extra_value,$this->db->expression($this->extra_table,array(
674
										$this->db->concat("','",$this->extra_value,"','").' '.$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote('%,'.$val.',%')
675
									))
676
								);
677
							}
678
							elseif ($this->customfields[$cf_name]['type'] == 'text')
679
							{
680
								$sql_filter = str_replace($this->extra_value,'extra_filter.'.$this->extra_value,
681
										$this->db->expression($this->extra_table,array(
682
										$this->extra_value.' '.$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($wildcard.$val.$wildcard)
683
									))
684
								);
685
							}
686
							else
687
							{
688
								$sql_filter = str_replace($this->extra_value,'extra_filter.'.
689
									$this->extra_value,$this->db->expression($this->extra_table,array($this->extra_value => $val)));
690
							}
691
						}
692
						// need to use a LEFT JOIN for negative search or to allow NULL values
693
						$need_left_join = $val[0] === '!' || strpos($sql_filter,'IS NULL') !== false ? ' LEFT ' : '';
694
						$join .= str_replace('extra_filter','extra_filter'.$extra_filter,$need_left_join.$this->extra_join_filter.
695
							' AND extra_filter.'.$this->extra_key.'='.$this->db->quote($cf_name).
696
							' AND '.$sql_filter);
697
						++$extra_filter;
698
					}
699
					unset($filter[$name]);
700
				}
701
				elseif(is_int($name) && $this->is_cf($val))	// lettersearch: #cfname LIKE 's%'
702
				{
703
					$_cf = explode(' ',$val);
704
					foreach($_cf as $cf_np)
705
					{
706
						// building cf_name by glueing parts together (, in case someone used whitespace in their custom field names)
707
						$tcf_name = ($tcf_name?$tcf_name.' ':'').$cf_np;
708
						// reacts on the first one found that matches an existing customfield, should be better then the old behavior of
709
						// simply splitting by " " and using the first part
710
						if ($this->is_cf($tcf_name) && ($cfn = $this->get_cf_name($tcf_name)) && array_search($cfn,(array)$_cfnames,true)!==false )
711
						{
712
							$cf = $tcf_name;
713
							break;
714
						}
715
					}
716
					unset($filter[$name]);
717
					$cf_name = $this->get_cf_name($cf);
718
					if (!isset($this->customfields[$cf_name])) continue;	// ignore unavailable CF
719
					$join .= str_replace('extra_filter','extra_filter'.$extra_filter,$this->extra_join_filter.
720
						' AND extra_filter.'.$this->extra_key.'='.$this->db->quote($cf_name).
721
						' AND '.str_replace($cf,'extra_filter.'.$this->extra_value,$val));
722
					++$extra_filter;
723
				}
724
			}
725
		}
726
		// add DISTINCT as by joining custom fields for search a row can be returned multiple times
727
		if ($join && strpos($join, $this->extra_join) !== false)
728
		{
729
			if (is_array($only_keys))
730
			{
731
				$only_keys = array_values($only_keys);
732
				$only_keys[0] = 'DISTINCT '.($only_keys[0] != $this->autoinc_id ? $only_keys[0] :
733
					$this->table_name.'.'.$this->autoinc_id.' AS '.$this->autoinc_id);
734
			}
735
			else
736
			{
737
				$only_keys = 'DISTINCT '.$only_keys;
738
			}
739
		}
740
		return parent::search($criteria,$only_keys,$order_by,$extra_cols,$wildcard,$empty,$op,$start,$filter,$join,$need_full_no_count);
741
	}
742
743
	/**
744
	 * Function to test if $field is a custom field: check for the prefix
745
	 *
746
	 * @param string $field
747
	 * @return boolean true if $name is a custom field, false otherwise
748
	 */
749
	function is_cf($field)
750
	{
751
		return $field[0] == self::CF_PREFIX;
752
	}
753
754
	/**
755
	 * Get name part from a custom field: remove the prefix
756
	 *
757
	 * @param string $field
758
	 * @return string name without prefix
759
	 */
760
	function get_cf_name($field)
761
	{
762
		return substr($field,1);
763
	}
764
765
	/**
766
	 * Get the field-name from the name of a custom field: prepend the prefix
767
	 *
768
	 * @param string $name
769
	 * @return string prefix-name
770
	 */
771
	function get_cf_field($name)
772
	{
773
		return self::CF_PREFIX.$name;
774
	}
775
776
	/**
777
	 * Check if cf is stored as 1:N relation in DB and array in memory
778
	 *
779
	 * @param string $name
780
	 * @return string
781
	 */
782
	function is_multiple($name)
783
	{
784
		return $this->allow_multiple_values && in_array($this->customfields[$name]['type'],array('select','select-account')) &&
785
			$this->customfields[$name]['rows'] > 1;
786
	}
787
}
788