Base::update()   B
last analyzed

Complexity

Conditions 10
Paths 162

Size

Total Lines 40
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 19
nc 162
nop 2
dl 0
loc 40
rs 7.1499
c 0
b 0
f 0

How to fix   Complexity   

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:

1
<?php
2
/**
3
 * EGroupware generalized SQL Storage Object
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 2002-16 by [email protected]
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Storage;
15
16
use EGroupware\Api;
17
18
/**
19
 * generalized SQL Storage Object
20
 *
21
 * the class can be used in following ways:
22
 * 1) by calling the constructor with an app and table-name or
23
 * 2) by setting the following documented class-vars in a class derived from this one
24
 * Of cause you can derive from the class and call the constructor with params.
25
 *
26
 * @todo modify search() to return an interator instead of an array
27
 */
28
class Base
29
{
30
 	/**
31
 	 * need to be set in the derived class to the db-table-name
32
 	 *
33
 	 * @var string
34
 	 */
35
	var $table_name;
36
 	/**
37
 	 * db-col-name of autoincrement id or ''
38
 	 *
39
 	 * @var string
40
 	 */
41
	var $autoinc_id = '';
42
	/**
43
	 * all cols in data which are not (direct)in the db, for data_merge
44
	 *
45
	 * @var array
46
	 */
47
	var $non_db_cols = array();
48
	/**
49
	 * 4 turns on the so_sql debug-messages, default 0
50
	 *
51
	 * @var int
52
	 */
53
	var $debug = 0;
54
	/**
55
	 * string to be written to db if a col-value is '', eg. "''" or 'NULL' (default)
56
	 *
57
	 * @var string
58
	 */
59
	var $empty_on_write = 'NULL';
60
	/**
61
	 * total number of entries of last search with start != false
62
	 *
63
	 * @var int|boolean
64
	 */
65
	var $total = false;
66
	/**
67
	 * protected instance or reference (depeding on $no_clone param of constructor) of the db-object
68
	 *
69
	 * @var Api\Db
70
	 */
71
	protected $db;
72
	/**
73
	 * unique keys/index, set by derived class or via so_sql($app,$table)
74
	 *
75
	 * @var array
76
	 */
77
	var $db_uni_cols = array();
78
	/**
79
	 * db-col-name / internal-name pairs, set by derived calls or via so_sql($app,$table)
80
	 *
81
	 * @var array
82
	 */
83
	var $db_key_cols = array();
84
	/**
85
	 * db-col-name / internal-name pairs, set by derived calls or via so_sql($app,$table)
86
	 *
87
	 * @var array
88
	 */
89
	var $db_data_cols = array();
90
	/**
91
	 * @var array $db_cols all columns = $db_key_cols + $db_data_cols, set in the constructor
92
	 */
93
	var $db_cols = array();
94
	/**
95
	 * eGW table definition
96
	 *
97
	 * @var array
98
	 */
99
	var $table_def = array();
100
	/**
101
	 * Appname to use in all queries, set via constructor
102
	 *
103
	 * @var string
104
	 */
105
	var $app;
106
	/**
107
	 * holds the content of all columns
108
	 *
109
	 * @var array
110
	 */
111
	var $data = array();
112
	/**
113
	 * Timestaps that need to be adjusted to user-time on reading or saving
114
	 *
115
	 * @var array
116
	 */
117
	var $timestamps = array();
118
	/**
119
	 * Type of timestamps returned by this class (read and search methods), default null means leave them unchanged
120
	 *
121
	 * Possible values:
122
	 * - 'ts'|'integer' convert every timestamp to an integer unix timestamp
123
	 * - 'string' convert every timestamp to a 'Y-m-d H:i:s' string
124
	 * - 'object' convert every timestamp to a Api\DateTime object
125
	 *
126
	 * @var string
127
	 */
128
	public $timestamp_type;
129
	/**
130
	 * Offset in secconds between user and server-time,	it need to be add to a server-time to get the user-time
131
	 * or substracted from a user-time to get the server-time
132
	 *
133
	 * @var int
134
	 * @deprecated use Api\DateTime methods instead, as the offset between user and server time is only valid for current time
135
	 */
136
	var $tz_offset_s;
137
	/**
138
	 * Current time in user timezone
139
	 *
140
	 * @var int|string|DateTime format depends on $this->timestamp_type
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\DateTime was not found. Did you mean DateTime? If so, make sure to prefix the type with \.
Loading history...
141
	 */
142
	var $now;
143
	/**
144
	 * Which columns should be searched, if a non-empty string is passed to criteria parameter of search()
145
	 *
146
	 * If not set (by extending class), all data columns will be searched.
147
	 *
148
	 * @var array
149
	 */
150
	var $columns_to_search;
151
152
	/**
153
	 * Table has boolean fields, which need automatic conversation, got set automatic by call to setup_table
154
	 *
155
	 * Set it to false, if you dont want automatic conversation
156
	 *
157
	 * @var boolean
158
	 */
159
	protected $has_bools = false;
160
161
	/**
162
	 * Should search return an iterator (true) or an array (false = default)
163
	 *
164
	 * @var boolean
165
	 */
166
	public $search_return_iterator = false;
167
168
	/**
169
	 * constructor of the class
170
	 *
171
	 * NEED to be called from the constructor of the derived class !!!
172
	 *
173
	 * @param string $app should be set if table-defs to be read from <app>/setup/tables_current.inc.php
174
	 * @param string $table should be set if table-defs to be read from <app>/setup/tables_current.inc.php
175
	 * @param Api\Db $db database object, if not the one in $GLOBALS['egw']->db should be used, eg. for an other database
176
	 * @param string $column_prefix ='' column prefix to automatic remove from the column-name, if the column name starts with it
177
	 * @param boolean $no_clone =false can we avoid to clone the db-object, default no
178
	 * 	new code using appnames and foreach(select(...,$app) can set it to avoid an extra instance of the db object
179
	 * @param string $timestamp_type =null default null=leave them as is, 'ts'|'integer' use integer unix timestamps,
180
	 * 	'object' use Api\DateTime objects or 'string' use DB timestamp (Y-m-d H:i:s) string
181
	 */
182
	function __construct($app='',$table='',Api\Db $db=null,$column_prefix='',$no_clone=false,$timestamp_type=null)
183
	{
184
		// ease the transition to api
185
		if ($app == 'phpgwapi') $app = 'api';
186
187
		if ($no_clone)
188
		{
189
			$this->db = is_object($db) ? $db : $GLOBALS['egw']->db;
190
		}
191
		else
192
		{
193
			$this->db = is_object($db) ? clone($db) : clone($GLOBALS['egw']->db);
194
		}
195
		$this->db_cols = $this->db_key_cols + $this->db_data_cols;
196
197
		if ($app)
198
		{
199
			$this->app = $app;
200
201
			if (!$no_clone) $this->db->set_app($app);
202
203
			if ($table) $this->setup_table($app,$table,$column_prefix);
204
		}
205
		$this->init();
206
207
		if ((int) $this->debug >= 4)
208
		{
209
			echo "<p>".__METHOD__."('$app','$table')</p>\n";
210
			_debug_array($this);
211
		}
212
		$this->set_times($timestamp_type);
213
	}
214
215
	/**
216
	 * Set class vars timestamp_type, now and tz_offset_s
217
	 *
218
	 * @param string|boolean $timestamp_type =false default false do NOT set time_stamptype,
219
	 * 	null=leave them as is, 'ts'|'integer' use integer unix timestamps, 'object' use Api\DateTime objects,
220
	 *  'string' use DB timestamp (Y-m-d H:i:s) string
221
	 */
222
	public function set_times($timestamp_type=false)
223
	{
224
		if ($timestamp_type !== false) $this->timestamp_type = $timestamp_type;
0 ignored issues
show
Documentation Bug introduced by
It seems like $timestamp_type can also be of type true. However, the property $timestamp_type is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
225
226
		// set current time
227
		switch($this->timestamp_type)
228
		{
229
			case 'object':
230
				$this->now = new Api\DateTime('now');
231
				break;
232
			case 'string':
233
				$this->now = Api\DateTime::to('now',Api\DateTime::DATABASE);
234
				break;
235
			default:
236
				$this->now = Api\DateTime::to('now','ts');
237
		}
238
		$this->tz_offset_s = Api\DateTime::tz_offset_s();
0 ignored issues
show
Deprecated Code introduced by
The property EGroupware\Api\Storage\Base::$tz_offset_s has been deprecated: use Api\DateTime methods instead, as the offset between user and server time is only valid for current time ( Ignorable by Annotation )

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

238
		/** @scrutinizer ignore-deprecated */ $this->tz_offset_s = Api\DateTime::tz_offset_s();

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
239
	}
240
241
	/**
242
	 * sets up the class for an app and table (by using the table-definition of $app/setup/tables_current.inc.php
243
	 *
244
	 * If you need a more complex conversation then just removing the column_prefix, you have to do so in a derifed class !!!
245
	 *
246
	 * @param string $app app-name $table belongs too
247
	 * @param string $table table-name
248
	 * @param string $colum_prefix ='' column prefix to automatic remove from the column-name, if the column name starts with it
249
	 */
250
	function setup_table($app,$table,$colum_prefix='')
251
	{
252
		$this->table_name = $table;
253
		$this->table_def = $this->db->get_table_definitions($app,$table);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->get_table_definitions($app, $table) can also be of type false. However, the property $table_def is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
254
		if (!$this->table_def || !is_array($this->table_def['fd']))
255
		{
256
			throw new Api\Exception\WrongParameter(__METHOD__."('$app','$table'): No table definition for '$table' found !!!");
257
		}
258
		$this->db_key_cols = $this->db_data_cols = $this->db_cols = array();
259
		$this->autoinc_id = '';
260
		$len_prefix = strlen($colum_prefix);
261
		foreach($this->table_def['fd'] as $col => $def)
262
		{
263
			$name = $col;
264
			if ($len_prefix && substr($name,0,$len_prefix) == $colum_prefix)
265
			{
266
				$name = substr($col,$len_prefix);
267
			}
268
			if (in_array($col,$this->table_def['pk']))
269
			{
270
				$this->db_key_cols[$col] = $name;
271
			}
272
			else
273
			{
274
				$this->db_data_cols[$col] = $name;
275
			}
276
			$this->db_cols[$col] = $name;
277
278
			if ($def['type'] == 'auto')
279
			{
280
				$this->autoinc_id = $col;
281
			}
282
			if ($def['type'] == 'bool') $this->has_bools = true;
283
284
			foreach($this->table_def['uc'] as $k => $uni_index)
285
			{
286
				if (is_array($uni_index) && in_array($name,$uni_index))
287
				{
288
					$this->db_uni_cols[$k][$col] = $name;
289
				}
290
				elseif($name === $uni_index)
291
				{
292
					$this->db_uni_cols[$col] = $name;
293
				}
294
			}
295
		}
296
	}
297
298
	/**
299
	 * Add all timestamp fields to $this->timestamps to get automatically converted to usertime
300
	 *
301
	 */
302
	function convert_all_timestamps()
303
	{
304
		$check_already_included = !empty($this->timestamps);
305
		foreach($this->table_def['fd'] as $name => $data)
306
		{
307
			if ($data['type'] == 'timestamp' && (!$check_already_included || !in_array($name,$this->timestamps)))
308
			{
309
				$this->timestamps[] = $name;
310
			}
311
		}
312
	}
313
314
	/**
315
	 * merges in new values from the given new data-array
316
	 *
317
	 * @param $new array in form col => new_value with values to set
318
	 */
319
	function data_merge($new)
320
	{
321
		if ((int) $this->debug >= 4) echo "<p>so_sql::data_merge(".print_r($new,true).")</p>\n";
322
323
		if (!is_array($new) || !count($new))
324
		{
325
			return;
326
		}
327
		foreach($this->db_cols as $db_col => $col)
328
		{
329
			if (array_key_exists($col,$new))
330
			{
331
				$this->data[$col] = $new[$col];
332
			}
333
		}
334
		foreach($this->non_db_cols as $db_col => $col)
335
		{
336
			if (array_key_exists($col,$new))
337
			{
338
				$this->data[$col] = $new[$col];
339
			}
340
		}
341
		if (isset($new[self::USER_TIMEZONE_READ]))
342
		{
343
			$this->data[self::USER_TIMEZONE_READ] = $new[self::USER_TIMEZONE_READ];
344
		}
345
		if ((int) $this->debug >= 4) _debug_array($this->data);
346
	}
347
348
	/**
349
	 * changes the data from the db-format to your work-format
350
	 *
351
	 * It gets called everytime when data is read from the db.
352
	 * This default implementation only converts the timestamps mentioned in $this->timestamps from server to user time.
353
	 * You can reimplement it in a derived class like this:
354
	 *
355
	 * function db2data($data=null)
356
	 * {
357
	 * 		if (($intern = !is_array($data)))
358
	 * 		{
359
	 * 			$data =& $this->data;
360
	 * 		}
361
	 * 		// do your own modifications here
362
	 *
363
	 * 		return parent::db2data($intern ? null : $data);	// important to use null, if $intern!
364
	 * }
365
	 *
366
	 * @param array $data =null if given works on that array and returns result, else works on internal data-array
367
	 * @return array
368
	 */
369
	function db2data($data=null)
370
	{
371
		if (!is_array($data))
372
		{
373
			$data = &$this->data;
374
		}
375
		if ($this->timestamps)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->timestamps 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...
376
		{
377
			foreach($this->timestamps as $name)
378
			{
379
				if (isset($data[$name]) && $data[$name])
380
				{
381
					if ($data[$name] === '0000-00-00 00:00:00')
382
					{
383
						$data[$name] = null;
384
					}
385
					else
386
					{
387
						$data[$name] = Api\DateTime::server2user($data[$name],$this->timestamp_type);
388
					}
389
				}
390
			}
391
		}
392
		// automatic convert booleans (eg. PostgreSQL stores 't' or 'f', which both evaluate to true!)
393
		if ($this->has_bools !== false)
394
		{
395
			if (!isset($this->table_def))
396
			{
397
				$this->table_def = $this->db->get_table_definitions($this->app, $this->table);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->db->get_table_def...his->app, $this->table) can also be of type false. However, the property $table_def is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Bug introduced by
The property table does not exist on EGroupware\Api\Storage\Base. Did you mean table_def?
Loading history...
398
				if (!$this->table_def || !is_array($this->table_def['fd']))
399
				{
400
					throw new Api\Exception\WrongParameter(__METHOD__."(): No table definition for '$this->table' found !!!");
401
				}
402
			}
403
			foreach($this->table_def['fd'] as $col => $def)
404
			{
405
				if ($def['type'] == 'bool' && isset($data[$col]))
406
				{
407
					$data[$col] = $this->db->from_bool($data[$col]);
408
				}
409
			}
410
		}
411
		return $data;
412
	}
