Storage::process_search()   F
last analyzed

Complexity

Conditions 78
Paths > 20000

Size

Total Lines 282
Code Lines 151

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 78
eloc 151
nc 39680
nop 8
dl 0
loc 282
rs 0
c 2
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
use EGroupware\Api\Storage\Customfields;
17
18
/**
19
 * Generalized SQL Storage Object with build in custom field support
20
 *
21
 * This class allows to display, search, order and filter by custom fields simply by replacing Storage\Base
22
 * by it and adding custom field widgets to the eTemplates of an applications.
23
 * It's inspired by the code from Klaus Leithoff, which does the same thing limited to addressbook.
24
 *
25
 * The schema of the custom fields table should be like (the lenght of the cf name is nowhere enfored and
26
 * varies throughout eGW from 40-255, the value column from varchar(255) to longtext!):
27
 *
28
 * 'egw_app_extra' => array(
29
 * 	'fd' => array(
30
 * 		'prefix_id' => array('type' => 'int','precision' => '4','nullable' => False),
31
 * 		'prefix_name' => array('type' => 'string','precision' => '64','nullable' => False),
32
 * 		'prefix_value' => array('type' => 'text'),
33
 * 	),
34
 *  'pk' => array('prefix_id','prefix_name'),
35
 *	'fk' => array(),
36
 *	'ix' => array(),
37
 *	'uc' => array()
38
 * )
39
 *
40
 * @package etemplate
41
 * @subpackage api
42
 * @author RalfBecker-AT-outdoor-training.de
43
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
44
 */
