Passed
Push — 16.1 ( 6e101a...277864 )
by Ralf
18:52
created

Base::init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
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
 	/**
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
31
 	 * need to be set in the derived class to the db-table-name
32
 	 *
33
 	 * @var string
34
 	 */
35
	var $table_name;
36
 	/**
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
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
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 boolean. 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);
0 ignored issues
show
Documentation Bug introduced by
It seems like \EGroupware\Api\DateTime...Api\DateTime::DATABASE) can also be of type array or object<EGroupware\Api\DateTime>. However, the property $now is declared as type integer|string|object<EG...e\Api\Storage\DateTime>. 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...
234
				break;
235
			default:
236
				$this->now = Api\DateTime::to('now','ts');
0 ignored issues
show
Documentation Bug introduced by
It seems like \EGroupware\Api\DateTime::to('now', 'ts') can also be of type array or object<EGroupware\Api\DateTime>. However, the property $now is declared as type integer|string|object<EG...e\Api\Storage\DateTime>. 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...
237
		}
238
		$this->tz_offset_s = Api\DateTime::tz_offset_s();
0 ignored issues
show
Documentation Bug introduced by
It seems like \EGroupware\Api\DateTime::tz_offset_s() can also be of type double. However, the property $tz_offset_s is declared as type integer. 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...
Deprecated Code introduced by
The property EGroupware\Api\Storage\Base::$tz_offset_s has been deprecated with message: use Api\DateTime methods instead, as the offset between user and server time is only valid for current time

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);
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $col can also be of type integer. However, the property $autoinc_id 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...
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 View Code Duplication
		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)
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);
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)
442
		{
443 View Code Duplication
			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))
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
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 View Code Duplication
					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 View Code Duplication
				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)
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: $nothing_affected = (!$t...s->db->affected_rows()), Probably Intended Meaning: ($nothing_affected = !$t...is->db->affected_rows()
Loading history...
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;
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
			$criteria = $this->data2db($criteria);
854
			foreach($criteria as $col => $val)
855
			{
856
				if (is_int($col))
857
				{
858
					$query[] = $val;
859
				}
860
				elseif ($empty || $val != '')
861
				{
862
					if (!($db_col = array_search($col,$this->db_cols)))
863
					{
864
						$db_col = $col;
865
					}
866
					if ($val === '')
867
					{
868
						if (isset($this->table_def['fd'][$db_col]) &&
869
							$this->table_def['fd'][$db_col]['type'] == 'varchar' &&
870
							$this->table_def['fd'][$db_col]['nullable'] !== false)
871
						{
872
							unset($criteria[$col]);
873
							$query[] =  '(' . $db_col . ' IS NULL OR ' . $db_col . " = '')";
874
						}
875
						else
876
						{
877
							$query[$db_col] = '';
878
						}
879
					}
880
					elseif ($wildcard || $criteria[$col][0] == '!' ||
881
						is_string($criteria[$col]) && (strpos($criteria[$col],'*')!==false || strpos($criteria[$col],'?')!==false))
882
					{
883
						// if search pattern alread contains a wildcard, do NOT add further ones automatic
884
						if (is_string($criteria[$col]) && (strpos($criteria[$col],'*')!==false || strpos($criteria[$col],'?')!==false))
885
						{
886
							$wildcard = '';
887
						}
888
						$cmp_op = ' '.$this->db->capabilities['case_insensitive_like'].' ';
889
						$negate = false;
890
						if ($criteria[$col][0] == '!')
891
						{
892
							$cmp_op = ' NOT'.$cmp_op;
893
							$criteria[$col] = substr($criteria[$col],1);
894
							$negate = true;
895
						}
896
						foreach(explode(' ',$criteria[$col]) as $crit)
897
						{
898
							$query[] = ($negate ? ' ('.$db_col.' IS NULL OR ' : '').$db_col.$cmp_op.
899
								$this->db->quote($wildcard.str_replace(array('%','_','*','?'),array('\\%','\\_','%','_'),$crit).$wildcard).
900
								($negate ? ') ' : '');
901
						}
902
					}
903
					elseif (strpos($db_col,'.') !== false)	// we have a table-name specified
904
					{
905
						list($table,$only_col) = explode('.',$db_col);
906
						$type = $this->db->get_column_attribute($only_col, $table, true, 'type');
907
						if (empty($type))
908
						{
909
							throw new Api\Db\Exception("Can not determine type of column '$only_col' in table '$table'!");
910
						}
911
						if (is_array($val) && count($val) > 1)
912
						{
913
							foreach($val as &$v)
914
							{
915
								$v = $this->db->quote($v, $type);
916
							}
917
							$query[] = $sql = $db_col.' IN (' .implode(',',$val).')';
918
						}
919
						else
920
						{
921
							$query[] = $db_col.'='.$this->db->quote(is_array($val)?array_shift($val):$val,$type);
922
						}
923
					}
924
					else
925
					{
926
						$query[$db_col] = $criteria[$col];
927
					}
928
				}
929
			}
930
			if (is_array($query) && $op != 'AND') $query = $this->db->column_data_implode(' '.$op.' ',$query);
931
		}
932
		if (is_array($filter))
933
		{
934
			$db_filter = array();
935
			$data2db_filter = $this->data2db($filter);
936
			if (!is_array($data2db_filter)) {
937
				echo function_backtrace()."<br/>\n";
938
				echo "filter=";_debug_array($filter);
939
				echo "data2db(filter)=";_debug_array($data2db_filter);
940
			}
941
			foreach($data2db_filter as $col => $val)
942
			{
943
				if ($val !== '')
944
				{
945
					// check if a db-internal name conversation necessary
946
					if (!is_int($col) && ($c = array_search($col,$this->db_cols)))
947
					{
948
						$col = $this->table_name . '.' . $c;
949
					}
950
					if(is_int($col))
951
					{
952
						$db_filter[] = $val;
953
					}
954
					elseif ($val === "!''")
955
					{
956
						$db_filter[] = $col." != ''";
957
					}
958
					else
959
					{
960
						$db_filter[$col] = $val;
961
					}
962
				}
963
			}
964
			if ($query)
965
			{
966
				if ($op != 'AND')
967
				{
968
					$db_filter[] = '('.$this->db->column_data_implode(' '.$op.' ',$query).')';
0 ignored issues
show
Bug introduced by
It seems like $query can also be of type string; however, EGroupware\Api\Db::column_data_implode() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
969
				}
970
				else
971
				{
972
					$db_filter = array_merge($db_filter,$query);
973
				}
974
			}
975
			$query = $db_filter;
976
		}
977
		if ((int) $this->debug >= 4)
978
		{
979
			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";
980
			echo "<br>criteria = "; _debug_array($criteria);
981
		}
982
		if ($only_keys === true)
983
		{
984
			$colums = array_keys($this->db_key_cols);
985
			foreach($colums as &$column)
986
			{
987
				$column = $this->table_name . '.' . $column;
988
			}
989
		}
990
		elseif (is_array($only_keys))
991
		{
992
			$colums = array();
993
			foreach($only_keys as $key => $col)
994
			{
995
				//Convert ambiguous columns to prefixed tablename.column name
996
				$colums[] = ($db_col = array_search($col,$this->db_cols)) ?  $this->table_name .'.'.$db_col.' AS '.$col :$col;
997
			}
998
		}
999
		elseif (!$only_keys)
1000
		{
1001
			$colums = '*';
1002
		}
1003
		else
1004
		{
1005
			$colums = $only_keys;
1006
		}
1007
		if ($extra_cols)
1008
		{
1009
			if (!is_array($colums))
1010
			{
1011
				$colums .= ','.(is_array($extra_cols) ? implode(',', $extra_cols) : $extra_cols);
1012
			}
1013
			else
1014
			{
1015
				$colums = array_merge($colums, is_array($extra_cols) ? $extra_cols : explode(',', $extra_cols));
1016
			}
1017
		}
1018
1019
		// add table-name to otherwise ambiguous id over which we join (incl. "AS id" to return it with the right name)
1020
		if ($join && $this->autoinc_id)
1021
		{
1022
			if (is_array($colums) && ($key = array_search($this->autoinc_id, $colums)) !== false)
1023
			{
1024
				$colums[$key] = $this->table_name.'.'.$this->autoinc_id.' AS '.$this->autoinc_id;
1025
			}
1026
			elseif (!is_array($colums) && strpos($colums,$this->autoinc_id) !== false)
1027
			{
1028
				$colums = preg_replace('/(?<! AS)([ ,]+)'.preg_quote($this->autoinc_id).'([ ,]+)/','\\1'.$this->table_name.'.'.$this->autoinc_id.' AS '.$this->autoinc_id.'\\2',$colums);
1029
			}
1030
		}
1031
		$num_rows = 0;	// as spec. in max_matches in the user-prefs
1032
		if (is_array($start)) list($start,$num_rows) = $start;
1033
1034
		// fix GROUP BY clause to contain all non-aggregate selected columns
1035
		if ($order_by && stripos($order_by,'GROUP BY') !== false)
1036
		{
1037
			$order_by = $this->fix_group_by_columns($order_by, $colums, $this->table_name, $this->autoinc_id);
1038
		}
1039
		elseif ($order_by && stripos($order_by,'ORDER BY')===false && stripos($order_by,'GROUP BY')===false && stripos($order_by,'HAVING')===false)
1040
		{
1041
			$order_by = 'ORDER BY '.$order_by;
1042
		}
1043
		if (is_array($colums))
1044
		{
1045
			$colums = implode(',', $colums);
1046
		}
1047
		static $union = array();
1048
		static $union_cols = array();
1049
		if ($start === 'UNION' || $union)
1050
		{
1051
			if ($start === 'UNION')
1052
			{
1053
				$union[] = array(
1054
					'table'  => $this->table_name,
1055
					'cols'   => $colums,
1056
					'where'  => $query,
1057
					'append' => $order_by,
1058
					'join'   => $join,
1059
				);
1060
				if (!$union_cols)	// union used the colum-names of the first query
1061
				{
1062
					$union_cols = $this->_get_columns($only_keys,$extra_cols);
1063
				}
1064
				return true;	// waiting for further calls, before running the union-query
1065
			}
1066
			// running the union query now
1067
			if ($start !== false)	// need to get the total too, saved in $this->total
1068
			{
1069
				if ($this->db->Type == 'mysql' && $this->db->ServerInfo['version'] >= 4.0)
1070
				{
1071
					$union[0]['cols'] = ($mysql_calc_rows = 'SQL_CALC_FOUND_ROWS ').$union[0]['cols'];
1072
				}
1073
				else	// cant do a count, have to run the query without limit
1074
				{
1075
					$this->total = $this->db->union($union,__LINE__,__FILE__)->NumRows();
1076
				}
1077
			}
1078
			$rs = $this->db->union($union,__LINE__,__FILE__,$order_by,$start,$num_rows);
1079
			if ($this->debug) error_log(__METHOD__."() ".$this->db->Query_ID->sql);
0 ignored issues
show
Bug introduced by
The property Query_ID does not seem to exist in EGroupware\Api\Db.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1080
1081
			$cols = $union_cols;
1082
			$union = $union_cols = array();
1083
		}
1084
		else	// no UNION
1085
		{
1086
			if ($start !== false)	// need to get the total too, saved in $this->total
1087
			{
1088
				if ($this->db->Type == 'mysql' && $this->db->ServerInfo['version'] >= 4.0)
1089
				{
1090
					$mysql_calc_rows = 'SQL_CALC_FOUND_ROWS ';
1091
				}
1092
				elseif (!$need_full_no_count && (!$join || stripos($join,'LEFT JOIN')!==false))
1093
				{
1094
					$this->total = $this->db->select($this->table_name,'COUNT(*)',$query,__LINE__,__FILE__,false,'',$this->app,0,$join)->fetchColumn();
1095
				}
1096
				else	// cant do a count, have to run the query without limit
1097
				{
1098
					$this->total = $this->db->select($this->table_name,$colums,$query,__LINE__,__FILE__,false,$order_by,false,0,$join)->NumRows();
1099
				}
1100
			}
1101
			$rs = $this->db->select($this->table_name,$mysql_calc_rows.$colums,$query,__LINE__,__FILE__,
1102
				$start,$order_by,$this->app,$num_rows,$join);
1103
			if ($this->debug) error_log(__METHOD__."() ".$this->db->Query_ID->sql);
1104
			$cols = $this->_get_columns($only_keys,$extra_cols);
1105
		}
1106
		if ((int) $this->debug >= 4) echo "<p>sql='{$this->db->Query_ID->sql}'</p>\n";
1107
1108
		if ($mysql_calc_rows)
1109
		{
1110
			$this->total = $this->db->query('SELECT FOUND_ROWS()')->fetchColumn();
1111
		}
1112
		// ToDo: Implement that as an iterator, as $rs is also an interator and we could return one instead of an array
1113
		if ($this->search_return_iterator)
1114
		{
1115
			return new Db2DataIterator($this,$rs);
1116
		}
1117
		$arr = array();
1118
		$n = 0;
1119
		if ($rs) foreach($rs as $row)
1120
		{
1121
			$data = array();
1122
			foreach($cols as $db_col => $col)
1123
			{
1124
				$data[$col] = (isset($row[$db_col]) ? $row[$db_col] : $row[$col]);
1125
			}
1126
			$arr[] = $this->db2data($data);
1127
			$n++;
1128
		}
1129
		return $n ? $arr : null;
1130
	}
1131
1132
	/**
1133
	 * Fix GROUP BY clause to contain all non-aggregate selected columns
1134
	 *
1135
	 * No need to call for MySQL because MySQL does NOT give an error in above case.
1136
	 * (Of cause developer has to make sure to group by enough columns, eg. a unique key, for selected columns to be defined.)
1137
	 *
1138
	 * MySQL also does not allow to use [tablename.]* in GROUP BY, which PostgreSQL allows!
1139
	 * (To use this for MySQL too, we would have to replace * with all columns of a table.)
1140
	 *
1141
	 * @param string $group_by [GROUP BY ...[HAVING ...]][ORDER BY ...]
1142
	 * @param string|array $columns better provide an array as exploding by comma can lead to error with functions containing one
1143
	 * @param string $table_name table-name
1144
	 * @param string $autoinc_id id-column
1145
	 * @return string
1146
	 */