413
414
	/**
415
	 * changes the data from your work-format to the db-format
416
	 *
417
	 * It gets called everytime when data gets writen into db or on keys for db-searches.
418
	 * This default implementation only converts the timestamps mentioned in $this->timestampfs from user to server time.
419
	 * You can reimplement it in a derived class like this:
420
	 *
421
	 * function data2db($data=null)
422
	 * {
423
	 * 		if (($intern = !is_array($data)))
424
	 * 		{
425
	 * 			$data =& $this->data;
426
	 * 		}
427
	 * 		// do your own modifications here
428
	 *
429
	 * 		return parent::data2db($intern ? null : $data);	// important to use null, if $intern!
430
	 * }
431
	 *
432
	 * @param array $data =null if given works on that array and returns result, else works on internal data-array
433
	 * @return array
434
	 */
435
	function data2db($data=null)
436
	{
437
		if (!is_array($data))
438
		{
439
			$data = &$this->data;
440
		}
441
		if ($this->timestamps)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->timestamps 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...
442
		{
443
			foreach($this->timestamps as $name)
444
			{
445
				if (isset($data[$name]) && $data[$name])
446
				{
447
					$data[$name] = Api\DateTime::user2server($data[$name],$this->timestamp_type);
448
				}
449
			}
450
		}
451
		return $data;
452
	}
453
454
	/**
455
	 * initializes data with the content of key
456
	 *
457
	 * @param array $keys =array() array with keys in form internalName => value
458
	 * @return array internal data after init
459
	 */
460
	function init($keys=array())
461
	{
462
		$this->data = array();
463
464
		$this->db2data();
465
466
		$this->data_merge($keys);
467
468
		return $this->data;
469
	}
470
471
	/**
472
	 * Name of automatically set user timezone field from read
473
	 */
474
	const USER_TIMEZONE_READ = 'user_timezone_read';
475
476
	/**
477
	 * reads row matched by key and puts all cols in the data array
478
	 *
479
	 * @param array $keys array with keys in form internalName => value, may be a scalar value if only one key
480
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
481
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
482
	 * @return array|boolean data if row could be retrived else False
483
	 */
484
	function read($keys,$extra_cols='',$join='')