45
class Storage extends Storage\Base
46
{
47
	/**
48
	 * Prefix used by the class
49
	 */
50
	const CF_PREFIX = '#';
51
52
	/**
53
	 * name of customefields table
54
	 *
55
	 * @var string
56
	 */
57
	var $extra_table;
58
59
	/**
60
	 * name of id column, defaults to the regular tables auto id
61
	 *
62
	 * @var string
63
	 */
64
	var $extra_id = '_id';
65
66
	/**
67
	 * Name of key (cf name) column or just a postfix added to the table prefix
68
	 *
69
	 * @var string
70
	 */
71
	var $extra_key = '_name';
72
73
	/**
74
	 * Name of value column or just a postfix added to the table prefix
75
	 *
76
	 * @var string
77
	 */
78
	var $extra_value = '_value';
79
80
	var $extra_join;
81
	var $extra_join_order;
82
	var $extra_join_filter;
83
84
	/**
85
	 * Does extra table has a unique index (over id and name)
86
	 *
87
	 * @var boolean
88
	 */
89
	var $extra_has_unique_index;
90
91
	/**
92
	 * Custom fields of $app, read by the constructor
93
	 *
94
	 * @var array
95
	 */
96
	var $customfields;
97
98
	/**
99
	 * Do we allow AND store multiple values for a cf (1:N) relations
100
	 *
101
	 * @var boolean
102
	 */
103
	var $allow_multiple_values = false;
104
105
	/**
106
	 * constructor of the class
107
	 *
108
	 * Please note the different params compared to Storage\Base!
109
	 *
110
	 * @param string $app application name to load table schemas
111
	 * @param string $table name of the table to use
112
	 * @param string $extra_table name of the custom field table
113
	 * @param string $column_prefix ='' column prefix to automatic remove from the column-name, if the column name starts with it
114
	 * @param string $extra_key ='_name' column name for cf name column (will be prefixed with colum prefix, if starting with _)
115
	 * @param string $extra_value ='_value' column name for cf value column (will be prefixed with colum prefix, if starting with _)
116
	 * @param string $extra_id ='_id' column name for cf id column (will be prefixed with colum prefix, if starting with _)
117
	 * @param Db $db =null database object, if not the one in $GLOBALS['egw']->db should be used, eg. for an other database
118
	 * @param boolean $no_clone =true can we avoid to clone the db-object, default yes (different from Storage\Base!)
119
	 * 	new code using appnames and foreach(select(...,$app) can set it to avoid an extra instance of the db object
120
	 * @param boolean $allow_multiple_values =false should we allow AND store multiple values (1:N relations)
121
	 * @param string $timestamp_type =null default null=leave them as is, 'ts'|'integer' use integer unix timestamps, 'object' use DateTime objects
122
	 */
123
	function __construct($app,$table,$extra_table,$column_prefix='',
124
		$extra_key='_name',$extra_value='_value',$extra_id='_id',
125
		Db $db=null,$no_clone=true,$allow_multiple_values=false,$timestamp_type=null)
126
	{
127
		// calling the Storage\Base constructor
128
		parent::__construct($app,$table,$db,$column_prefix,$no_clone,$timestamp_type);
129
130
		$this->allow_multiple_values = $allow_multiple_values;
131
		$this->extra_table = $extra_table;
132
		if (!$this->extra_id) $this->extra_id = $this->autoinc_id;	// default to auto id of regular table
133
134
		// if names from columns of extra table are only postfixes (starting with _), prepend column prefix
135
		if (!($prefix=$column_prefix))
136
		{
137
			list($prefix) = explode('_',$this->autoinc_id);
138
		}
139
		elseif(substr($prefix,-1) == '_')
140
		{
141
			$prefix = substr($prefix,0,-1);	// remove trailing underscore from column prefix parameter
142
		}
143
		foreach(array(
144
			'extra_id' => $extra_id,
145
			'extra_key' => $extra_key,
146
			'extra_value' => $extra_value
147
		) as $col => $val)
148
		{
149
			$this->$col = $col_name = $val;
150
			if ($col_name[0] == '_') $this->$col = $prefix . $val;
151
		}
152
		// some sanity checks, maybe they should be active only for development
153
		if (!($extra_defs = $this->db->get_table_definitions($app,$extra_table)))
154
		{
155
			throw new Exception\WrongParameter("extra table $extra_table is NOT defined!");
156
		}
157
		foreach(array('extra_id','extra_key','extra_value') as $col)
158
		{
159
			if (!$this->$col || !isset($extra_defs['fd'][$this->$col]))
160
			{
161
				throw new Exception\WrongParameter("$col column $extra_table.{$this->$col} is NOT defined!");
162
			}
163
		}
164
		// check if our extra table has a unique index (if not we have to delete the old values, as replacing does not work!)
165
		$this->extra_has_unique_index = $extra_defs['pk'] || $extra_defs['uc'];
166
167
		// setting up our extra joins, now we know table and column names
168
		$this->extra_join = " LEFT JOIN $extra_table ON $table.$this->autoinc_id=$extra_table.$this->extra_id";
169
		$this->extra_join_order = " LEFT JOIN $extra_table extra_order ON $table.$this->autoinc_id=extra_order.$this->extra_id";
170
		$this->extra_join_filter = " JOIN $extra_table extra_filter ON $table.$this->autoinc_id=extra_filter.$this->extra_id";
171
172
		$this->customfields = Storage\Customfields::get($app, false, null, $db);
173
	}
174
175
	/**
176
	 * Read all customfields of the given id's
177
	 *
178
	 * @param int|array $ids one ore more id's
179
	 * @param array $field_names =null custom fields to read, default all
180
	 * @return array id => $this->cf_field(name) => value
181
	 */
182
	function read_customfields($ids,$field_names=null)
183
	{
184
		if (is_null($field_names)) $field_names = array_keys($this->customfields);
185
186
		foreach((array)$ids as $key => $id)
187
		{
188
			if (!(int)$id && is_array($ids)) unset($ids[$key]);
189
		}
190
		if (!$ids || !$field_names) return array();	// nothing to do
0 ignored issues
show
Bug Best Practice introduced by
The expression $field_names of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
191
192
		$entries = array();
193
		foreach($this->db->select($this->extra_table,'*',array(
194
			$this->extra_id => $ids,
195
			$this->extra_key => $field_names,
196
		),__LINE__,__FILE__,false,'',$this->app) as $row)
197
		{
198
			$entry =& $entries[$row[$this->extra_id]];
199
			if (!is_array($entry)) $entry = array();
200
			$field = $this->get_cf_field($row[$this->extra_key]);
201
202
			if ($this->allow_multiple_values && $this->is_multiple($row[$this->extra_key]))
203
			{
204
				$entry[$field][] = $row[$this->extra_value];
205
			}
206
			else
207
			{
208
				$entry[$field] = $row[$this->extra_value];
209
			}
210
		}
211
		return $entries;
212
	}
213
214
	/**
215
	* saves custom field data
216
	*
217
	* @param array $data data to save (cf's have to be prefixed with self::CF_PREFIX = #)
218
	* @param array $extra_cols =array() extra-data to be saved
219
	* @return bool false on success, errornumber on failure
220
	*/
221
	function save_customfields(&$data, array $extra_cols=array())
222
	{
223
		$id = isset($data[$this->autoinc_id]) ? $data[$this->autoinc_id] : $data[$this->db_key_cols[$this->autoinc_id]];
224
225
		Customfields::handle_files($this->app, $id, $data, $this->customfields);
226
227
		foreach (array_keys((array)$this->customfields) as $name)
228
		{
229
			if (!isset($data[$field = $this->get_cf_field($name)])) continue;
230
231
			$where = array(
232
				$this->extra_id    => $id,
233
				$this->extra_key   => $name,
234
			);
235
			$is_multiple = $this->is_multiple($name);
236
237
			// we explicitly need to delete fields, if value is empty or field allows multiple values or we have no unique index
238
			if(empty($data[$field]) || $is_multiple || !$this->extra_has_unique_index)
239
			{
240
				$this->db->delete($this->extra_table,$where,__LINE__,__FILE__,$this->app);
241
				if (empty($data[$field])) continue;	// nothing else to do for empty values
242
			}
243
			foreach($is_multiple && !is_array($data[$field]) ? explode(',',$data[$field]) :
244
				// regular custom fields (!$is_multiple) eg. addressbook store multiple values comma-separated
245
				(array)(!$is_multiple && is_array($data[$field]) ? implode(',', $data[$field]) : $data[$field]) as $value)
246
			{
247
				if (!$this->db->insert($this->extra_table,array($this->extra_value => $value)+$extra_cols,$where,__LINE__,__FILE__,$this->app))
248
				{
249
					return $this->db->Errno;
250
				}
251
			}
252
		}
253
		return false;	// no error
254
	}
255
256
	/**
257
	 * merges in new values from the given new data-array
258
	 *
259
	 * reimplemented to also merge the customfields
260
	 *
261
	 * @param $new array in form col => new_value with values to set
262
	 */
263
	function data_merge($new)
264
	{
265
		parent::data_merge($new);
266
267
		if ($this->customfields)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
268
		{
269
			foreach(array_keys($this->customfields) as $name)
270
			{
271
				if (isset($new[$field = $this->get_cf_field($name)]))
272
				{
273
					$this->data[$field] = $new[$field];
274
				}
275
			}
276
		}
277
	}
278
279
	/**
280
	 * reads row matched by key and puts all cols in the data array
281
	 *
282
	 * reimplented to also read the custom fields
283
	 *
284
	 * @param array $keys array with keys in form internalName => value, may be a scalar value if only one key
285
	 * @param string|array $extra_cols string or array of strings to be added to the SELECT, eg. "count(*) as num"
286
	 * @param string $join sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
287
	 * @return array|boolean data if row could be retrived else False
288
	 */
289
	function read($keys,$extra_cols='',$join='')
290
	{
291
		if (!parent::read($keys,$extra_cols,$join))
292
		{
293
			return false;
294
		}
295
		if (($id = (int)$this->data[$this->db_key_cols[$this->autoinc_id]]) && $this->customfields &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
296
			($cfs = $this->read_customfields($id)))
297
		{
298
			$this->data = array_merge($this->data,$cfs[$id]);
299
		}
300
		return $this->data;
301
	}
302
303
	/**
304
	 * saves the content of data to the db
305
	 *
306
	 * reimplented to also save the custom fields
307
	 *
308
	 * @param array $keys if given $keys are copied to data before saveing => allows a save as
309
	 * @param string|array $extra_where =null extra where clause, eg. to check an etag, returns true if no affected rows!
310
	 * @return int|boolean 0 on success, or errno != 0 on error, or true if $extra_where is given and no rows affected
311
	 */
312
	function save($keys=null,$extra_where=null)
313
	{
314
		if (is_array($keys) && count($keys) && !isset($keys[0]))	// allow to use an etag, eg array('etag=etag+1')
315
		{
316
			$this->data_merge($keys);
317
			$keys = null;
318
		}
319
		$ret = parent::save($keys,$extra_where);
320
321
		if ($ret == 0 && $this->customfields)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
322
		{
323
			$this->save_customfields($this->data);
324
		}
325
		return $ret;
326
	}
327
328
	/**
329
	 * deletes row representing keys in internal data or the supplied $keys if != null
330
	 *
331
	 * reimplented to also delete the custom fields
332
	 *
333
	 * @param array|int $keys =null if given array with col => value pairs to characterise the rows to delete, or integer autoinc id
334
	 * @param boolean $only_return_ids =false return $ids of delete call to db object, but not run it (can be used by extending classes!)
335
	 * @return int|array affected rows, should be 1 if ok, 0 if an error or array with id's if $only_return_ids
336
	 */
337
	function delete($keys=null,$only_return_ids=false)
338
	{
339
		if ($this->customfields || $only_return_ids)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
340
		{
341
			$query = parent::delete($keys,true);
342
			// check if query contains more then the id's
343
			if (!isset($query[$this->autoinc_id]) || count($query) != 1)
344
			{
345
				foreach($this->db->select($this->table_name,$this->autoinc_id,$query,__LINE__,__FILE__,false,'',$this->app) as $row)
346
				{
347
					$ids[] = $row[$this->autoinc_id];
348
				}
349
				if (!$ids) return 0;	// no rows affected
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ids seems to be defined by a foreach iteration on line 345. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
350
			}
351
			else
352
			{
353
				$ids = (array)$query[$this->autoinc_id];
354
			}
355
			if ($only_return_ids) return $ids;
356
			$this->db->delete($this->extra_table,array($this->extra_id => $ids),__LINE__,__FILE__);
357
		}
358
		return parent::delete($keys);
359
	}
360
361
	/**
362
	 * query rows for the nextmatch widget
363
	 *
364
	 * Reimplemented to also read the custom fields (if enabled via $query['selectcols']).
365
	 *
366
	 * Please note: the name of the nextmatch-customfields has to be 'customfields'!
367
	 *
368
	 * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter'
369
	 *	For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class.
370
	 * @param array &$rows returned rows/competitions
371
	 * @param array &$readonlys eg. to disable buttons based on acl, not use here, maybe in a derived class
372
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
373
	 *	"LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
374
	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
375
	 * @param mixed $only_keys =false, see search
376
	 * @param string|array $extra_cols =array()
377
	 * @return int total number of rows
378
	 */
379
	function get_rows($query,&$rows,&$readonlys,$join='',$need_full_no_count=false,$only_keys=false,$extra_cols=array())
380
	{
381
		parent::get_rows($query,$rows,$readonlys,$join,$need_full_no_count,$only_keys,$extra_cols);
382
383
		$selectcols = $query['selectcols'] ? (is_string($query['selectcols']) ? explode(',',$query['selectcols']) :$query['selectcols']): array();
384
385
		if ($rows && $this->customfields && (!$selectcols || in_array('customfields',$selectcols)))
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
386
		{
387
			$id2keys = array();
388
			foreach($rows as $key => $row)
389
			{
390
				$id2keys[$row[$this->db_key_cols[$this->autoinc_id]]] = $key;
391
			}
392
			// check if only certain cf's to show
393
			if (!in_array('customfields', $selectcols))
394
			{
395
				foreach($selectcols as $col)
396
				{
397
					if ($this->is_cf($col)) $fields[] = $this->get_cf_name($col);
398
				}
399
			}
400
			if (($cfs = $this->read_customfields(array_keys($id2keys),$fields)))
401
			{
402
				foreach($cfs as $id => $data)
403
				{
404
					$rows[$id2keys[$id]] = array_merge($rows[$id2keys[$id]],$data);
405
				}
406
			}
407
		}
408
		return $this->total;
409
	}
410
411
	/**
412
	 * Return SQL fragment to search custom-fields for given $pattern
413
	 *
414
	 * To be or-ed to query for $_pattern in regular columns of main-table.
415
	 *
416
	 * @param string $_pattern search pattern incl. * or ? as wildcard, if no wildcards used we append and prepend one!
417
	 * @return string with SQL fragment running on main table: "id IN (SELECT id FROM extra-table WHERE extra_value like '$pattern')"
418
	 */
419
	public function cf_match($_pattern)
420
	{
421
		static $private_cfs=null;
422
423
		if (!$this->customfields) return '';	// no custom-fields --> no search
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
424
425
		$sql = ' OR '.$this->table_name.'.'.$this->autoinc_id.' IN (SELECT '.$this->extra_id.
426
			' FROM '.$this->extra_table.' WHERE '.$this->extra_value.' '.
427
			$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.
428
			$GLOBALS['egw']->db->quote($_pattern);
429
430
		// check if there are private cfs not available to current user --> filter by available cfs
431
		if (!isset($private_cfs))
432
		{
433
			$private_cfs = array_diff_key(
434
				Storage\Customfields::get($this->app, true, null, $this->db),	// true: get private cfs too
435
				$this->customfields);
436
			//error_log(__METHOD__."() private_cfs=".array2string($private_cfs));
437
		}
438
		if ($private_cfs)
439
		{
440
			$sql .= ' AND '.$this->db->expression($this->extra_table, array($this->extra_key => array_keys($this->customfields)));
441
		}
442
		return $sql.')';
443
	}
444
445
	/**
446
	 * searches db for rows matching searchcriteria
447
	 *
448
	 * Reimplemented to search, order and filter by custom fields
449
	 *
450
	 * @param array|string $criteria array of key and data cols, OR string with search pattern (incl. * or ? as wildcards)
451
	 * @param boolean|string/array $only_keys =true True returns only keys, False returns all cols. or
0 ignored issues
show
Documentation Bug introduced by
The doc comment boolean|string/array at position 2 could not be parsed: Unknown type name 'string/array' at position 2 in boolean|string/array.
Loading history...
452
	 *	comma seperated list or array of columns to return
453
	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
454
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
455
	 * @param string $wildcard ='' appended befor and after each criteria
456
	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
457
	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
458
	 * @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
459
	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
460
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. "JOIN table2 ON x=y" or
461
	 *	"LEFT JOIN table2 ON (x=y AND z=o)", Note: there's no quoting done on $join, you are responsible for it!!!
462
	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
463
	 * @return array|NULL array of matching rows (the row is an array of the cols) or NULL
464
	 */
465
	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)