1147
	public static function fix_group_by_columns($group_by, &$columns, $table_name, $autoinc_id)
1148
	{
1149
		$matches = null;
1150
		if (substr($GLOBALS['egw']->db->Type, 0, 5) == 'mysql' || !preg_match('/(GROUP BY .*)(HAVING.*|ORDER BY.*)?$/iU', $group_by, $matches))
1151
		{
1152
			return $group_by;	// nothing to do
1153
		}
1154
		$changes = 0;
1155
		$group_by_cols = preg_split('/, */', trim(substr($matches[1], 9)));
1156
1157
		if (!is_array($columns))
1158
		{
1159
			$columns = preg_split('/, */', $columns);
1160
1161
			// fix columns containing commas as part of function calls
1162
			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...
1163
			{
1164
				$col =& $columns[$n];
1165
				while (substr_count($col, '(') > substr_count($col, ')') && ++$n < count($columns))
1166
				{
1167
					$col .= ','.$columns[$n];
1168
					unset($columns[$n]);
1169
				}
1170
			}
1171
			unset($col);
1172
		}
1173
		foreach($columns as $n => $col)
1174
		{
1175
			if ($col == '*')
1176
			{
1177
				// MySQL does NOT allow to GROUP BY table.*
1178
				$col = $columns[$n] = $table_name.'.'.($GLOBALS['egw']->db->Type == 'mysql' ? $autoinc_id : '*');
1179
				++$changes;
1180
			}
1181
			// only check columns and non-aggregate functions
1182
			if (strpos($col, '(') === false || !preg_match('/(COUNT|MIN|MAX|AVG|SUM|BIT_[A-Z]+|STD[A-Z_]*|VAR[A-Z_]*|ARRAY_AGG)\(/i', $col))
1183
			{
1184 View Code Duplication
				if (($pos = stripos($col, 'DISTINCT ')) !== false)
1185
				{
1186
					$col = substr($col, $pos+9);
1187
				}
1188
				$alias = $col;
1189
				if (stripos($col, ' AS ')) list($col, $alias) = preg_split('/ +AS +/i', $col);
1190
				// do NOT group by constant expressions
1191
				if (preg_match('/^ *(-?[0-9]+|".*"|\'.*\'|NULL) *$/i', $col)) continue;
1192
				if (!in_array($col, $group_by_cols) && !in_array($alias, $group_by_cols))
1193
				{
1194
					// instead of aliased primary key, we have to use original column incl. table-name as alias is ambigues
1195
					$group_by_cols[] = $col == $table_name.'.'.$autoinc_id ? $col : $alias;
1196
					//error_log(__METHOD__."() col=$col, alias=$alias --> group_by_cols=".array2string($group_by_cols));
1197
					++$changes;
1198
				}
1199
			}
1200
		}
1201
		$ret = $group_by;
1202
		if ($changes)
1203
		{
1204
			$ret = str_replace($matches[1], 'GROUP BY '.implode(',', $group_by_cols).' ',  $group_by);
1205
			//error_log(__METHOD__."('$group_by', ".array2string($columns).") group_by_cols=".array2string($group_by_cols)." changed to $ret");
1206
		}
1207
		return $ret;
1208
	}