485
	{
486
		if (!is_array($keys))
0 ignored issues
show
introduced by
The condition is_array($keys) is always true.
Loading history...
487
		{
488
			$pk = array_values($this->db_key_cols);
489
			if ($pk) $keys = array($pk[0] => $keys);
490
		}
491
492
		$this->init($keys);
493
		$this->data2db();
494
495
		$query = false;
496
		foreach ($this->db_key_cols as $db_col => $col)
497
		{
498
			if ($this->data[$col] != '')
499
			{
500
				$query[$db_col] = $this->data[$col];
501
			}
502
		}
503
		if (!$query)	// no primary key in keys, lets try the data_cols for a unique key
0 ignored issues
show
introduced by
The condition $query is always false.
Loading history...
504
		{
505
			foreach($this->db_uni_cols as $db_col => $col)
506
			{
507
				if (!is_array($col) && $this->data[$col] != '')
508
				{
509
					$query[$db_col] = $this->data[$col];
510
				}
511
				elseif(is_array($col))
512
				{
513
					$q = array();
514
					foreach($col as $db_c => $c)
515
					{
516
						if ($this->data[$col] == '')
517
						{
518
							$q = null;
519
							break;
520
						}
521
						$q[$db_c] = $this->data[$c];
522
					}
523
					if ($q) $query += $q;
524
				}
525
			}
526
		}
527
		if (!$query)	// no unique key in keys, lets try everything else
528
		{
529
			foreach($this->db_data_cols as $db_col => $col)
530
			{
531
				if ($this->data[$col] != '')
532
				{
533
					$query[$db_col] = $this->data[$col];
534
				}
535
			}
536
		}
537
		if (!$query)	// keys has no cols
538
		{
539
			$this->db2data();
540
541
			return False;
542
		}
543
		if ($join)	// Prefix the columns with the table-name, as they might exist in the join
544
		{
545
			foreach($query as $col => $val)
546
			{
547
				if (is_int($col) || strpos($join,$col) === false) continue;
548
				$query[] = $this->db->expression($this->table_name,$this->table_name.'.',array($col=>$val));
549
				unset($query[$col]);
550
			}
551
		}
552
		// copy direct SQL parts from $keys
553
		for($i = 0; is_array($keys) && isset($keys[$i]); ++$i)
554
		{
555
			$query[] = $keys[$i];
556
		}
557
		foreach($this->db->select($this->table_name,'*'.($extra_cols?','.(is_array($extra_cols)?implode(',',$extra_cols):$extra_cols):''),
558
			$query,__LINE__,__FILE__,False,'',$this->app,0,$join) as $row)
559
		{
560
			$cols = $this->db_cols;
561
			if ($extra_cols)	// extra columns to report
562
			{
563
				foreach(is_array($extra_cols) ? $extra_cols : array($extra_cols) as $col)
564
				{
565
					if (FALSE!==stripos($col,' as ')) $col = preg_replace('/^.* as *([a-z0-9_]+) *$/i','\\1',$col);
566
					$cols[$col] = $col;
567
				}
568
			}
569
			foreach ($cols as $db_col => $col)
570
			{
571
				$this->data[$col] = $row[$db_col];
572
			}
573
			$this->db2data();
574
575
			// store user timezone used for reading
576
			$this->data[self::USER_TIMEZONE_READ] = Api\DateTime::$user_timezone->getName();
577
578
			if ((int) $this->debug >= 4)
579
			{
580
				echo "data =\n"; _debug_array($this->data);
581
			}
582
			return $this->data;
583
		}
584
		if ($this->autoinc_id)
585
		{
586
			unset($this->data[$this->db_key_cols[$this->autoinc_id]]);
587
		}
588
		if ((int) $this->debug >= 4) echo "nothing found !!!</p>\n";
589
590
		$this->db2data();
591
592
		return False;
593
	}
594
595
	/**
596
	 * saves the content of data to the db
597
	 *
598
	 * @param array $keys =null if given $keys are copied to data before saveing => allows a save as
599
	 * @param string|array $extra_where =null extra where clause, eg. to check an etag, returns true if no affected rows!
600
	 * @return int|boolean 0 on success, or errno != 0 on error, or true if $extra_where is given and no rows affected
601
	 */
602
	function save($keys=null,$extra_where=null)
603
	{
604
		if (is_array($keys) && count($keys)) $this->data_merge($keys);
605
606
		// check if data contains user timezone during read AND user changed timezone since then
607
		// --> load old timezone for the rest of this request
608
		// this only a grude hack, better handle this situation in app code:
609
		// history logging eg. depends on old data read before calling save, which is then in new timezone!
610
		// anyway it's better fixing it here then not fixing it at all ;-)
611
		if (isset($this->data[self::USER_TIMEZONE_READ]) && $this->data[self::USER_TIMEZONE_READ] != Api\DateTime::$user_timezone->getName())
612
		{
613
			//echo "<p>".__METHOD__."() User change TZ since read! tz-read=".$this->data[self::USER_TIMEZONE_READ].' != current-tz='.Api\DateTime::$user_timezone->getName()." --> fixing</p>\n";
614
			error_log(__METHOD__."() User changed TZ since read! tz-read=".$this->data[self::USER_TIMEZONE_READ].' != current-tz='.Api\DateTime::$user_timezone->getName()." --> fixing</p>");
615
			$GLOBALS['egw_info']['user']['preferences']['common']['tz'] = $this->data[self::USER_TIMEZONE_READ];
616
			Api\DateTime::setUserPrefs($this->data[self::USER_TIMEZONE_READ]);
617
			$this->set_times();
618
		}
619
		$this->data2db();
620
621
		if ((int) $this->debug >= 4) { echo "so_sql::save(".print_r($keys,true).") autoinc_id='$this->autoinc_id', data="; _debug_array($this->data); }
622
623
		if ($this->autoinc_id && !$this->data[$this->db_key_cols[$this->autoinc_id]])	// insert with auto id
624
		{
625
			foreach($this->db_cols as $db_col => $col)
626
			{
627
				if (!$this->autoinc_id || $db_col != $this->autoinc_id)	// not write auto-inc-id
628
				{
629
					if (!array_key_exists($col,$this->data) && 	// handling of unset columns in $this->data
630
						(isset($this->table_def['fd'][$db_col]['default']) ||	// we have a default value
631
						 !isset($this->table_def['fd'][$db_col]['nullable']) || $this->table_def['fd'][$db_col]['nullable']))	// column is nullable
632
					{
633
						continue;	// no need to write that (unset) column
634
					}
635
					if ($this->table_def['fd'][$db_col]['type'] == 'varchar' &&
636
						strlen($this->data[$col]) > $this->table_def['fd'][$db_col]['precision'])
637
					{
638
						// truncate the field to mamimum length, if upper layers didn't care
639
						$data[$db_col] = substr($this->data[$col],0,$this->table_def['fd'][$db_col]['precision']);
640
					}
641
					else
642
					{
643
						$data[$db_col] = (string) $this->data[$col] === '' && $this->empty_on_write == 'NULL' ? null : $this->data[$col];
644
					}
645
				}
646
			}
647
			$this->db->insert($this->table_name,$data,false,__LINE__,__FILE__,$this->app);
648
649
			if ($this->autoinc_id)
650
			{
651
				$this->data[$this->db_key_cols[$this->autoinc_id]] = $this->db->get_last_insert_id($this->table_name,$this->autoinc_id);
652
			}
653
		}
654
		else // insert in table without auto id or update of existing row, dont write colums unset in $this->data
655
		{
656
			foreach($this->db_data_cols as $db_col => $col)
657
			{
658
				// we need to update columns set to null: after a $this->data[$col]=null:
659
				// - array_key_exits($col,$this->data) === true
660
				// - isset($this->data[$col]) === false
661
				if (!array_key_exists($col,$this->data) &&	// handling of unset columns in $this->data
662
					($this->autoinc_id ||			// update of table with auto id or
663
					 isset($this->table_def['fd'][$db_col]['default']) ||	// we have a default value or
664
					 !isset($this->table_def['fd'][$db_col]['nullable']) || $this->table_def['fd'][$db_col]['nullable']))	// column is nullable
665
				{
666
					continue;	// no need to write that (unset) column
667
				}
668
				$data[$db_col] = !is_object($this->data[$col]) && (string) $this->data[$col] === '' && $this->empty_on_write == 'NULL' ? null : $this->data[$col];
669
			}
670
			// allow to add direct sql updates, eg. "etag=etag+1" with int keys
671
			if (is_array($keys) && isset($keys[0]))
672
			{
673
				for($n=0; isset($keys[$n]); ++$n)
674
				{
675
					$data[] = $keys[$n];
676
				}
677
			}
678
			$keys = $extra_where;
679
			foreach($this->db_key_cols as $db_col => $col)
680
			{
681
				$keys[$db_col] = $this->data[$col];
682
			}
683
			if (!$data && !$this->autoinc_id)	// happens if all columns are in the primary key
684
			{
685
				$data = $keys;
686
				$keys = False;
687
			}
688
			if ($this->autoinc_id)
689
			{
690
				$this->db->update($this->table_name,$data,$keys,__LINE__,__FILE__,$this->app);
691
				if (($nothing_affected = !$this->db->Errno && !$this->db->affected_rows()) && $extra_where)
692
				{
693
					return true;	// extra_where not met, eg. etag wrong
694
				}
695
			}
696
			// always try an insert if we have no autoinc_id, as we dont know if the data exists
697
			if (!$this->autoinc_id || $nothing_affected)
698
			{
699
				$this->db->insert($this->table_name,$data,$keys,__LINE__,__FILE__,$this->app);
700
			}
701
		}
702
		$this->db2data();
703
704
		return $this->db->Errno;
705
	}