466
	{
467
		//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())).')');
468
		$this->process_search($criteria, $only_keys, $order_by, $extra_cols, $wildcard, $op, $filter, $join);
469
470
		return parent::search($criteria, $only_keys, $order_by, $extra_cols, $wildcard, $empty, $op, $start, $filter, $join, $need_full_no_count);
471
	}
472
473
	/**
474
	 * Full logic of search to be reused in custom search methods
475
	 *
476
	 * Works by modifying the parameters so search calls this method and then it's parent with the modified parameters.
477
	 *
478
	 * @param array|string $criteria array of key and data cols, OR string with search pattern (incl. * or ? as wildcards)
479
	 * @param boolean|string/array $only_keys =true True returns only keys, False returns all cols. or
0 ignored issues
show
Documentation Bug introduced by
The doc comment boolean|string/array at position 2 could not be parsed: Unknown type name 'string/array' at position 2 in boolean|string/array.
Loading history...
480
	 *	comma seperated list or array of columns to return
481
	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
482
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
483
	 * @param string $wildcard ='' appended befor and after each criteria
484
	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
485
	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
486
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. "JOIN table2 ON x=y" or
487
	 *	"LEFT JOIN table2 ON (x=y AND z=o)", Note: there's no quoting done on $join, you are responsible for it!!!
488
	 */