1209
1210
	/**
1211
	 * Return criteria array for a given search pattern
1212
	 *
1213
	 * We handle quoted text, wildcards and boolean operators (+/-, AND/OR).  If
1214
	 * the pattern is '#' followed by an integer, the search is limited to just
1215
	 * the primary key.
1216
	 *
1217
	 * @param string $_pattern search pattern incl. * or ? as wildcard, if no wildcards used we append and prepend one!
1218
	 * @param string &$wildcard ='' on return wildcard char to use, if pattern does not already contain wildcards!
1219
	 * @param string &$op ='AND' on return boolean operation to use, if pattern does not start with ! we use OR else AND
1220
	 * @param string $extra_col =null extra column to search
1221
	 * @param array $search_cols =array() List of columns to search.  If not provided, all columns in $this->db_cols will be considered
1222
	 * @return array or column => value pairs
1223
	 */
1224
	public function search2criteria($_pattern,&$wildcard='',&$op='AND',$extra_col=null, $search_cols = array())
1225
	{
1226
		$pattern = trim($_pattern);
1227
		// This function can get called multiple times.  Make sure it doesn't re-process.
1228
		if (empty($pattern) || is_array($pattern)) return $pattern;
1229
		if(strpos($pattern, 'CAST(COALESCE(') !== false)
1230
		{
1231
			return $pattern;
1232
		}
1233
1234
		$criteria = array();
1235
		$filter = array();
1236
		$columns = array();
1237
1238
		/*
1239
		* Special handling for numeric columns.  They are only considered if the pattern is numeric.
1240
		* If the pattern is numeric, an equality search is used instead.
1241
		*/
1242
		$numeric_types = array('auto', 'int', 'float', 'double', 'decimal');
1243
		$numeric_columns = array();
1244
1245
		// Special handling for an ID search, #<int>
1246
		if(strpos($_pattern, '#') === 0 && is_numeric(substr($_pattern, 1)))
1247
		{
1248
			return array('(' . $this->table_name.'.'. $this->autoinc_id . '=' . (int)substr($_pattern,1) . ')');
1249
		}
1250
		if(!$search_cols)
1251
		{
1252
			$search_cols = $this->get_default_search_columns();
1253
		}
1254
		// Concat all fields to be searched together, so the conditions operate across the whole record
1255
		foreach($search_cols as $col)
1256
		{
1257
			$col_name = $col;
1258
			$table = $this->table_name;
1259
			if (strpos($col,'.') !== false)
1260
			{
1261
				list($table,$col_name) = explode('.',$col);
1262
			}
1263
			$table_def = $table == $this->table_name ? $this->table_def : $this->db->get_table_definitions(true,$table);
1264
			if ($table_def['fd'][$col_name] && in_array($table_def['fd'][$col_name]['type'], $numeric_types))
1265
			{
1266
				$numeric_columns[] = $col;
1267
				continue;
1268
			}
1269
			if ($this->db->Type == 'mysql' && $table_def['fd'][$col_name]['type'] === 'ascii' && preg_match('/[\x80-\xFF]/', $_pattern))
1270
			{
1271
				continue;	// will only give sql error
1272
			}
1273
			$columns[] = sprintf($this->db->capabilities[Api\Db::CAPABILITY_CAST_AS_VARCHAR],"COALESCE($col,'')");
1274
		}
1275
		if(!$columns)
1276
		{
1277
			return array();
1278
		}
1279
1280
		// Break the search string into tokens
1281
		$break = ' ';
1282
		$token = strtok($pattern, $break);
1283
1284
		while($token)
1285
		{
1286
			if($token == strtoupper(lang('AND')) || $token == 'AND')
1287
			{
1288
				$token = '+'.strtok($break);
1289
			}
1290 View Code Duplication
			elseif ($token == strtoupper(lang('OR')) || $token == 'OR')
1291
			{
1292
				$token = strtok($break);
1293
				continue;
1294
			}
1295 View Code Duplication
			elseif ($token == strtoupper(lang('NOT')) || $token == 'NOT')
1296
			{
1297
				$token = '-'.strtok($break);
1298
			}
1299
			if ($token[0]=='"')
1300
			{
1301
				$token = substr($token, 1,strlen($token));
1302
 				if(substr($token, -1) != '"')
1303
				{
1304
					$token .= ' '.strtok('"');
1305
				}
1306
				else
1307
				{
1308
					$token = substr($token, 0, -1);
1309
				}
1310
			}
1311
1312
			// prepend and append extra wildcard %, if pattern does NOT already contain wildcards
1313
			if (strpos($token,'*') === false && strpos($token,'?') === false)
1314
			{
1315
				$wildcard = '%';	// if pattern contains no wildcards, add them before AND after the pattern
1316
			}
1317
			else
1318
			{
1319
				$wildcard = '';		// no extra wildcard, if pattern already contains some
1320
			}
1321
1322
			switch($token[0])
1323
			{
1324
				case '+':
1325
					$op = 'AND';
1326
					$token = substr($token, 1, strlen($token));
1327
					break;
1328
				case '-':
1329
				case '!':
1330
					$op = 'NOT';
1331
					$token = substr($token, 1, strlen($token));
1332
					break;
1333
				default:
1334
					$op = 'OR';
1335
					break;
1336
			}
1337
			$token_filter = ' '.call_user_func_array(array($GLOBALS['egw']->db,'concat'),$columns).' '.
1338
				$this->db->capabilities['case_insensitive_like'] . ' ' .
1339
				$GLOBALS['egw']->db->quote($wildcard.str_replace(array('%','_','*','?'),array('\\%','\\_','%','_'),$token).$wildcard);
1340
1341
			// Compare numeric token as equality for numeric columns
1342
			// skip user-wildcards (*,?) in is_numeric test, but not SQL wildcards, which get escaped and give sql-error
1343
			if (is_numeric(str_replace(array('*','?'), '', $token)))
1344
			{
1345
				$numeric_filter = array();
1346
				foreach($numeric_columns as $col)
1347
				{
1348
					if($wildcard == '')
1349
					{
1350
						// Token has a wildcard from user, use LIKE
1351
						$numeric_filter[] = "($col IS NOT NULL AND CAST($col AS CHAR) " .
1352
							$this->db->capabilities['case_insensitive_like'] . ' ' .
1353
							$GLOBALS['egw']->db->quote(str_replace(array('*','?'), array('%','_'), $token)) . ')';
1354
					}
1355
					else
1356
					{
1357
						$numeric_filter[] = "($col IS NOT NULL AND $col = $token)";
1358
					}
1359
				}
1360
				if(count($numeric_filter) > 0)
1361
				{
1362
					$token_filter = '(' . $token_filter . ' OR ' . implode(' OR ', $numeric_filter) . ')';
1363
				}
1364
			}
1365
			$criteria[$op][] = $token_filter;
1366
1367
			$token = strtok($break);
1368
		}
1369
1370 View Code Duplication
		if($criteria['NOT'])
1371
		{
1372
			$filter[] = 'NOT (' . implode(' OR ', $criteria['NOT']) . ') ';
1373
		}
1374
		if($criteria['AND'])
1375
		{
1376
			$filter[] = implode(' AND ', $criteria['AND']) . ' ';
1377
		}
1378 View Code Duplication
		if($criteria['OR'])
1379
		{
1380
			$filter[] = '(' . implode(' OR ', $criteria['OR']) . ') ';
1381
		}
1382
1383
		if(count($filter))
1384
		{
1385
			$result = '(' . implode(' AND ', $filter) . ')';
1386
		}
1387
1388
		// OR extra column on the end so a null or blank won't block a hit in the main columns
1389
		if ($extra_col)
1390
		{
1391
			$result .= (strlen($result) ? ' OR ' : ' ') . "$extra_col = " . $GLOBALS['egw']->db->quote($pattern);
1392
		}
1393
1394
		$op = 'OR';
1395
		return array('(' . $result . ')');
1396
	}