706
707
	/**
708
	 * Update only the given fields, if the primary key is not given, it will be taken from $this->data
709
	 *
710
	 * @param array $_fields
711
	 * @param boolean $merge =true if true $fields will be merged with $this->data (after update!), otherwise $this->data will be just $fields
712
	 * @return int|boolean 0 on success, or errno != 0 on error, or true if $extra_where is given and no rows affected
713
	 */
714
	function update($_fields,$merge=true)
715
	{
716
		if ($merge) $this->data_merge($_fields);
717
718
		$fields = $this->data2db($_fields);
719
720
		// extract the keys from $fields or - if not set there - from $this->data
721
		$keys = array();
722
		foreach($this->db_key_cols as $col => $name)
723
		{
724
			$keys[$col] = isset($fields[$name]) ? $fields[$name] : $this->data[$name];
725
			unset($fields[$name]);
726
		}
727
		// extract the data from $fields
728
		$data = array();
729
		foreach($this->db_data_cols as $col => $name)
730
		{
731
			if (array_key_exists($name,$fields))
732
			{
733
				$data[$col] = $fields[$name];
734
				unset($fields[$name]);
735
			}
736
		}
737
		// add direct sql like 'etag=etag+1' (it has integer keys)
738
		foreach($fields as $key => $value)
739
		{
740
			if (is_int($key))
741
			{
742
				$data[] = $value;
743
			}
744
		}
745
		if (!$data)
746
		{
747
			return 0;	// nothing to update
748
		}
749
		if (!$this->db->update($this->table_name,$data,$keys,__LINE__,__FILE__,$this->app))
750
		{
751
			return $this->db->Errno;
752
		}
753
		return 0;
754
	}
755
756
	/**
757
	 * deletes row representing keys in internal data or the supplied $keys if != null
758
	 *
759
	 * @param array|int $keys =null if given array with col => value pairs to characterise the rows to delete, or integer autoinc id
760
	 * @param boolean $only_return_query =false return $query of delete call to db object, but not run it (used by so_sql_cf!)
761
	 * @return int|array affected rows, should be 1 if ok, 0 if an error or array with id's if $only_return_ids
762
	 */
763
	function delete($keys=null,$only_return_query=false)
764
	{
765
		if ($this->autoinc_id && $keys && !is_array($keys))
766
		{
767
			$keys = array($this->autoinc_id => $keys);
768
		}
769
		if (!is_array($keys) || !count($keys))	// use internal data
770
		{
771
			$data = $this->data;
772
			$keys = $this->db_key_cols;
773
		}
774
		else	// data and keys are supplied in $keys
775
		{
776
			$data = $keys; $keys = array();
777
			foreach($this->db_cols as $db_col => $col)
778
			{
779
				if (isset($data[$col]))
780
				{
781
					$keys[$db_col] = $col;
782
				}
783
			}
784
			// keep sql fragments (with integer key)
785
			foreach($data as $key => $val)
786
			{
787
				if (is_int($key))
788
				{
789
					$keys[] = $val;
790
				}
791
			}
792
		}
793
		$data = $this->data2db($data);
794
795
		foreach($keys as $db_col => $col)
796
		{
797
			if (is_int($db_col))
798
			{
799
				$query[] = $col;
800
			}
801
			else
802
			{
803
				$query[$db_col] = $data[$col];
804
			}
805
		}
806
		if ($only_return_query) return $query;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $query seems to be defined by a foreach iteration on line 795. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
807
808
		$this->db->delete($this->table_name,$query,__LINE__,__FILE__,$this->app);
809
810
		return $this->db->affected_rows();
811
	}
812
813
	/**
814
	 * searches db for rows matching searchcriteria
815
	 *
816
	 * '*' and '?' are replaced with sql-wildcards '%' and '_'
817
	 *
818
	 * For a union-query you call search for each query with $start=='UNION' and one more with only $order_by and $start set to run the union-query.
819
	 *
820
	 * @param array|string $criteria array of key and data cols, OR string with search pattern (incl. * or ? as wildcards)
821
	 * @param boolean|string|array $only_keys =true True returns only keys, False returns all cols. or
822
	 *	comma seperated list or array of columns to return
823
	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
824
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
825
	 * @param string $wildcard ='' appended befor and after each criteria
826
	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
827
	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
828
	 * @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
829
	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
830
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. "JOIN table2 ON x=y" or
831
	 *	"LEFT JOIN table2 ON (x=y AND z=o)", Note: there's no quoting done on $join, you are responsible for it!!!
832
	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
833
	 * @todo return an interator instead of an array
834
	 * @return array|NULL array of matching rows (the row is an array of the cols) or NULL
835
	 */
836
	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)