489
	protected function process_search(&$criteria, &$only_keys=True, &$order_by='', &$extra_cols='', &$wildcard='', &$op='AND', &$filter=null, &$join='')
490
	{
491
		if ($only_keys === false)
492
		{
493
			$only_keys = $this->table_name.'.*';
494
		}
495
		$extra_join_added = $join && strpos($join, $this->extra_join) !== false;
496
		if ($criteria && is_string($criteria))
497
		{
498
			$extra_join_added = true;	// we have NOT added the join, as we use a sub-query and therefore not need it
499
500
			$criteria = $this->search2criteria($criteria, $wildcard, $op);
501
		}
502
		if ($criteria && is_array($criteria))
503
		{
504
			// check if we search in the custom fields
505
			if (isset($criteria[$this->extra_value]))
506
			{
507
				// we should check if the CF is (still) available, but that makes the slow search even slower :(
508
				if (($negate = $criteria[$this->extra_value][0] === '!'))
509
				{
510
					$criteria[$this->extra_value] = substr($criteria[$this->extra_value],1);
511
				}
512
				$criteria[] = $this->extra_table.'.'.$this->extra_value . ' ' .($negate ? 'NOT ' : '').
513
					$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE]. ' ' .
514
					$this->db->quote($wildcard.$criteria[$this->extra_value].$wildcard);
515
				unset($criteria[$this->extra_value]);
516
			}
517
			// replace ambiguous auto-id with (an exact match of) table_name.autoid
518
			if (isset($criteria[$this->autoinc_id]))
519
			{
520
				if ($criteria[$this->autoinc_id])
521
				{
522
					$criteria[] = $this->db->expression($this->table_name,$this->table_name.'.',
523
						array($this->autoinc_id => $criteria[$this->autoinc_id]));
524
				}
525
				unset($criteria[$this->autoinc_id]);
526
			}
527
			// replace ambiguous column with (an exact match of) table_name.column
528
			foreach($criteria as $name => $val)
529
			{
530
				// only add extra_join, if we really need it
531
				if (!$extra_join_added && (
532
					is_int($name) && strpos($val, $this->extra_value) !== false ||
533
					is_string($name) && $this->is_cf($name)
534
				))
535
				{
536
					$join .= $this->extra_join;
537
					$extra_join_added = true;
538
				}
539
				$extra_columns = $this->db->get_table_definitions($this->app, $this->extra_table);
540
				if(is_string($name) && $extra_columns['fd'][array_search($name, $this->db_cols)])
541
				{
542
					$criteria[] = $this->db->expression($this->table_name,$this->table_name.'.',array(
543
						array_search($name, $this->db_cols) => $val,
544
					));
545
					unset($criteria[$name]);
546
				}
547
				elseif (is_string($name) && $this->is_cf($name))
548
				{
549
					if ($op != 'AND')
550
					{
551
						$name = substr($name, 1);
552
						if (($negate = $criteria[$name][0] === '!'))
553
						{
554
							$val = substr($val,1);
555
						}
556
						$cfcriteria[] = '(' . $this->extra_table.'.'.$this->extra_value . ' ' .($negate ? 'NOT ' : '').
557
							$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE]. ' ' .
558
							$this->db->quote($wildcard.$val.$wildcard) . ' AND ' .
559
							$this->extra_table.'.'.$this->extra_key . ' = ' . $this->db->quote($name) .
560
							')';
561
						unset($criteria[self::CF_PREFIX.$name]);
562
					}
563
					else
564
					{
565
						// criteria operator is AND we remap the criteria to be transformed to filters
566
						$filter[$name] = $val;
567
						unset($criteria[$name]);
568
					}
569
				}
570
			}