1397
1398
	/**
1399
	* Get a default list of columns to search
1400
	* This is to be used as a fallback, for when the extending class does not define
1401
	* $this->columns_to_search.  All the columns are considered, and any with $skip_columns_with in
1402
	* their name are discarded because these columns are expected to be foreign keys or other numeric
1403
	* values with no meaning to the user.
1404
	*
1405
	* @return array of column names
1406
	*/
1407
	protected function get_default_search_columns()
1408
	{
1409
		$skip_columns_with = array('_id', 'modified', 'modifier', 'status', 'cat_id', 'owner');
1410
		$search_cols = is_null($this->columns_to_search) ? $this->db_cols : $this->columns_to_search;
1411
		$numeric_types = array('auto', 'int', 'float', 'double');
1412
1413
		// Skip some numeric columns that don't make sense to search if we have to default to all columns
1414
		if(is_null($this->columns_to_search))
1415
		{
1416
			foreach($search_cols as $key => &$col)
1417
			{
1418
				// If the name as given isn't a real column name, and adding the prefix doesn't help, skip it
1419
				if(!$this->table_def['fd'][$col] && !($col = $this->prefix.array_search($col, $search_cols))) {
1420
					// Can't search this column
1421
					unset($search_cols[$key]);
1422
					continue;
1423
				}
1424
				if(in_array($this->table_def['fd'][$col]['type'], $numeric_types))
1425
				{
1426
					foreach($skip_columns_with as $bad)
1427
					{
1428
						if(strpos($col, $bad) !== false)
1429
						{
1430
							unset($search_cols[$key]);
1431
							continue 2;
1432
						}
1433
					}
1434
				}
1435
				// Prefix with table name to avoid ambiguity
1436
				$col = $this->table_name.'.'.$col;
1437
			}
1438
		}
1439
		return $search_cols;
1440
	}