837
	{
838
		//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())).')');
839
		if ((int) $this->debug >= 4) echo "<p>so_sql::search(".print_r($criteria,true).",'$only_keys','$order_by',".print_r($extra_cols,true).",'$wildcard','$empty','$op','$start',".print_r($filter,true).",'$join')</p>\n";
840
841
		// if extending class or instanciator set columns to search, convert string criteria to array
842
		if ($criteria && !is_array($criteria))
843
		{
844
			$search = $this->search2criteria($criteria,$wildcard,$op);
845
			$criteria = array($search);
846
		}
847
		if (!is_array($criteria))
848
		{
849
			$query = $criteria;
850
		}
851
		else
852
		{
853
			$query = $this->parse_search($criteria, $wildcard, $empty, $op);
854
		}
855
		if (is_array($filter))
856
		{
857
			$db_filter = array();
858
			$data2db_filter = $this->data2db($filter);
859
			if (!is_array($data2db_filter)) {
0 ignored issues
show
introduced by
The condition is_array($data2db_filter) is always true.
Loading history...
860
				echo function_backtrace()."<br/>\n";
861
				echo "filter=";_debug_array($filter);
862
				echo "data2db(filter)=";_debug_array($data2db_filter);
863
			}
864
			foreach($data2db_filter as $col => $val)
865
			{
866
				if ($val !== '')
867
				{
868
					// check if a db-internal name conversation necessary
869
					if (!is_int($col) && ($c = array_search($col,$this->db_cols)))
870
					{
871
						$col = $this->table_name . '.' . $c;
872
					}
873
					if(is_int($col))
874
					{
875
						$db_filter[] = $val;
876
					}
877
					elseif ($val === "!''")
878
					{
879
						$db_filter[] = $col." != ''";
880
					}
881
					else
882
					{
883
						$db_filter[$col] = $val;
884
					}
885
				}
886
			}
887
			if ($query)
888
			{
889
				if ($op != 'AND')
890
				{
891
					$db_filter[] = '('.$this->db->column_data_implode(' '.$op.' ',$query).')';
892
				}
893
				else
894
				{
895
					$db_filter = array_merge($db_filter,$query);
896
				}
897
			}
898
			$query = $db_filter;
899
		}
900
		if ((int) $this->debug >= 4)
901
		{
902
			echo "<p>so_sql::search(,only_keys=$only_keys,order_by='$order_by',wildcard='$wildcard',empty=$empty,$op,start='$start',".print_r($filter,true).") query=".print_r($query,true).", total='$this->total'</p>\n";
903
			echo "<br>criteria = "; _debug_array($criteria);
904
		}
905
		if ($only_keys === true)
906
		{
907
			$colums = array_keys($this->db_key_cols);
908
			foreach($colums as &$column)
909
			{
910
				$column = $this->table_name . '.' . $column;
911
			}
912
		}
913
		elseif (is_array($only_keys))
914
		{
915
			$colums = array();
916
			foreach($only_keys as $key => $col)
917
			{
918
				//Convert ambiguous columns to prefixed tablename.column name
919
				$colums[] = ($db_col = array_search($col,$this->db_cols)) ?  $this->table_name .'.'.$db_col.' AS '.$col :$col;
920
			}
921
		}
922
		elseif (!$only_keys)
923
		{
924
			$colums = '*';
925
		}
926
		else
927
		{
928
			$colums = $only_keys;
929
		}
930
		if ($extra_cols)
931
		{
932
			if (!is_array($colums))
933
			{
934
				$colums .= ','.(is_array($extra_cols) ? implode(',', $extra_cols) : $extra_cols);
935
			}
936
			else
937
			{
938
				$colums = array_merge($colums, is_array($extra_cols) ? $extra_cols : explode(',', $extra_cols));
939
			}
940
		}
941
942
		// add table-name to otherwise ambiguous id over which we join (incl. "AS id" to return it with the right name)
943
		if ($join && $this->autoinc_id)
944
		{
945
			if (is_array($colums) && ($key = array_search($this->autoinc_id, $colums)) !== false)
946
			{
947
				$colums[$key] = $this->table_name.'.'.$this->autoinc_id.' AS '.$this->autoinc_id;
948
			}
949
			elseif (!is_array($colums) && strpos($colums,$this->autoinc_id) !== false)
950
			{
951
				$colums = preg_replace('/(?<! AS)([ ,]+)'.preg_quote($this->autoinc_id).'([ ,]+)/','\\1'.$this->table_name.'.'.$this->autoinc_id.' AS '.$this->autoinc_id.'\\2',$colums);
952
			}
953
		}
954
		$num_rows = 0;	// as spec. in max_matches in the user-prefs
955
		if (is_array($start)) list($start,$num_rows) = $start;
956
957
		// fix GROUP BY clause to contain all non-aggregate selected columns
958
		if ($order_by && stripos($order_by,'GROUP BY') !== false)
959
		{
960
			$order_by = $this->fix_group_by_columns($order_by, $colums, $this->table_name, $this->autoinc_id);
961
		}
962
		elseif ($order_by && stripos($order_by,'ORDER BY')===false && stripos($order_by,'GROUP BY')===false && stripos($order_by,'HAVING')===false)
963
		{
964
			$order_by = 'ORDER BY '.$order_by;
965
		}
966
		if (is_array($colums))
967
		{
968
			$colums = implode(',', $colums);
969
		}
970
		static $union = array();
971
		static $union_cols = array();
972
		if ($start === 'UNION' || $union)
973
		{
974
			if ($start === 'UNION')
975
			{
976
				$union[] = array(
977
					'table'  => $this->table_name,
978
					'cols'   => $colums,
979
					'where'  => $query,
980
					'append' => $order_by,
981
					'join'   => $join,
982
				);
983
				if (!$union_cols)	// union used the colum-names of the first query
984
				{
985
					$union_cols = $this->_get_columns($only_keys,$extra_cols);
986
				}
987
				return true;	// waiting for further calls, before running the union-query
988
			}
989
			// running the union query now
990
			if ($start !== false)	// need to get the total too, saved in $this->total
991
			{
992
				if ($this->db->Type == 'mysql' && $this->db->ServerInfo['version'] >= 4.0)
993
				{
994
					$union[0]['cols'] = ($mysql_calc_rows = 'SQL_CALC_FOUND_ROWS ').$union[0]['cols'];
995
				}
996
				else	// cant do a count, have to run the query without limit
997
				{
998
					$this->total = $this->db->union($union,__LINE__,__FILE__)->NumRows();
999
				}
1000
			}
1001
			$rs = $this->db->union($union,__LINE__,__FILE__,$order_by,$start,$num_rows);
1002
			if ($this->debug) error_log(__METHOD__."() ".$this->db->Query_ID->sql);
1003
1004
			$cols = $union_cols;
1005
			$union = $union_cols = array();
1006
		}
1007
		else	// no UNION
1008
		{
1009
			if ($start !== false)	// need to get the total too, saved in $this->total
1010
			{
1011
				if ($this->db->Type == 'mysql' && $this->db->ServerInfo['version'] >= 4.0)
1012
				{
1013
					$mysql_calc_rows = 'SQL_CALC_FOUND_ROWS ';
1014
				}
1015
				elseif (!$need_full_no_count && (!$join || stripos($join,'LEFT JOIN')!==false))
1016
				{
1017
					$this->total = $this->db->select($this->table_name,'COUNT(*)',$query,__LINE__,__FILE__,false,'',$this->app,0,$join)->fetchColumn();
1018
				}
1019
				else	// cant do a count, have to run the query without limit
1020
				{
1021
					$this->total = $this->db->select($this->table_name,$colums,$query,__LINE__,__FILE__,false,$order_by,false,0,$join)->NumRows();
1022
				}
1023
			}
1024
			$rs = $this->db->select($this->table_name,$mysql_calc_rows.$colums,$query,__LINE__,__FILE__,
1025
				$start,$order_by,$this->app,$num_rows,$join);
1026
			if ($this->debug) error_log(__METHOD__."() ".$this->db->Query_ID->sql);
1027
			$cols = $this->_get_columns($only_keys,$extra_cols);
1028
		}
1029
		if ((int) $this->debug >= 4) echo "<p>sql='{$this->db->Query_ID->sql}'</p>\n";
1030
1031
		if ($mysql_calc_rows)
1032
		{
1033
			$this->total = $this->db->query('SELECT FOUND_ROWS()')->fetchColumn();
1034
		}
1035
		// ToDo: Implement that as an iterator, as $rs is also an interator and we could return one instead of an array
1036
		if ($this->search_return_iterator)
1037
		{
1038
			return new Db2DataIterator($this,$rs);
1039
		}
1040
		$arr = array();
1041
		$n = 0;
1042
		if ($rs) foreach($rs as $row)
0 ignored issues
show
introduced by
$rs is of type EGroupware\Api\ADORecordSet, thus it always evaluated to true.
Loading history...
1043
		{
1044
			$data = array();
1045
			foreach($cols as $db_col => $col)
1046
			{
1047
				$data[$col] = (isset($row[$db_col]) ? $row[$db_col] : $row[$col]);
1048
			}
1049
			$arr[] = $this->db2data($data);
1050
			$n++;
1051
		}
1052
		return $n ? $arr : null;
1053
	}
1054
1055
	/**
1056
	 * Parse an array of search criteria into something that can be passed on
1057
	 * to the DB
1058
	 *
1059
	 * @param array $_criteria
1060
	 * @param string $wildcard ='' appended befor and after each criteria
1061
	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
1062
	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
1063
	 * @return Array
1064
	 * @throws Api\Db\Exception
1065
	 */
1066
	protected function parse_search(Array $_criteria, $wildcard, $empty, $op)