571
			if ($cfcriteria && $op =='OR') $criteria[] = implode(' OR ',$cfcriteria);
572
		}
573
		if($only_keys === true)
574
		{
575
			// Expand to keys here, so table_name can be prepended below
576
			$only_keys = array_values($this->db_key_cols);
577
		}
578
		// replace ambiguous column with (an exact match of) table_name.column
579
		if(is_array($only_keys))
580
		{
581
			foreach($only_keys as $key => &$col)
582
			{
583
				if(is_numeric($key) && in_array($col, $this->db_cols, true))
584
				{
585
					$col = $this->table_name .'.'.array_search($col, $this->db_cols).' AS '.$col;
0 ignored issues
show
Bug introduced by
Are you sure array_search($col, $this->db_cols) of type false|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

585
					$col = $this->table_name .'.'./** @scrutinizer ignore-type */ array_search($col, $this->db_cols).' AS '.$col;
Loading history...
586
				}
587
				// Check to make sure our order by doesn't have aliases that won't work
588
				else if (stripos($col, 'AS') !== false && $order_by)
589
				{
590
					list($value, $alias) = explode(' AS ', $col);
591
					if(stripos($order_by, $alias) !== FALSE && stripos($value, $this->table_name) === FALSE)
592
					{
593
						$order_by = str_replace($alias, $value, $order_by);
594
					}
595
				}
596
			}
597
		}