1441
1442
	/**
1443
	 * extract the requested columns from $only_keys and $extra_cols param of a search
1444
	 *
1445
	 * @internal
1446
	 * @param boolean|string $only_keys =true True returns only keys, False returns all cols. comma seperated list of keys to return
1447
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
1448
	 * @return array with columns as db-name => internal-name pairs
1449
	 */
1450
	function _get_columns($only_keys,$extra_cols)
1451
	{
1452
		//echo "_get_columns() only_keys="; _debug_array($only_keys); echo "extra_cols="; _debug_array($extra_cols);
1453
		if ($only_keys === true)	// only primary key
1454
		{
1455
			$cols = $this->db_key_cols;
1456
		}
1457
		else
1458
		{
1459
			$cols = array();
1460
			$distinct_checked = false;
1461
			foreach(is_array($only_keys) ? $only_keys : explode(',', $only_keys) as $col)
1462
			{
1463 View Code Duplication
				if (!$distinct_checked)
1464
				{
1465
					if (stripos($col, 'DISTINCT ') === 0) $col = substr($col, 9);
1466
					$distinct_checked = true;
1467
				}
1468
				if (!$col || $col == '*' || $col == $this->table_name.'.*')	// all columns
1469
				{
1470
					$cols = array_merge($cols,$this->db_cols);
1471
				}
1472
				else	// only the specified columns
1473
				{
1474
					if (stripos($col,'as'))	// if there's already an explicit naming of the column, just use it
1475
					{
1476
						$col = preg_replace('/^.*as +([a-z0-9_]+) *$/i','\\1',$col);
1477
						$cols[$col] = $col;
1478
						continue;
1479
					}
1480 View Code Duplication
					if (($db_col = array_search($col,$this->db_cols)) !== false)
1481
					{
1482
						$cols[$db_col] = $col;
1483
					}
1484
					else
1485
					{
1486
						$cols[$col] = isset($this->db_cols[$col]) ? $this->db_cols[$col] : $col;
1487
					}
1488
				}
1489
			}
1490
		}
1491
		if ($extra_cols)	// extra columns to report
1492
		{
1493
			foreach(is_array($extra_cols) ? $extra_cols : explode(',',$extra_cols) as $col)
1494
			{
1495
				if (stripos($col,'as ')!==false) $col = preg_replace('/^.*as +([a-z0-9_]+) *$/i','\\1',$col);
1496 View Code Duplication
				if (($db_col = array_search($col,$this->db_cols)) !== false)
1497
				{
1498
					$cols[$db_col] = $col;
1499
				}
1500
				else
1501
				{
1502
					$cols[$col] = isset($this->db_cols[$col]) ? $this->db_cols[$col] : $col;
1503
				}
1504
			}
1505
		}
1506
		return $cols;
1507
	}