1067
	{
1068
		$criteria = $this->data2db($_criteria);
1069
		foreach($criteria as $col => $val)
1070
		{
1071
			if (is_int($col))
1072
			{
1073
				$query[] = $val;
1074
			}
1075
			elseif ($empty || $val != '')
1076
			{
1077
				if (!($db_col = array_search($col,$this->db_cols)))
1078
				{
1079
					$db_col = $col;
1080
				}
1081
				if ($val === '')
1082
				{
1083
					if (isset($this->table_def['fd'][$db_col]) &&
1084
						$this->table_def['fd'][$db_col]['type'] == 'varchar' &&
1085
						$this->table_def['fd'][$db_col]['nullable'] !== false)
1086
					{
1087
						unset($criteria[$col]);
1088
						$query[] =  '(' . $db_col . ' IS NULL OR ' . $db_col . " = '')";
1089
					}
1090
					else
1091
					{
1092
						$query[$db_col] = '';
1093
					}
1094
				}
1095
				elseif ($wildcard || $criteria[$col][0] == '!' ||
1096
					is_string($criteria[$col]) && (strpos($criteria[$col],'*')!==false || strpos($criteria[$col],'?')!==false))
1097
				{
1098
					// if search pattern alread contains a wildcard, do NOT add further ones automatic
1099
					if (is_string($criteria[$col]) && (strpos($criteria[$col],'*')!==false || strpos($criteria[$col],'?')!==false))
1100
					{
1101
						$wildcard = '';
1102
					}
1103
					$cmp_op = ' '.$this->db->capabilities['case_insensitive_like'].' ';
1104
					$negate = false;
1105
					if ($criteria[$col][0] == '!')
1106
					{
1107
						$cmp_op = ' NOT'.$cmp_op;
1108
						$criteria[$col] = substr($criteria[$col],1);
1109
						$negate = true;
1110
					}
1111
					foreach(explode(' ',$criteria[$col]) as $crit)
1112
					{
1113
						$query[] = ($negate ? ' ('.$db_col.' IS NULL OR ' : '').$db_col.$cmp_op.
1114
							$this->db->quote($wildcard.str_replace(array('%','_','*','?'),array('\\%','\\_','%','_'),$crit).$wildcard).
1115
							($negate ? ') ' : '');
1116
					}
1117
				}
1118
				elseif (strpos($db_col,'.') !== false)	// we have a table-name specified
1119
				{
1120
					list($table,$only_col) = explode('.',$db_col);
1121
					$type = $this->db->get_column_attribute($only_col, $table, true, 'type');
1122
					if (empty($type))
1123
					{
1124
						throw new Api\Db\Exception("Can not determine type of column '$only_col' in table '$table'!");
1125
					}
1126
					if (is_array($val) && count($val) > 1)
1127
					{
1128
						foreach($val as &$v)
1129
						{
1130
							$v = $this->db->quote($v, $type);
1131
						}
1132
						$query[] = $sql = $db_col.' IN (' .implode(',',$val).')';
0 ignored issues
show
Unused Code introduced by
The assignment to $sql is dead and can be removed.
Loading history...
1133
					}
1134
					else
1135
					{
1136
						$query[] = $db_col.'='.$this->db->quote(is_array($val)?array_shift($val):$val,$type);
1137
					}
1138
				}
1139
				else
1140
				{
1141
					$query[$db_col] = $criteria[$col];
1142
				}
1143
			}
1144
		}
1145
		if (is_array($query) && $op != 'AND') $query = $this->db->column_data_implode(' '.$op.' ',$query);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $query seems to be defined by a foreach iteration on line 1069. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1146
		return $query;
1147
	}
1148
1149
	/**
1150
	 * Fix GROUP BY clause to contain all non-aggregate selected columns
1151
	 *
1152
	 * No need to call for MySQL because MySQL does NOT give an error in above case.
1153
	 * (Of cause developer has to make sure to group by enough columns, eg. a unique key, for selected columns to be defined.)
1154
	 *
1155
	 * MySQL also does not allow to use [tablename.]* in GROUP BY, which PostgreSQL allows!
1156
	 * (To use this for MySQL too, we would have to replace * with all columns of a table.)
1157
	 *
1158
	 * @param string $group_by [GROUP BY ...[HAVING ...]][ORDER BY ...]
1159
	 * @param string|array $columns better provide an array as exploding by comma can lead to error with functions containing one
1160
	 * @param string $table_name table-name
1161
	 * @param string $autoinc_id id-column
1162
	 * @return string
1163
	 */
1164
	public static function fix_group_by_columns($group_by, &$columns, $table_name, $autoinc_id)
1165
	{
1166
		$matches = null;
1167
		if (substr($GLOBALS['egw']->db->Type, 0, 5) == 'mysql' || !preg_match('/(GROUP BY .*)(HAVING.*|ORDER BY.*)?$/iU', $group_by, $matches))
1168
		{
1169
			return $group_by;	// nothing to do
1170
		}
1171
		$changes = 0;
1172
		$group_by_cols = preg_split('/, */', trim(substr($matches[1], 9)));
1173
1174
		if (!is_array($columns))
1175
		{
1176
			$columns = preg_split('/, */', $columns);
1177
1178
			// fix columns containing commas as part of function calls
1179
			for($n = 0; $n < count($columns); ++$n)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1180
			{
1181
				$col =& $columns[$n];
1182
				while (substr_count($col, '(') > substr_count($col, ')') && ++$n < count($columns))
1183
				{
1184
					$col .= ','.$columns[$n];
1185
					unset($columns[$n]);
1186
				}
1187
			}
1188
			unset($col);
1189
		}
1190
		foreach($columns as $n => $col)
1191
		{
1192
			if ($col == '*')
1193
			{
1194
				// MySQL does NOT allow to GROUP BY table.*
1195
				$col = $columns[$n] = $table_name.'.'.($GLOBALS['egw']->db->Type == 'mysql' ? $autoinc_id : '*');
1196
				++$changes;
1197
			}
1198
			// only check columns and non-aggregate functions
1199
			if (strpos($col, '(') === false || !preg_match('/(COUNT|MIN|MAX|AVG|SUM|BIT_[A-Z]+|STD[A-Z_]*|VAR[A-Z_]*|ARRAY_AGG)\(/i', $col))
1200
			{
1201
				if (($pos = stripos($col, 'DISTINCT ')) !== false)
1202
				{
1203
					$col = substr($col, $pos+9);
1204
				}
1205
				$alias = $col;
1206
				if (stripos($col, ' AS ')) list($col, $alias) = preg_split('/ +AS +/i', $col);
1207
				// do NOT group by constant expressions
1208
				if (preg_match('/^ *(-?[0-9]+|".*"|\'.*\'|NULL) *$/i', $col)) continue;
1209
				if (!in_array($col, $group_by_cols) && !in_array($alias, $group_by_cols))
1210
				{
1211
					// instead of aliased primary key, we have to use original column incl. table-name as alias is ambigues
1212
					$group_by_cols[] = $col == $table_name.'.'.$autoinc_id ? $col : $alias;
1213
					//error_log(__METHOD__."() col=$col, alias=$alias --> group_by_cols=".array2string($group_by_cols));
1214
					++$changes;
1215
				}
1216
			}
1217
		}
1218
		$ret = $group_by;
1219
		if ($changes)
1220
		{
1221
			$ret = str_replace($matches[1], 'GROUP BY '.implode(',', $group_by_cols).' ',  $group_by);
1222
			//error_log(__METHOD__."('$group_by', ".array2string($columns).") group_by_cols=".array2string($group_by_cols)." changed to $ret");
1223
		}
1224
		return $ret;
1225
	}
1226
1227
	/**
1228
	 * Return criteria array for a given search pattern
1229
	 *
1230
	 * We handle quoted text, wildcards and boolean operators (+/-, AND/OR).  If
1231
	 * the pattern is '#' followed by an integer, the search is limited to just
1232
	 * the primary key.
1233
	 *
1234
	 * @param string $_pattern search pattern incl. * or ? as wildcard, if no wildcards used we append and prepend one!
1235
	 * @param string &$wildcard ='' on return wildcard char to use, if pattern does not already contain wildcards!
1236
	 * @param string &$op ='AND' on return boolean operation to use, if pattern does not start with ! we use OR else AND
1237
	 * @param string $extra_col =null extra column to search
1238
	 * @param array $search_cols =array() List of columns to search.  If not provided, all columns in $this->db_cols will be considered
1239
	 * @return array or column => value pairs
1240
	 */
1241
	public function search2criteria($_pattern,&$wildcard='',&$op='AND',$extra_col=null, $search_cols = array())
