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

Storage::__construct()   C

Complexity

Conditions 11
Paths 108

Size

Total Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 108
nop 11
dl 0
loc 51
rs 6.869
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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