1508
1509
	/**
1510
	 * query rows for the nextmatch widget
1511
	 *
1512
	 * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter'
1513
	 *	For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class.
1514
	 * @param array &$rows returned rows/competitions
1515
	 * @param array &$readonlys eg. to disable buttons based on acl, not use here, maybe in a derived class
1516
	 * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
1517
	 *	"LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
1518
	 * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false
1519
	 * @param mixed $only_keys =false, see search
1520
	 * @param string|array $extra_cols =array()
1521
	 * @return int total number of rows
1522
	 */
1523
	function get_rows($query,&$rows,&$readonlys,$join='',$need_full_no_count=false,$only_keys=false,$extra_cols=array())
1524
	{
1525
		unset($readonlys);	// required by function signature, but not used in this default implementation
1526 View Code Duplication
		if ((int) $this->debug >= 4)
1527
		{
1528
			echo "<p>so_sql::get_rows(".print_r($query,true).",,)</p>\n";
1529
		}
1530
		$criteria = array();
1531
		$op = 'AND';
1532
		if ($query['search'])
1533
		{
1534
			$criteria = $query['search'];
1535
		}
1536
		$rows = $this->search($criteria,$only_keys,$query['order']?$query['order'].' '.$query['sort']:'',$extra_cols,
1537
			'',false,$op,$query['num_rows']?array((int)$query['start'],$query['num_rows']):(int)$query['start'],
1538
			$query['col_filter'],$join,$need_full_no_count);
1539
1540
		if (!$rows) $rows = array();	// otherwise false returned from search would be returned as array(false)
1541
1542
		return $this->total;
1543
	}