1242
	{
1243
		$pattern = trim($_pattern);
1244
		// This function can get called multiple times.  Make sure it doesn't re-process.
1245
		if (empty($pattern) || is_array($pattern)) return $pattern;
1246
		if(strpos($pattern, 'CAST(COALESCE(') !== false)
1247
		{
1248
			return $pattern;
1249
		}
1250
1251
		$criteria = array();
1252
		$filter = array();
1253
		$columns = array();
1254
1255
		/*
1256
		* Special handling for numeric columns.  They are only considered if the pattern is numeric.
1257
		* If the pattern is numeric, an equality search is used instead.
1258
		*/
1259
		$numeric_types = array('auto', 'int', 'float', 'double', 'decimal');
1260
		$numeric_columns = array();
1261
1262
		// Special handling for an ID search, #<int>
1263
		if(strpos($_pattern, '#') === 0 && is_numeric(substr($_pattern, 1)))
1264
		{
1265
			return array('(' . $this->table_name.'.'. $this->autoinc_id . '=' . (int)substr($_pattern,1) . ')');
1266
		}
1267
		if(!$search_cols)
0 ignored issues
show
Bug Best Practice introduced by
The expression $search_cols 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...
1268
		{
1269
			$search_cols = $this->get_default_search_columns();
1270
		}
1271
		// Concat all fields to be searched together, so the conditions operate across the whole record
1272
		foreach($search_cols as $col)
1273
		{
1274
			$col_name = $col;
1275
			$table = $this->table_name;
1276
			if (strpos($col,'.') !== false)
1277
			{
1278
				list($table,$col_name) = explode('.',$col);
1279
			}
1280
			$table_def = $table == $this->table_name ? $this->table_def : $this->db->get_table_definitions(true,$table);
1281
			if ($table_def['fd'][$col_name] && in_array($table_def['fd'][$col_name]['type'], $numeric_types))
1282
			{
1283
				$numeric_columns[] = $col;
1284
				continue;
1285
			}
1286
			if ($this->db->Type == 'mysql' && $table_def['fd'][$col_name]['type'] === 'ascii' && preg_match('/[\x80-\xFF]/', $_pattern))
1287
			{
1288
				continue;	// will only give sql error
1289
			}
1290
			$columns[] = sprintf($this->db->capabilities[Api\Db::CAPABILITY_CAST_AS_VARCHAR],"COALESCE($col,'')");
1291
		}
1292
		if(!$columns)
1293
		{
1294
			return array();
1295
		}
1296
1297
		// Break the search string into tokens
1298
		$break = ' ';
1299
		$token = strtok($pattern, $break);
1300
1301
		while($token)
1302
		{
1303
			if($token == strtoupper(lang('AND')) || $token == 'AND')
1304
			{
1305
				$token = '+'.strtok($break);
1306
			}
1307
			elseif ($token == strtoupper(lang('OR')) || $token == 'OR')
1308
			{
1309
				$token = strtok($break);
1310
				continue;
1311
			}
1312
			elseif ($token == strtoupper(lang('NOT')) || $token == 'NOT')
1313
			{
1314
				$token = '-'.strtok($break);
1315
			}
1316
			if ($token[0]=='"')
1317
			{
1318
				$token = substr($token, 1,strlen($token));
1319
 				if(substr($token, -1) != '"')
1320
				{
1321
					$token .= ' '.strtok('"');
1322
				}
1323
				else
1324
				{
1325
					$token = substr($token, 0, -1);
1326
				}
1327
			}
1328
1329
			// prepend and append extra wildcard %, if pattern does NOT already contain wildcards
1330
			if (strpos($token,'*') === false && strpos($token,'?') === false)
1331
			{
1332
				$wildcard = '%';	// if pattern contains no wildcards, add them before AND after the pattern
1333
			}
1334
			else
1335
			{
1336
				$wildcard = '';		// no extra wildcard, if pattern already contains some
1337
			}
1338
1339
			switch($token[0])
1340
			{
1341
				case '+':
1342
					$op = 'AND';
1343
					$token = substr($token, 1, strlen($token));
1344
					break;
1345
				case '-':
1346
				case '!':
1347
					$op = 'NOT';
1348
					$token = substr($token, 1, strlen($token));
1349
					break;
1350
				default:
1351
					$op = 'OR';
1352
					break;
1353
			}
1354
			$search_token = $wildcard.str_replace(array('%','_','*','?'),array('\\%','\\_','%','_'),$token).$wildcard;
1355
			$token_filter = ' '.call_user_func_array(array($GLOBALS['egw']->db,'concat'),$columns).' '.
1356
				$this->db->capabilities['case_insensitive_like'] . ' ' .
1357
				$GLOBALS['egw']->db->quote($search_token);
1358
1359
			// if we have customfields and this is Api\Storage (not Api\Storage\Base)
1360
			if (is_a($this, __NAMESPACE__))
1361
			{
1362
				// add custom-field search: OR id IN (SELECT id FROM extra_table WHERE extra_value LIKE '$search_token')
1363
				$token_filter .= $this->cf_match($search_token);
1364
			}
1365
1366
			// Compare numeric token as equality for numeric columns
1367
			// skip user-wildcards (*,?) in is_numeric test, but not SQL wildcards, which get escaped and give sql-error
1368
			if (is_numeric(str_replace(array('*','?'), '', $token)))
1369
			{
1370
				$numeric_filter = array();
1371
				foreach($numeric_columns as $col)
1372
				{
1373
					if($wildcard == '')
1374
					{
1375
						// Token has a wildcard from user, use LIKE
1376
						$numeric_filter[] = "($col IS NOT NULL AND CAST($col AS CHAR) " .
1377
							$this->db->capabilities['case_insensitive_like'] . ' ' .
1378
							$GLOBALS['egw']->db->quote(str_replace(array('*','?'), array('%','_'), $token)) . ')';
1379
					}
1380
					else
1381
					{
1382
						$numeric_filter[] = "($col IS NOT NULL AND $col = $token)";
1383
					}
1384
				}
1385
				if(count($numeric_filter) > 0)
1386
				{
1387
					$token_filter .= ' OR ' . implode(' OR ', $numeric_filter);
1388
				}
1389
			}
1390
			$criteria[$op][] = '('.$token_filter.')';
1391
1392
			$token = strtok($break);
1393
		}
1394
1395
		if($criteria['NOT'])
1396
		{
1397
			$filter[] = 'NOT (' . implode(' OR ', $criteria['NOT']) . ') ';
1398
		}
1399
		if($criteria['AND'])
1400
		{
1401
			$filter[] = implode(' AND ', $criteria['AND']) . ' ';
1402
		}
1403
		if($criteria['OR'])
1404
		{
1405
			$filter[] = '(' . implode(' OR ', $criteria['OR']) . ') ';
1406
		}
1407
1408
		if(count($filter))
1409
		{
1410
			$result = '(' . implode(' AND ', $filter) . ')';
1411
		}
1412
1413
		// OR extra column on the end so a null or blank won't block a hit in the main columns
1414
		if ($extra_col)
1415
		{
1416
			$result .= (strlen($result) ? ' OR ' : ' ') . "$extra_col = " . $GLOBALS['egw']->db->quote($pattern);
1417
		}
1418
1419
		$op = 'OR';
1420
		return array('(' . $result . ')');
1421
	}
1422
1423
	/**
1424
	* Get a default list of columns to search
1425
	* This is to be used as a fallback, for when the extending class does not define
1426
	* $this->columns_to_search.  All the columns are considered, and any with $skip_columns_with in
1427
	* their name are discarded because these columns are expected to be foreign keys or other numeric
1428
	* values with no meaning to the user.
1429
	*
1430
	* @return array of column names
1431
	*/
1432
	protected function get_default_search_columns()
1433
	{
1434
		$skip_columns_with = array('_id', 'modified', 'modifier', 'status', 'cat_id', 'owner');
1435
		$search_cols = is_null($this->columns_to_search) ? $this->db_cols : $this->columns_to_search;
0 ignored issues
show
introduced by
The condition is_null($this->columns_to_search) is always false.
Loading history...
1436
		$numeric_types = array('auto', 'int', 'float', 'double');
1437
1438
		// Skip some numeric columns that don't make sense to search if we have to default to all columns
1439
		if(is_null($this->columns_to_search))
0 ignored issues
show
introduced by
The condition is_null($this->columns_to_search) is always false.
Loading history...
1440
		{
1441
			foreach($search_cols as $key => &$col)
1442
			{
1443
				// If the name as given isn't a real column name, and adding the prefix doesn't help, skip it
1444
				if(!$this->table_def['fd'][$col] && !($col = $this->prefix.array_search($col, $search_cols))) {
1445
					// Can't search this column
1446
					unset($search_cols[$key]);
1447
					continue;
1448
				}
1449
				if(in_array($this->table_def['fd'][$col]['type'], $numeric_types))
1450
				{
1451
					foreach($skip_columns_with as $bad)
1452
					{
1453
						if(strpos($col, $bad) !== false)
1454
						{
1455
							unset($search_cols[$key]);
1456
							continue 2;
1457
						}
1458
					}
1459
				}
1460
				// Prefix with table name to avoid ambiguity
1461
				$col = $this->table_name.'.'.$col;
1462
			}
1463
		}
1464
		return $search_cols;
1465
	}
1466
1467
	/**
1468
	 * extract the requested columns from $only_keys and $extra_cols param of a search
1469
	 *
1470
	 * @internal
1471
	 * @param boolean|string $only_keys =true True returns only keys, False returns all cols. comma seperated list of keys to return
1472
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
1473
	 * @return array with columns as db-name => internal-name pairs
1474
	 */
1475
	function _get_columns($only_keys,$extra_cols)