598
		// check if we order by a custom field --> join cf table for given cf and order by it's value
599
		if (strpos($order_by,self::CF_PREFIX) !== false)
600
		{
601
			// if $order_by contains more then order by columns (eg. group by) split it off before
602
			if (($pos = stripos($order_by, 'order by')) !== false)
603
			{
604
				$group_by = substr($order_by, 0, $pos+9);
605
				$order_by = substr($order_by, $pos+9);
606
			}
607
			// fields to order by, as cutomfields may have names with spaces, we examine each order by criteria
608
			$fields2order = explode(',',$order_by);
609
			foreach($fields2order as $v)
610
			{
611
				if (strpos($v,self::CF_PREFIX) !== false)
612
				{
613
					// we found a customfield, so we split that part by space char in order to get Sorting Direction and Fieldname
614
					$buff = explode(' ',trim($v));
615
					$orderDir = array_pop($buff);
616
					$key = substr(trim(implode(' ',$buff)), 1);
617
					if (!isset($this->customfields[$key]))
618
					{
619
						$order_by = preg_replace('/'.preg_quote($v, '/').',?/', '', $order_by);
620
						continue;	// ignore unavaiable CF
621
					}
622
					switch($this->customfields[$key]['type'])
623
					{
624
						case 'int':
625
							$order_by = str_replace($v, 'extra_order.'.$this->extra_value.' IS NULL,'.
626
								$this->db->to_int('extra_order.'.$this->extra_value).' '.$orderDir, $order_by);
627
							break;
628
						case 'float':
629
							$order_by = str_replace($v, 'extra_order.'.$this->extra_value.' IS NULL,'.
630
								$this->db->to_double('extra_order.'.$this->extra_value).' '.$orderDir, $order_by);
631
							break;
632
						default:
633
							$order_by = str_replace($v, 'extra_order.'.$this->extra_value.' IS NULL,extra_order.'.
634
								$this->extra_value.' '.$orderDir, $order_by);
635
					}
636
					// postgres requires that expressions in order by appear in the columns of a distinct select
637
					if ($this->db->Type != 'mysql')
638
					{
639
						if (!is_array($extra_cols))
640
						{
641
							$extra_cols = $extra_cols ? explode(',', $extra_cols) : array();
642
						}
643
						$extra_cols[] = 'extra_order.'.$this->extra_value;
644
						$extra_cols[] = 'extra_order.'.$this->extra_value.' IS NULL';
645
					}
646
					$join .= $this->extra_join_order.' AND extra_order.'.$this->extra_key.'='.$this->db->quote($key);
647
				}
648
			}
649
			// add group by again
650
			if (isset($group_by))
651
			{
652
				$order_by = $group_by.$order_by;
653
			}
654
		}