1544
1545
	/**
1546
	 * Check if values for unique keys and the primary keys are unique are unique
1547
	 *
1548
	 * @param array $data =null data-set to check, defaults to $this->data
1549
	 * @return int 0: all keys are unique, 1: first key not unique, 2: ...
1550
	 */
1551
	function not_unique($data=null)
1552
	{
1553
		if (!is_array($data))
1554
		{
1555
			$data = $this->data;
1556
		}
1557
		$n = 1;
1558
		$uni_keys = $this->db_uni_cols;
1559
		// add the primary key, only if it's NOT an auto id
1560
		if (!$this->autoinc_id)
1561
		{
1562
			$uni_keys[] = $this->db_key_cols;
1563
		}
1564
		foreach($uni_keys as $db_col => $col)
1565
		{
1566
			if (is_array($col))
1567
			{
1568
				$query = array();
1569
				foreach($col as $db_c => $c)
1570
				{
1571
					$query[$db_c] = $data[$c];
1572
				}
1573
			}
1574
			else
1575
			{
1576
				$query = array($db_col => $data[$col]);
1577
			}
1578
			foreach($this->db->select($this->table_name,$this->db_key_cols,$query,__LINE__,__FILE__,false,'',$this->app) as $other)
1579
			{
1580
				foreach($this->db_key_cols as $key_col)
1581
				{
1582
					if ($data[$key_col] != $other[$key_col])
1583
					{
1584
						if ((int) $this->debug >= 4)
1585
						{
1586
							echo "<p>not_unique in ".array2string($col)." as for '$key_col': '${data[$key_col]}' != '${other[$key_col]}'</p>\n";
1587
						}
1588
						return $n;	// different entry => $n not unique
1589
					}
1590
				}
1591
			}
1592
			++$n;
1593
		}
1594
		return 0;
1595
	}