1476
	{
1477
		//echo "_get_columns() only_keys="; _debug_array($only_keys); echo "extra_cols="; _debug_array($extra_cols);
1478
		if ($only_keys === true)	// only primary key
1479
		{
1480
			$cols = $this->db_key_cols;
1481
		}
1482
		else
1483
		{
1484
			$cols = array();
1485
			$distinct_checked = false;
1486
			foreach(is_array($only_keys) ? $only_keys : explode(',', $only_keys) as $col)
1487
			{
1488
				if (!$distinct_checked)
1489
				{
1490
					if (stripos($col, 'DISTINCT ') === 0) $col = substr($col, 9);
1491
					$distinct_checked = true;
1492
				}
1493
				if (!$col || $col == '*' || $col == $this->table_name.'.*')	// all columns
1494
				{
1495
					$cols = array_merge($cols,$this->db_cols);
1496
				}
1497
				else	// only the specified columns
1498
				{
1499
					if (stripos($col,'as'))	// if there's already an explicit naming of the column, just use it
1500
					{
1501
						$col = preg_replace('/^.*as +([a-z0-9_]+) *$/i','\\1',$col);
1502
						$cols[$col] = $col;
1503
						continue;
1504
					}
1505
					if (($db_col = array_search($col,$this->db_cols)) !== false)
1506
					{
1507
						$cols[$db_col] = $col;
1508
					}
1509
					else
1510
					{
1511
						$cols[$col] = isset($this->db_cols[$col]) ? $this->db_cols[$col] : $col;
1512
					}
1513
				}
1514
			}
1515
		}
1516
		if ($extra_cols)	// extra columns to report
1517
		{
1518
			foreach(is_array($extra_cols) ? $extra_cols : explode(',',$extra_cols) as $col)
1519
			{
1520
				if (stripos($col,'as ')!==false) $col = preg_replace('/^.*as +([a-z0-9_]+) *$/i','\\1',$col);
1521
				if (($db_col = array_search($col,$this->db_cols)) !== false)
1522
				{
1523
					$cols[$db_col] = $col;
1524
				}
1525
				else
1526
				{
1527
					$cols[$col] = isset($this->db_cols[$col]) ? $this->db_cols[$col] : $col;
1528
				}
1529
			}
1530
		}
1531
		return $cols;
1532
	}
1533
1534
	/**
1535
	 * query rows for the nextmatch widget
1536
	 *
1537
	 * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter'
1538
	 *	For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class.
1539
	 * @param array &$rows returned rows/competitions
1540
	 * @param array &$readonlys eg. to disable buttons based on acl, not use here, maybe in a derived class
1541
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
1542
	 *	"LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
1543
	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
1544
	 * @param mixed $only_keys =false, see search
1545
	 * @param string|array $extra_cols =array()
1546
	 * @return int total number of rows
1547
	 */
1548
	function get_rows($query,&$rows,&$readonlys,$join='',$need_full_no_count=false,$only_keys=false,$extra_cols=array())
1549
	{
1550
		unset($readonlys);	// required by function signature, but not used in this default implementation
1551
		if ((int) $this->debug >= 4)
1552
		{
1553
			echo "<p>so_sql::get_rows(".print_r($query,true).",,)</p>\n";
1554
		}
1555
		$criteria = array();
1556
		$op = 'AND';
1557
		if ($query['search'])
1558
		{
1559
			$criteria = $query['search'];
1560
		}
1561
		$rows = $this->search($criteria,$only_keys,$query['order']?$query['order'].' '.$query['sort']:'',$extra_cols,
1562
			'',false,$op,$query['num_rows']?array((int)$query['start'],$query['num_rows']):(int)$query['start'],
1563
			$query['col_filter'],$join,$need_full_no_count);
1564
1565
		if (!$rows) $rows = array();	// otherwise false returned from search would be returned as array(false)
1566
1567
		return $this->total;
1568
	}
1569
1570
	/**
1571
	 * Check if values for unique keys and the primary keys are unique are unique
1572
	 *
1573
	 * @param array $data =null data-set to check, defaults to $this->data
1574
	 * @return int 0: all keys are unique, 1: first key not unique, 2: ...
1575
	 */
1576
	function not_unique($data=null)
1577
	{
1578
		if (!is_array($data))
1579
		{
1580
			$data = $this->data;
1581
		}
1582
		$n = 1;
1583
		$uni_keys = $this->db_uni_cols;
1584
		// add the primary key, only if it's NOT an auto id
1585
		if (!$this->autoinc_id)
1586
		{
1587
			$uni_keys[] = $this->db_key_cols;
1588
		}
1589
		foreach($uni_keys as $db_col => $col)
1590
		{
1591
			if (is_array($col))
1592
			{
1593
				$query = array();
1594
				foreach($col as $db_c => $c)
1595
				{
1596
					$query[$db_c] = $data[$c];
1597
				}
1598
			}
1599
			else
1600
			{
1601
				$query = array($db_col => $data[$col]);
1602
			}
1603
			foreach($this->db->select($this->table_name,$this->db_key_cols,$query,__LINE__,__FILE__,false,'',$this->app) as $other)
1604
			{
1605
				foreach($this->db_key_cols as $key_col)
1606
				{
1607
					if ($data[$key_col] != $other[$key_col])
1608
					{
1609
						if ((int) $this->debug >= 4)
1610
						{
1611
							echo "<p>not_unique in ".array2string($col)." as for '$key_col': '${data[$key_col]}' != '${other[$key_col]}'</p>\n";
1612
						}
1613
						return $n;	// different entry => $n not unique
1614
					}
1615
				}
1616
			}
1617
			++$n;
1618
		}
1619
		return 0;
1620
	}
1621
1622
	/**
1623
	 * Query DB for a list / array with one colum as key and an other one(s) as value, eg. id => title pairs
1624
	 *
1625
	 * We do some caching as these kind of function is usualy called multiple times, eg. for option-lists.
1626
	 *
1627
	 * @param string $value_col array of column-names for the values of the array, can also be an expression aliased with AS,
1628
	 *	if more then one column given, an array with keys identical to the given ones is returned and not just the value of the column
1629
	 * @param string $key_col ='' column-name for the keys, default '' = same as (first) $value_col: returns a distinct list
1630
	 * @param array $filter =array() to filter the entries
1631
	 * @param string $order ='' order, default '' = same as (first) $value_col
1632
	 * @return array with key_col => value_col pairs or array if more then one value_col given (keys as in value_col)
1633
	 */
1634
	function query_list($value_col,$key_col='',$filter=array(),$order='')
1635
	{
1636
		static $cache = array();
1637
1638
		$cache_key = serialize($value_col).'-'.$key_col.'-'.serialize($filter).'-'.$order;
1639
1640
		if (isset($cache[$cache_key]))
1641
		{
1642
			return $cache[$cache_key];
1643
		}
1644
		if (!is_array($value_col)) $value_col = array($value_col);
0 ignored issues
show
introduced by
The condition is_array($value_col) is always false.
Loading history...
1645
1646
		$cols = $ret = array();
1647
		foreach($value_col as $key => $col)
1648
		{
1649
			$matches = null;
1650
			$cols[$key] = preg_match('/AS ([a-z_0-9]+)$/i',$col,$matches) ? $matches[1] : $col;
1651
		}
1652
		if (!$order) $order = current($cols);
1653
1654
		if (($search =& $this->search(array(),($key_col ? $key_col.',' : 'DISTINCT ').implode(',',$value_col),$order,'','',false,'AND',false,$filter)))
1655
		{
1656
			if (preg_match('/AS ([a-z_0-9]+)$/i',$key_col,$matches))
1657
			{
1658
				$key_col = $matches[1];
1659
			}
1660
			elseif (!$key_col)
1661
			{
1662
				$key_col = current($cols);
1663
			}
1664
			foreach($search as $row)
1665
			{
1666
				if (count($cols) > 1)
1667
				{
1668
					$data = array();
1669
					foreach($cols as $key => $col)
1670
					{
1671
						$data[$key] = $row[$col];
1672
					}
1673
				}
1674
				else
1675
				{
1676
					$data = $row[current($cols)];
1677
				}
1678
				if ($data) $ret[$row[$key_col]] = $data;
1679
			}
1680
		}
1681
		return $cache[$cache_key] =& $ret;
1682
	}
1683
1684
	/**
1685
	 * Get comments for all columns or a specific one
1686
	 *
1687
	 * @param string $column =null name of column or null for all (default)
1688
	 * @return array|string array with internal-name => comment pairs, or string with comment, if $column given
1689
	 */
1690
	public function get_comments($column=null)
1691
	{
1692
		static $comments=null;
1693
1694
		if (is_null($comments))
1695
		{
1696
			foreach($this->db_cols as $db_col => $col)
1697
			{
1698
				$comments[$col] = $this->table_def['fd'][$db_col]['comment'];
1699
			}
1700
		}
1701
		return is_null($column) ? $comments : $comments[$column];
1702
	}
1703
}
1704