655
		// check if we filter by a custom field
656
		if (is_array($filter))
657
		{
658
			$_cfnames = array_keys($this->customfields);
659
			$extra_filter = null;
660
			foreach($filter as $name => $val)
661
			{
662
				// replace ambiguous auto-id with (an exact match of) table_name.autoid
663
				if (is_string($name) && $name == $this->autoinc_id)
664
				{
665
					if ((int)$filter[$this->autoinc_id])
666
					{
667
						$filter[] = $this->db->expression($this->table_name,$this->table_name.'.',array(
668
							$this->autoinc_id => $filter[$this->autoinc_id],
669
						));
670
					}
671
					unset($filter[$this->autoinc_id]);
672
				}
673
				// replace ambiguous column with (an exact match of) table_name.column
674
				elseif (is_string($name) && $val!=null && in_array($name, $this->db_cols))
675
				{
676
					$extra_columns = $this->db->get_table_definitions($this->app, $this->extra_table);
677
					if ($extra_columns['fd'][array_search($name, $this->db_cols)])
678
					{
679
						$filter[] = $this->db->expression($this->table_name,$this->table_name.'.',array(
680
							array_search($name, $this->db_cols) => $val,
681
						));
682
						unset($filter[$name]);
683
					}
684
				}
685
				elseif (is_string($name) && $this->is_cf($name))
686
				{
687
					$cf_name = $this->get_cf_name($name);
688
					if (!isset($this->customfields[$cf_name]))
689
					{
690
						unset($filter[$name]);
691
						continue;	// ignore unavailable CF
692
					}
693
					if (!empty($val))	// empty -> dont filter
694
					{
695
						if ($val[0] === '!')	// negative filter
696
						{
697
							$sql_filter = 'extra_filter.'.$this->extra_value.'!='.$this->db->quote(substr($val,1));
698
						}
699
						else	// using Db::expression to allow to use array() with possible values or NULL
700
						{
701
							if($this->customfields[$cf_name]['type'] == 'select' &&
702
								$this->customfields[$cf_name]['rows'] > 1)
703
							{
704
								// Multi-select - any entry with the filter value selected matches
705
								$sql_filter = str_replace($this->extra_value,'extra_filter.'.
706
									$this->extra_value,$this->db->expression($this->extra_table,array(
707
										$this->db->concat("','",$this->extra_value,"','").' '.$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote('%,'.$val.',%')
708
									))
709
								);
710
							}
711
							elseif ($this->customfields[$cf_name]['type'] == 'text')
712
							{
713
								$sql_filter = str_replace($this->extra_value,'extra_filter.'.$this->extra_value,
714
										$this->db->expression($this->extra_table,array(
715
										$this->extra_value.' '.$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($wildcard.$val.$wildcard)
716
									))
717
								);
718
							}
719
							else
720
							{
721
								$sql_filter = str_replace($this->extra_value,'extra_filter.'.
722
									$this->extra_value,$this->db->expression($this->extra_table,array($this->extra_value => $val)));
723
							}
724
						}
725
						// need to use a LEFT JOIN for negative search or to allow NULL values
726
						$need_left_join = $val[0] === '!' || strpos($sql_filter,'IS NULL') !== false ? ' LEFT ' : '';
727
						$join .= str_replace('extra_filter','extra_filter'.$extra_filter,$need_left_join.$this->extra_join_filter.
728
							' AND extra_filter.'.$this->extra_key.'='.$this->db->quote($cf_name).
729
							' AND '.$sql_filter);
730
						++$extra_filter;
731
					}
732
					unset($filter[$name]);
733
				}
734
				elseif(is_int($name) && $this->is_cf($val))	// lettersearch: #cfname LIKE 's%'
735
				{
736
					$_cf = explode(' ',$val);
737
					foreach($_cf as $cf_np)
738
					{
739
						// building cf_name by glueing parts together (, in case someone used whitespace in their custom field names)
740
						$tcf_name = ($tcf_name?$tcf_name.' ':'').$cf_np;
741
						// reacts on the first one found that matches an existing customfield, should be better then the old behavior of
742
						// simply splitting by " " and using the first part
743
						if ($this->is_cf($tcf_name) && ($cfn = $this->get_cf_name($tcf_name)) && array_search($cfn,(array)$_cfnames,true)!==false )
744
						{
745
							$cf = $tcf_name;
746
							break;
747
						}
748
					}
749
					unset($filter[$name]);
750
					$cf_name = $this->get_cf_name($cf);
751
					if (!isset($this->customfields[$cf_name])) continue;	// ignore unavailable CF
752
					$join .= str_replace('extra_filter','extra_filter'.$extra_filter,$this->extra_join_filter.
753
						' AND extra_filter.'.$this->extra_key.'='.$this->db->quote($cf_name).
754
						' AND '.str_replace($cf,'extra_filter.'.$this->extra_value,$val));
755
					++$extra_filter;
756
				}
757
			}
758
		}