1596
1597
	/**
1598
	 * Query DB for a list / array with one colum as key and an other one(s) as value, eg. id => title pairs
1599
	 *
1600
	 * We do some caching as these kind of function is usualy called multiple times, eg. for option-lists.
1601
	 *
1602
	 * @param string $value_col array of column-names for the values of the array, can also be an expression aliased with AS,
1603
	 *	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
1604
	 * @param string $key_col ='' column-name for the keys, default '' = same as (first) $value_col: returns a distinct list
1605
	 * @param array $filter =array() to filter the entries
1606
	 * @param string $order ='' order, default '' = same as (first) $value_col
1607
	 * @return array with key_col => value_col pairs or array if more then one value_col given (keys as in value_col)
1608
	 */
1609
	function query_list($value_col,$key_col='',$filter=array(),$order='')
1610
	{
1611
		static $cache = array();
1612
1613
		$cache_key = serialize($value_col).'-'.$key_col.'-'.serialize($filter).'-'.$order;
1614
1615
		if (isset($cache[$cache_key]))
1616
		{
1617
			return $cache[$cache_key];
1618
		}
1619
		if (!is_array($value_col)) $value_col = array($value_col);
1620
1621
		$cols = $ret = array();
1622
		foreach($value_col as $key => $col)
1623
		{
1624
			$matches = null;
1625
			$cols[$key] = preg_match('/AS ([a-z_0-9]+)$/i',$col,$matches) ? $matches[1] : $col;
1626
		}
1627
		if (!$order) $order = current($cols);
1628
1629
		if (($search =& $this->search(array(),($key_col ? $key_col.',' : 'DISTINCT ').implode(',',$value_col),$order,'','',false,'AND',false,$filter)))
1630
		{
1631
			if (preg_match('/AS ([a-z_0-9]+)$/i',$key_col,$matches))
1632
			{
1633
				$key_col = $matches[1];
1634
			}
1635
			elseif (!$key_col)
1636
			{
1637
				$key_col = current($cols);
1638
			}
1639
			foreach($search as $row)
0 ignored issues
show
Bug introduced by
The expression $search of type boolean|object<EGroupwar...ataIterator>|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1640
			{
1641
				if (count($cols) > 1)
1642
				{
1643
					$data = array();
1644
					foreach($cols as $key => $col)
1645
					{
1646
						$data[$key] = $row[$col];
1647
					}
1648
				}
1649
				else
1650
				{
1651
					$data = $row[current($cols)];
1652
				}
1653
				if ($data) $ret[$row[$key_col]] = $data;
1654
			}
1655
		}
1656
		return $cache[$cache_key] =& $ret;
1657
	}
1658
1659
	/**
1660
	 * Get comments for all columns or a specific one
1661
	 *
1662
	 * @param string $column =null name of column or null for all (default)
1663
	 * @return array|string array with internal-name => comment pairs, or string with comment, if $column given
1664
	 */
1665
	public function get_comments($column=null)
1666
	{
1667
		static $comments=null;
1668
1669
		if (is_null($comments))
1670
		{
1671
			foreach($this->db_cols as $db_col => $col)
1672
			{
1673
				$comments[$col] = $this->table_def['fd'][$db_col]['comment'];
1674
			}
1675
		}
1676
		return is_null($column) ? $comments : $comments[$column];
1677
	}
1678
}
1679