759
		// add DISTINCT as by joining custom fields for search a row can be returned multiple times
760
		if ($join && strpos($join, $this->extra_join) !== false)
761
		{
762
			if (is_array($only_keys))
763
			{
764
				$only_keys = array_values($only_keys);
765
				$only_keys[0] = 'DISTINCT '.($only_keys[0] != $this->autoinc_id ? $only_keys[0] :
766
					$this->table_name.'.'.$this->autoinc_id.' AS '.$this->autoinc_id);
767
			}
768
			else
769
			{
770
				$only_keys = 'DISTINCT '.$only_keys;
771
			}
772
		}
773
	}
774
775
	/**
776
	 * Function to test if $field is a custom field: check for the prefix
777
	 *
778
	 * @param string $field
779
	 * @return boolean true if $name is a custom field, false otherwise
780
	 */
781
	function is_cf($field)
782
	{
783
		return $field[0] == self::CF_PREFIX;
784
	}
785
786
	/**
787
	 * Get name part from a custom field: remove the prefix
788
	 *
789
	 * @param string $field
790
	 * @return string name without prefix
791
	 */
792
	function get_cf_name($field)
793
	{
794
		return substr($field,1);
795
	}
796
797
	/**
798
	 * Get the field-name from the name of a custom field: prepend the prefix
799
	 *
800
	 * @param string $name
801
	 * @return string prefix-name
802
	 */
803
	function get_cf_field($name)
804
	{
805
		return self::CF_PREFIX.$name;
806
	}
807
808
	/**
809
	 * Check if cf is stored as 1:N relation in DB and array in memory
810
	 *
811
	 * @param string $name
812
	 * @return string
813
	 */
814
	function is_multiple($name)
815
	{
816
		return $this->allow_multiple_values && in_array($this->customfields[$name]['type'],array('select','select-account')) &&
817
			$this->customfields[$name]['rows'] > 1;
818
	}
819
}
820