Db::update()   F
last analyzed

Complexity

Conditions 26
Paths 1120

Size

Total Lines 73
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 42
nc 1120
nop 8
dl 0
loc 73
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * EGroupware API: Database abstraction library
4
 *
5
 * @link http://www.egroupware.org
6
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
7
 * @package api
8
 * @subpackage db
9
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
10
 * @copyright (c) 2003-19 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
11
 */
12
13
namespace EGroupware\Api;
14
15
if(empty($GLOBALS['egw_info']['server']['db_type']))
16
{
17
	$GLOBALS['egw_info']['server']['db_type'] = 'mysql';
18
}
19
20
/**
21
 * Database abstraction library
22
 *
23
 * This allows eGroupWare to use multiple database backends via ADOdb or in future with PDO
24
 *
25
 * You only need to clone the global database object $GLOBALS['egw']->db if:
26
 * - you access an application table (non phpgwapi) and you want to call set_app()
27
 *
28
 * Otherwise you can simply use $GLOBALS['egw']->db or a reference to it.
29
 *
30
 * a) foreach($db->query("SELECT * FROM $table",__LINE__,__FILE__) as $row)
31
 *
32
 * b) foreach($db->select($api_table,'*',$where,__LINE__,__FILE__) as $row)
33
 *
34
 * c) foreach($db->select($table,'*',$where,__LINE__,__FILE__,false,'',$app) as $row)
35
 *
36
 * To fetch only a single column (of the next row):
37
 *		$cnt = $db->query("SELECT COUNT(*) FROM ...")->fetchColumn($column_num=0);
38
 *
39
 * To fetch a next (single) row, you can use:
40
 *		$row = $db->query("SELECT COUNT(*) FROM ...")->fetch($fetchmod=null);
41
 *
42
 * Api\Db allows to use exceptions to catch sql-erros, not existing tables or failure to connect to the database, eg.:
43
 *		try {
44
 *			$this->db->connect();
45
 *			$num_config = $this->db->select(config::TABLE,'COUNT(config_name)',false,__LINE__,__FILE__)->fetchColumn();
46
 *		}
47
 *		catch(Exception $e) {
48
 *			echo "Connection to DB failed (".$e->getMessage().")!\n";
49
 *		}
50
 */
51
class Db
52
{
53
	/**
54
	 * Fetchmode to fetch only as associative array with $colname => $value pairs
55
	 *
56
	 * Use the FETCH_* constants to be compatible, if we replace ADOdb ...
57
	 */
58
	const FETCH_ASSOC = ADODB_FETCH_ASSOC;
59
	/**
60
	 * Fetchmode to fetch only as (numeric indexed) array: array($val1,$val2,...)
61
	 */
62
	const FETCH_NUM = ADODB_FETCH_NUM;
63
	/**
64
	 * Fetchmode to have both numeric and column-name indexes
65
	 */
66
	const FETCH_BOTH = ADODB_FETCH_BOTH;
67
	/**
68
	* @var string $type translated database type: mysqlt+mysqli ==> mysql, same for odbc-types
69
	*/
70
	var $Type     = '';
71
72
	/**
73
	* @var string $type database type as defined in the header.inc.php, eg. mysqlt
74
	*/
75
	var $setupType     = '';
76
77
	/**
78
	* @var string $Host database host to connect to
79
	*/
80
	var $Host     = '';
81
82
	/**
83
	* @var string $Port port number of database to connect to
84
	*/
85
	var $Port     = '';
86
87
	/**
88
	* @var string $Database name of database to use
89
	*/
90
	var $Database = '';
91
92
	/**
93
	* @var string $User name of database user
94
	*/
95
	var $User     = '';
96
97
	/**
98
	* @var string $Password password for database user
99
	*/
100
	var $Password = '';
101
102
	/**
103
	 * @var boolean $readonly only allow readonly access to database
104
	 */
105
	var $readonly = false;
106
107
	/**
108
	* @var int $Debug enable debuging - 0 no, 1 yes
109
	*/
110
	var $Debug         = 0;
111
112
	/**
113
	 * Log update querys to error_log
114
	 *
115
	 * @var boolean
116
	 */
117
	var $log_updates = false;
118
119
	/**
120
	* @var array $Record current record
121
	*/
122
	var $Record   = array();
123
124
	/**
125
	* @var int row number for current record
126
	*/
127
	var $Row;
128
129
	/**
130
	* @var int $Errno internal rdms error number for last error
131
	*/
132
	var $Errno    = 0;
133
134
	/**
135
	* @var string descriptive text from last error
136
	*/
137
	var $Error    = '';
138
139
	/**
140
	 * eGW's own query log, independent of the db-type, eg. /tmp/query.log
141
	 *
142
	 * @var string
143
	 */
144
	var $query_log;
145
146
	/**
147
	 * ADOdb connection
148
	 *
149
	 * @var ADOConnection
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\ADOConnection was not found. Did you mean ADOConnection? If so, make sure to prefix the type with \.
Loading history...
150
	 */
151
	var $Link_ID = 0;
152
	/**
153
	 * ADOdb connection
154
	 *
155
	 * @var ADOConnection
156
	 */
157
	var $privat_Link_ID = False;	// do we use a privat Link_ID or a reference to the global ADOdb object
158
159
	/**
160
	 * Can be used to transparently convert tablenames, eg. 'mytable' => 'otherdb.othertable'
161
	 *
162
	 * Can be set eg. at the *end* of header.inc.php.
163
	 * Only works with new Api\Db methods (select, insert, update, delete) not query!
164
	 *
165
	 * @var array
166
	 */
167
	static $tablealiases = array();
168
169
	/**
170
	 * Callback to check if selected node is healty / should be used
171
	 *
172
	 * @var callback throwing Db\Exception\Connection, if connected node should NOT be used
173
	 */
174
	static $health_check;
175
176
	/**
177
	 * db allows sub-queries, true for everything but mysql < 4.1
178
	 *
179
	 * use like: if ($db->capabilities[self::CAPABILITY_SUB_QUERIES]) ...
180
	 */
181
	const CAPABILITY_SUB_QUERIES = 'sub_queries';
182
	/**
183
	 * db allows union queries, true for everything but mysql < 4.0
184
	 */
185
	const CAPABILITY_UNION = 'union';
186
	/**
187
	 * db allows an outer join, will be set eg. for postgres
188
	 */
189
	const CAPABILITY_OUTER_JOIN = 'outer_join';
190
	/**
191
	 * db is able to use DISTINCT on text or blob columns
192
	 */
193
	const CAPABILITY_DISTINCT_ON_TEXT =	'distinct_on_text';
194
	/**
195
	 * DB is able to use LIKE on text columns
196
	 */
197
	const CAPABILITY_LIKE_ON_TEXT =	'like_on_text';
198
	/**
199
	 * DB allows ORDER on text columns
200
	 *
201
	 * boolean or string for sprintf for a cast (eg. 'CAST(%s AS varchar)
202
	 */
203
	const CAPABILITY_ORDER_ON_TEXT = 'order_on_text';
204
	/**
205
	 * case of returned column- and table-names: upper, lower(pgSql), preserv(MySQL)
206
	 */
207
	const CAPABILITY_NAME_CASE = 'name_case';
208
	/**
209
	 * does DB supports a changeable client-encoding
210
	 */
211
	const CAPABILITY_CLIENT_ENCODING = 'client_encoding';
212
	/**
213
	 * case insensitiv like statement (in $db->capabilities[self::CAPABILITY_CASE_INSENSITIV_LIKE]), default LIKE, ILIKE for postgres
214
	 */
215
	const CAPABILITY_CASE_INSENSITIV_LIKE = 'case_insensitive_like';
216
	/**
217
	 * DB requires varchar columns to be truncated to the max. size (eg. Postgres)
218
	 */
219
	const CAPABILITY_REQUIRE_TRUNCATE_VARCHAR = 'require_truncate_varchar';
220
	/**
221
	 * How to cast a column to varchar: CAST(%s AS varchar)
222
	 *
223
	 * MySQL requires to use CAST(%s AS char)!
224
	 *
225
	 * Use as: $sql = sprintf($GLOBALS['egw']->db->capabilities[self::CAPABILITY_CAST_AS_VARCHAR],$expression);
226
	 */
227
	const CAPABILITY_CAST_AS_VARCHAR = 'cast_as_varchar';
228
	/**
229
	 * default capabilities will be changed by method set_capabilities($ado_driver,$db_version)
230
	 *
231
	 * should be used with the CAPABILITY_* constants as key
232
	 *
233
	 * @var array
234
	 */
235
	var $capabilities = array(
236
		self::CAPABILITY_SUB_QUERIES      => true,
237
		self::CAPABILITY_UNION            => true,
238
		self::CAPABILITY_OUTER_JOIN       => false,
239
		self::CAPABILITY_DISTINCT_ON_TEXT => true,
240
		self::CAPABILITY_LIKE_ON_TEXT     => true,
241
		self::CAPABILITY_ORDER_ON_TEXT    => true,
242
		self::CAPABILITY_NAME_CASE        => 'upper',
243
		self::CAPABILITY_CLIENT_ENCODING  => false,
244
		self::CAPABILITY_CASE_INSENSITIV_LIKE => 'LIKE',
245
		self::CAPABILITY_REQUIRE_TRUNCATE_VARCHAR => true,
246
		self::CAPABILITY_CAST_AS_VARCHAR   => 'CAST(%s AS varchar)',
247
	);
248
249
	var $prepared_sql = array();	// sql is the index
250
251
	/**
252
	 * Constructor
253
	 *
254
	 * @param array $db_data =null values for keys 'db_name', 'db_host', 'db_port', 'db_user', 'db_pass', 'db_type', 'db_readonly'
255
	 */
256
	function __construct(array $db_data=null)
257
	{
258
		if (!is_null($db_data))
259
		{
260
			foreach(array(
261
				'Database' => 'db_name',
262
				'Host'     => 'db_host',
263
				'Port'     => 'db_port',
264
				'User'     => 'db_user',
265
				'Password' => 'db_pass',
266
				'Type'     => 'db_type',
267
				'readonly' => 'db_readonly',
268
			) as $var => $key)
269
			{
270
				$this->$var = $db_data[$key];
271
			}
272
		}
273
//if ($GLOBALS['egw_info']['server']['default_domain'] == 'ralfsmacbook.local') $this->query_log = '/tmp/query.log';
274
	}
275
276
	/**
277
	* @param string $query query to be executed (optional)
278
	*/
279
280
	function db($query = '')
281
	{
282
		$this->query($query);
283
	}
284
285
	/**
286
	* @return int current connection id
287
	*/
288
	function link_id()
289
	{
290
		return $this->Link_ID;
291
	}
292
293
	/**
294
	 * Open a connection to a database
295
	 *
296
	 * @param string $Database name of database to use (optional)
297
	 * @param string $Host database host to connect to (optional)
298
	 * @param string $Port database port to connect to (optional)
299
	 * @param string $User name of database user (optional)
300
	 * @param string $Password password for database user (optional)
301
	 * @param string $Type type of database (optional)
302
	 * @throws Db\Exception\Connection
303
	 * @return ADOConnection
304
	 */
305
	function connect($Database = NULL, $Host = NULL, $Port = NULL, $User = NULL, $Password = NULL, $Type = NULL)
306
	{
307
		/* Handle defaults */
308
		if (!is_null($Database) && $Database)
309
		{
310
			$this->Database = $Database;
311
		}
312
		if (!is_null($Host) && $Host)
313
		{
314
			$this->Host     = $Host;
315
		}
316
		if (!is_null($Port) && $Port)
317
		{
318
			$this->Port     = $Port;
319
		}
320
		if (!is_null($User) && $User)
321
		{
322
			$this->User     = $User;
323
		}
324
		if (!is_null($Password) && $Password)
325
		{
326
			$this->Password = $Password;
327
		}
328
		if (!is_null($Type) && $Type)
329
		{
330
			$this->Type = $Type;
331
		}
332
		elseif (!$this->Type)
333
		{
334
			$this->Type = $GLOBALS['egw_info']['server']['db_type'];
335
		}
336
		// on connection failure re-try with an other host
337
		// remembering in session which host we used last time
338
		$use_host_from_session = true;
339
		while(($host = $this->get_host(!$use_host_from_session)))
340
		{
341
			try {
342
				//error_log(__METHOD__."() this->Host(s)=$this->Host, n=$n --> host=$host");
343
				$new_connection = !$this->Link_ID || !$this->Link_ID->IsConnected();
344
				$this->_connect($host);
345
				// check if connected node is healty
346
				if ($new_connection && self::$health_check)
347
				{
348
					call_user_func(self::$health_check, $this);
349
				}
350
				//error_log(__METHOD__."() host=$host, new_connection=$new_connection, this->Type=$this->Type, this->Host=$this->Host, wsrep_local_state=".array2string($state));
351
				return $this->Link_ID;
352
			}
353
			catch(Db\Exception\Connection $e) {
354
				//_egw_log_exception($e);
355
				$this->disconnect();	// force a new connect
356
				$this->Type = $this->setupType;	// get set to "mysql" for "mysqli"
357
				$use_host_from_session = false;	// re-try with next host from list
358
			}
359
		}
360
		if (!isset($e))
361
		{
362
			$e = new Db\Exception\Connection('No DB host set!');
363
		}
364
		throw $e;
365
	}
366
367
	/**
368
	 * Check if just connected Galera cluster node is healthy / fully operational
369
	 *
370
	 * A node in state "Donor/Desynced" will block updates at the end of a SST.
371
	 * Therefore we try to avoid that node, if we have an alternative.
372
	 *
373
	 * To enable this check add the following to your header.inc.php:
374
	 *
375
	 * require_once(EGW_INCLUDE_ROOT.'/api/src/Db.php');
376
	 * EGroupware\Api\Db::$health_check = array('EGroupware\Api\Db', 'galera_cluster_health');
377
	 *
378
	 * @param Api\Db $db already connected Api\Db instance to check
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Api\Db was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
379
	 * @throws Db\Exception\Connection if node should NOT be used
380
	 */
381
	static function galera_cluster_health(Db $db)
382
	{
383
		if (($state = $db->query("SHOW STATUS WHERE Variable_name in ('wsrep_cluster_size','wsrep_local_state','wsrep_local_state_comment')",
384
			// GetAssoc in ADOdb 5.20 does not work with our default self::FETCH_BOTH
385
			__LINE__, __FILE__, 0, -1, false, self::FETCH_ASSOC)->GetAssoc()))
386
		{
387
			if ($state['wsrep_local_state_comment'] == 'Synced' ||
388
				// if we have only 2 nodes (2. one starting), we can only use the donor
389
				$state['wsrep_local_state_comment'] == 'Donor/Desynced' &&
390
					$state['wsrep_cluster_size'] == 2) return;
391
392
			throw new Db\Exception\Connection('Node is NOT Synced! '.array2string($state));
393
		}
394
	}
395
396
	/**
397
	 * Get one of multiple (semicolon-separated) DB-hosts to use
398
	 *
399
	 * Which host to use is cached in session, default is first one.
400
	 *
401
	 * @param boolean $next =false	true: move to next host
402
	 * @return boolean|string hostname or false, if already number-of-hosts plus 2 times called with $next == true
403
	 */
404
	public function get_host($next = false)
405
	{
406
		$hosts = explode(';', $this->Host[0] == '@' ? getenv(substr($this->Host, 1)) : $this->Host);
407
		$num_hosts = count($hosts);
408
		$n =& Cache::getSession(__CLASS__, $this->Host);
409
		if (!isset($n)) $n = 0;
410
411
		if ($next && ++$n >= $num_hosts+2)
412
		{
413
			$n = 0;	// start search again with default on next request
414
			$ret = false;
415
		}
416
		else
417
		{
418
			$ret = $hosts[$n % $num_hosts];
419
		}
420
		//error_log(__METHOD__."(next=".array2string($next).") n=$n returning ".array2string($ret));
421
		return $ret;
422
	}
423
424
	/**
425
	 * Connect to given host
426
	 *
427
	 * @param string $Host host to connect to
428
	 * @return ADOConnection
429
	 * @throws Db\Exception\Connection
430
	 */
431
	protected function _connect($Host)
432
	{
433
		if (!$this->Link_ID || $Host != $this->Link_ID->host)
434
		{
435
			$Database = $User = $Password = $Port = $Type = '';
436
			foreach(array('Database','User','Password','Port','Type') as $name)
437
			{
438
				$$name = $this->$name;
439
				if (${$name}[0] == '@' && $name != 'Password') $$name = getenv(substr($$name, 1));
440
			}
441
			$this->setupType = $php_extension = $Type;
442
443
			switch($Type)	// convert to ADO db-type-names
444
			{
445
				case 'pgsql':
446
					$Type = 'postgres'; // name in ADOdb
447
					// create our own pgsql connection-string, to allow unix domain soccets if !$Host
448
					$Host = "dbname=$Database".($Host ? " host=$Host".($Port ? " port=$Port" : '') : '').
449
						" user=$User".($Password ? " password='".addslashes($Password)."'" : '');
450
					$User = $Password = $Database = '';	// to indicate $Host is a connection-string
451
					break;
452
453
				case 'odbc_mssql':
454
					$php_extension = 'odbc';
455
					$Type = 'mssql';
456
					// fall through
457
				case 'mssql':
458
					if ($Port) $Host .= ','.$Port;
459
					break;
460
461
				case 'odbc_oracle':
462
					$php_extension = 'odbc';
463
					$Type = 'oracle';
464
					break;
465
				case 'oracle':
466
					$php_extension = $Type = 'oci8';
467
					break;
468
469
				case 'sapdb':
470
					$Type = 'maxdb';
471
					// fall through
472
				case 'maxdb':
473
					$Type ='sapdb';	// name in ADOdb
474
					$php_extension = 'odbc';
475
					break;
476
477
				case 'mysqlt':
478
				case 'mysql':
479
					// if mysqli is available silently switch to it, mysql extension is deprecated and no longer available in php7+
480
					if (check_load_extension('mysqli'))
481
					{
482
						$php_extension = $Type = 'mysqli';
483
					}
484
					else
485
					{
486
						$php_extension = 'mysql';	// you can use $this->setupType to determine if it's mysqlt or mysql
487
					}
488
					// fall through
489
				case 'mysqli':
490
					$this->Type = 'mysql';		// need to be "mysql", so apps can check just for "mysql"!
491
					// fall through
492
				default:
493
					if ($Port) $Host .= ':'.$Port;
494
					break;
495
			}
496
			if (!isset($GLOBALS['egw']->ADOdb) ||	// we have no connection so far
497
				(is_object($GLOBALS['egw']->db) &&	// we connect to a different db, then the global one
498
					($this->Type != $GLOBALS['egw']->db->Type ||
499
					$this->Database != $GLOBALS['egw']->db->Database ||
500
					$this->User != $GLOBALS['egw']->db->User ||
501
					$this->Host != $GLOBALS['egw']->db->Host ||
502
					$this->Port != $GLOBALS['egw']->db->Port)))
503
			{
504
				if (!check_load_extension($php_extension))
505
				{
506
					throw new Db\Exception\Connection("Necessary php database support for $this->Type (".PHP_SHLIB_PREFIX.$php_extension.'.'.PHP_SHLIB_SUFFIX.") not loaded and can't be loaded, exiting !!!");
507
				}
508
				if (!isset($GLOBALS['egw']->ADOdb))	// use the global object to store the connection
509
				{
510
					$this->Link_ID =& $GLOBALS['egw']->ADOdb;
511
				}
512
				else
513
				{
514
					$this->privat_Link_ID = True;	// remember that we use a privat Link_ID for disconnect
0 ignored issues
show
Documentation Bug introduced by
It seems like True of type true is incompatible with the declared type EGroupware\Api\ADOConnection of property $privat_Link_ID.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
515
				}
516
				$this->Link_ID = ADONewConnection($Type);
517
				if (!$this->Link_ID)
518
				{
519
					throw new Db\Exception\Connection("No ADOdb support for '$Type' ($this->Type) !!!");
520
				}
521
				if ($Type == 'mysqli')
0 ignored issues
show
introduced by
The condition $Type == 'mysqli' is always false.
Loading history...
522
				{
523
					// set a connection timeout of 1 second, to allow quicker failover to other db-nodes (default is 20s)
524
					$this->Link_ID->setConnectionParameter(MYSQLI_OPT_CONNECT_TIMEOUT, 1);
525
				}
526
				$connect = $GLOBALS['egw_info']['server']['db_persistent'] &&
527
					// do NOT attempt persistent connection, if it is switched off in php.ini (it will only cause a warning)
528
					($Type !== 'mysqli' || ini_get('mysqli.allow_persistent')) ?
529
					'PConnect' : 'Connect';
530
531
				if (($Ok = $this->Link_ID->$connect($Host, $User, $Password, $Database)))
532
				{
533
					$this->ServerInfo = $this->Link_ID->ServerInfo();
0 ignored issues
show
Bug Best Practice introduced by
The property ServerInfo does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
534
					$this->set_capabilities($Type,$this->ServerInfo['version']);
535
536
					// switch off MySQL 5.7+ ONLY_FULL_GROUP_BY sql_mode
537
					if (substr($this->Type, 0, 5) == 'mysql' && $this->ServerInfo['version'] >= 5.7 && $this->ServerInfo['version'] < 10.0)
538
					{
539
						$this->query("SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''))", __LINE__, __FILE__);
540
					}
541
				}
542
				if (!$Ok)
543
				{
544
					$Host = preg_replace('/password=[^ ]+/','password=$Password',$Host);	// eg. postgres dsn contains password
545
					throw new Db\Exception\Connection("ADOdb::$connect($Host, $User, \$Password, $Database) failed.");
546
				}
547
				if ($this->Debug)
548
				{
549
					echo function_backtrace();
550
					echo "<p>new ADOdb connection to $Type://$Host/$Database: Link_ID".($this->Link_ID === $GLOBALS['egw']->ADOdb ? '===' : '!==')."\$GLOBALS[egw]->ADOdb</p>";
551
					//echo "<p>".print_r($this->Link_ID->ServerInfo(),true)."</p>\n";
552
					_debug_array($this);
553
					echo "\$GLOBALS[egw]->db="; _debug_array($GLOBALS[egw]->db);
0 ignored issues
show
Bug introduced by
The constant EGroupware\Api\egw was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
554
				}
555
				if ($Type == 'mssql')
0 ignored issues
show
introduced by
The condition $Type == 'mssql' is always false.
Loading history...
556
				{
557
					// this is the format ADOdb expects
558
					$this->Link_ID->Execute('SET DATEFORMAT ymd');
559
					// sets the limit to the maximum
560
					ini_set('mssql.textlimit',2147483647);
561
					ini_set('mssql.sizelimit',2147483647);
562
				}
563
				// set our default charset
564
				$this->Link_ID->SetCharSet($this->Type == 'mysql' ? 'utf8' : 'utf-8');
565
566
				$new_connection = true;
567
			}
568
			else
569
			{
570
				$this->Link_ID =& $GLOBALS['egw']->ADOdb;
571
			}
572
		}
573
		if (!$this->Link_ID->isConnected() && !$this->Link_ID->Connect())
574
		{
575
			$Host = preg_replace('/password=[^ ]+/','password=$Password',$Host);	// eg. postgres dsn contains password
576
			throw new Db\Exception\Connection("ADOdb::$connect($Host, $User, \$Password, $Database) reconnect failed.");
577
		}
578
		// fix due to caching and reusing of connection not correctly set $this->Type == 'mysql'
579
		if ($this->Type == 'mysqli')
580
		{
581
			$this->setupType = $this->Type;
582
			$this->Type = 'mysql';
583
		}
584
		if ($new_connection)
585
		{
586
			foreach(get_included_files() as $file)
587
			{
588
				if (strpos($file,'adodb') !== false && !in_array($file,(array)$_SESSION['egw_required_files']))
589
				{
590
					$_SESSION['egw_required_files'][] = $file;
591
					//error_log(__METHOD__."() egw_required_files[] = $file");
592
				}
593
			}
594
		}
595
		//echo "<p>".print_r($this->Link_ID->ServerInfo(),true)."</p>\n";
596
		return $this->Link_ID;
597
	}
598
599
	/**
600
	 * Magic method to re-connect with the database, if the object get's restored from the session
601
	 */
602
	function __wakeup()
603
	{
604
		$this->connect();	// we need to re-connect
605
	}
606
607
	/**
608
	 * Magic method called when object get's serialized
609
	 *
610
	 * We do NOT store Link_ID and private_Link_ID, as we need to reconnect anyway.
611
	 * This also ensures reevaluating environment-data or multiple hosts in connection-data!
612
	 *
613
	 * @return array
614
	 */
615
	function __sleep()
616
	{
617
		if (!empty($this->setupType)) $this->Type = $this->setupType;	// restore Type eg. to mysqli
618
619
		$vars = get_object_vars($this);
620
		unset($vars['Link_ID'], $vars['Query_ID'], $vars['privat_Link_ID']);
621
		return array_keys($vars);
622
	}
623
624
	/**
625
	 * changes defaults set in class-var $capabilities depending on db-type and -version
626
	 *
627
	 * @param string $adodb_driver mysql, postgres, mssql, sapdb, oci8
628
	 * @param string $db_version version-number of connected db-server, as reported by ServerInfo
629
	 */
630
	function set_capabilities($adodb_driver,$db_version)
631
	{
632
		switch($adodb_driver)
633
		{
634
			case 'mysql':
635
			case 'mysqlt':
636
			case 'mysqli':
637
				$this->capabilities[self::CAPABILITY_SUB_QUERIES] = (float) $db_version >= 4.1;
638
				$this->capabilities[self::CAPABILITY_UNION] = (float) $db_version >= 4.0;
639
				$this->capabilities[self::CAPABILITY_NAME_CASE] = 'preserv';
640
				$this->capabilities[self::CAPABILITY_CLIENT_ENCODING] = (float) $db_version >= 4.1;
641
				$this->capabilities[self::CAPABILITY_CAST_AS_VARCHAR] = 'CAST(%s AS char)';
642
				break;
643
644
			case 'postgres':
645
				$this->capabilities[self::CAPABILITY_NAME_CASE] = 'lower';
646
				$this->capabilities[self::CAPABILITY_CLIENT_ENCODING] = (float) $db_version >= 7.4;
647
				$this->capabilities[self::CAPABILITY_OUTER_JOIN] = true;
648
				$this->capabilities[self::CAPABILITY_CASE_INSENSITIV_LIKE] = '::text ILIKE';
649
				$this->capabilities[self::CAPABILITY_REQUIRE_TRUNCATE_VARCHAR] = true;
650
				break;
651
652
			case 'mssql':
653
				$this->capabilities[self::CAPABILITY_DISTINCT_ON_TEXT] = false;
654
				$this->capabilities[self::CAPABILITY_ORDER_ON_TEXT] = 'CAST (%s AS varchar)';
655
				break;
656
657
			case 'maxdb':	// if Lim ever changes it to maxdb ;-)
658
			case 'sapdb':
659
				$this->capabilities[self::CAPABILITY_DISTINCT_ON_TEXT] = false;
660
				$this->capabilities[self::CAPABILITY_LIKE_ON_TEXT] = $db_version >= 7.6;
661
				$this->capabilities[self::CAPABILITY_ORDER_ON_TEXT] = false;
662
				break;
663
		}
664
		//echo "db::set_capabilities('$adodb_driver',$db_version)"; _debug_array($this->capabilities);
665
	}
666
667
	/**
668
	* Close a connection to a database
669
	*/
670
	function disconnect()
671
	{
672
		if (!$this->privat_Link_ID)
673
		{
674
			unset($GLOBALS['egw']->ADOdb);
675
		}
676
		unset($this->Link_ID);
677
		$this->Link_ID = 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like 0 of type integer is incompatible with the declared type EGroupware\Api\ADOConnection of property $Link_ID.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
678
679
		if (!empty($this->setupType)) $this->Type = $this->setupType;
680
	}
681
682
	/**
683
	* Convert a unix timestamp to a rdms specific timestamp
684
	*
685
	* @param int unix timestamp
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\unix was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
686
	* @return string rdms specific timestamp
687
	*/
688
	function to_timestamp($epoch)
689
	{
690
		if (!$this->Link_ID && !$this->connect())
691
		{
692
			return False;
693
		}
694
		// the substring is needed as the string is already in quotes
695
		return substr($this->Link_ID->DBTimeStamp($epoch),1,-1);
696
	}
697
698
	/**
699
	* Convert a rdms specific timestamp to a unix timestamp
700
	*
701
	* @param string rdms specific timestamp
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\rdms was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
702
	* @return int unix timestamp
703
	*/
704
	function from_timestamp($timestamp)
705
	{
706
		if (!$this->Link_ID && !$this->connect())
707
		{
708
			return False;
709
		}
710
		return $this->Link_ID->UnixTimeStamp($timestamp);
711
	}
712
713
	/**
714
	 * convert a rdbms specific boolean value
715
	 *
716
	 * @param string $val boolean value in db-specfic notation
717
	 * @return boolean
718
	 */
719
	public static function from_bool($val)
720
	{
721
		return $val && $val[0] !== 'f';	// everthing other then 0 or f[alse] is returned as true
722
	}
723
724
	/**
725
	* Execute a query
726
	*
727
	* @param string $Query_String the query to be executed
728
	* @param int $line the line method was called from - use __LINE__
729
	* @param string $file the file method was called from - use __FILE__
730
	* @param int $offset row to start from, default 0
731
	* @param int $num_rows number of rows to return (optional), default -1 = all, 0 will use $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs']
732
	* @param array|boolean $inputarr array for binding variables to parameters or false (default)
733
	* @param int $fetchmode =self::FETCH_BOTH self::FETCH_BOTH (default), self::FETCH_ASSOC or self::FETCH_NUM
734
	* @param boolean $reconnect =true true: try reconnecting if server closes connection, false: dont (mysql only!)
735
	* @return ADORecordSet or false, if the query fails
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\ADORecordSet was not found. Did you mean ADORecordSet? If so, make sure to prefix the type with \.
Loading history...
736
	* @throws Db\Exception\InvalidSql with $this->Link_ID->ErrorNo() as code
737
	*/
738
	function query($Query_String, $line = '', $file = '', $offset=0, $num_rows=-1, $inputarr=false, $fetchmode=self::FETCH_BOTH, $reconnect=true)
739
	{
740
		unset($line, $file);	// not used anymore
741
742
		if ($Query_String == '')
743
		{
744
			return 0;
745
		}
746
		if (!$this->Link_ID && !$this->connect())
747
		{
748
			return False;
749
		}
750
751
		if ($this->Link_ID->fetchMode != $fetchmode)
752
		{
753
			$this->Link_ID->SetFetchMode($fetchmode);
754
		}
755
		if (!$num_rows)
756
		{
757
			$num_rows = $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs'];
758
		}
759
		if (($this->readonly || $this->log_updates) && !preg_match('/^\(?(SELECT|SET|SHOW)/i', $Query_String))
760
		{
761
			if ($this->log_updates) error_log($Query_String.': '.function_backtrace());
762
			if ($this->readonly)
763
			{
764
				$this->Error = 'Database is readonly';
765
				$this->Errno = -2;
766
				return 0;
767
			}
768
		}
769
		if ($num_rows > 0)
770
		{
771
			$rs = $this->Link_ID->SelectLimit($Query_String,$num_rows,(int)$offset,$inputarr);
772
		}
773
		else
774
		{
775
			$rs = $this->Link_ID->Execute($Query_String,$inputarr);
776
		}
777
		$this->Row = 0;
778
		$this->Errno  = $this->Link_ID->ErrorNo();
779
		$this->Error  = $this->Link_ID->ErrorMsg();
780
781
		if ($this->query_log && ($f = @fopen($this->query_log,'a+')))
782
		{
783
			fwrite($f,'['.(isset($GLOBALS['egw_setup']) ? $GLOBALS['egw_setup']->ConfigDomain : $GLOBALS['egw_info']['user']['domain']).'] ');
784
			fwrite($f,date('Y-m-d H:i:s ').$Query_String.($inputarr ? "\n".print_r($inputarr,true) : '')."\n");
785
			if (!$rs)
786
			{
787
				fwrite($f,"*** Error $this->Errno: $this->Error\n".function_backtrace()."\n");
788
			}
789
			fclose($f);
790
		}
791
		if (!$rs)
792
		{
793
			if ($reconnect && $this->Type == 'mysql' && $this->Errno == 2006)	// Server has gone away
794
			{
795
				$this->disconnect();
796
				return $this->query($Query_String, $line, $file, $offset, $num_rows, $inputarr, $fetchmode, false);
797
			}
798
			throw new Db\Exception\InvalidSql("Invalid SQL: ".(is_array($Query_String)?$Query_String[0]:$Query_String).
0 ignored issues
show
introduced by
The condition is_array($Query_String) is always false.
Loading history...
799
				"\n$this->Error ($this->Errno)".
800
				($inputarr ? "\nParameters: '".implode("','",$inputarr)."'":''), $this->Errno);
801
		}
802
		elseif(empty($rs->sql)) $rs->sql = $Query_String;
803
		return $rs;
804
	}
805
806
	/**
807
	* Execute a query with limited result set
808
	*
809
	* @param string $Query_String the query to be executed
810
	* @param int $offset row to start from, default 0
811
	* @param int $line the line method was called from - use __LINE__
812
	* @param string $file the file method was called from - use __FILE__
813
	* @param int $num_rows number of rows to return (optional), default -1 = all, 0 will use $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs']
814
	* @param array|boolean $inputarr array for binding variables to parameters or false (default)
815
	* @return ADORecordSet or false, if the query fails
816
	*/
817
	function limit_query($Query_String, $offset, $line = '', $file = '', $num_rows = '',$inputarr=false)
818
	{
819
		return $this->query($Query_String,$line,$file,$offset,$num_rows,$inputarr);
820
	}
821
822
	/**
823
	* Begin Transaction
824
	*
825
	* @return int/boolean current transaction-id, of false if no connection
0 ignored issues
show
Documentation Bug introduced by
The doc comment int/boolean at position 0 could not be parsed: Unknown type name 'int/boolean' at position 0 in int/boolean.
Loading history...
826
	*/
827
	function transaction_begin()
828
	{
829
		if (!$this->Link_ID && !$this->connect())
830
		{
831
			return False;
832
		}
833
		//return $this->Link_ID->BeginTrans();
834
		return $this->Link_ID->StartTrans();
835
	}
836
837
	/**
838
	* Complete the transaction
839
	*
840
	* @return bool True if sucessful, False if fails
841
	*/
842
	function transaction_commit()
843
	{
844
		if (!$this->Link_ID && !$this->connect())
845
		{
846
			return False;
847
		}
848
		//return $this->Link_ID->CommitTrans();
849
		return $this->Link_ID->CompleteTrans();
850
	}
851
852
	/**
853
	* Rollback the current transaction
854
	*
855
	* @return bool True if sucessful, False if fails
856
	*/
857
	function transaction_abort()
858
	{
859
		if (!$this->Link_ID && !$this->connect())
860
		{
861
			return False;
862
		}
863
		//return $this->Link_ID->RollbackTrans();
864
		return $this->Link_ID->FailTrans();
865
	}
866
867
	/**
868
	 * Lock a rows in table
869
	 *
870
	 * Will escalate and lock the table if row locking not supported.
871
	 * Will normally free the lock at the end of the transaction.
872
	 *
873
	 * @param string $table name of table to lock
874
	 * @param string $where ='true' where clause to use, eg: "WHERE row=12". Defaults to lock whole table.
875
	 * @param string $col ='1 as adodbignore'
876
	 */
877
	function row_lock($table, $where='true', $col='1 as adodbignore')
878
	{
879
		if (!$this->Link_ID && !$this->connect())
880
		{
881
			return False;
882
		}
883
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
0 ignored issues
show
Bug Best Practice introduced by
The expression self::tablealiases 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...
884
		{
885
			$table = self::$tablealiases[$table];
886
		}
887
888
		return $this->Link_ID->RowLock($table, $where, $col);
889
	}
890
891
	/**
892
	 * Commit changed rows in table
893
	 *
894
	 * @param string $table
895
	 * @return boolean
896
	 */
897
	function commit_lock($table)
898
	{
899
		if (!$this->Link_ID && !$this->connect())
900
		{
901
			return False;
902
		}
903
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
0 ignored issues
show
Bug Best Practice introduced by
The expression self::tablealiases 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...
904
		{
905
			$table = self::$tablealiases[$table];
906
		}
907
908
		return $this->Link_ID->CommitLock($table);
909
	}
910
911
	/**
912
	 * Unlock rows in table
913
	 *
914
	 * @param string $table
915
	 * @return boolean
916
	 */
917
	function rollback_lock($table)
918
	{
919
		if (!$this->Link_ID && !$this->connect())
920
		{
921
			return False;
922
		}
923
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
0 ignored issues
show
Bug Best Practice introduced by
The expression self::tablealiases 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...
924
		{
925
			$table = self::$tablealiases[$table];
926
		}
927
928
		return $this->Link_ID->RollbackLock($table);
929
	}
930
931
	/**
932
	* Find the primary key of the last insertion on the current db connection
933
	*
934
	* @param string $table name of table the insert was performed on
935
	* @param string $field the autoincrement primary key of the table
936
	* @return int the id, -1 if fails
937
	*/
938
	function get_last_insert_id($table, $field)
939
	{
940
		if (!$this->Link_ID && !$this->connect())
941
		{
942
			return False;
943
		}
944
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
0 ignored issues
show
Bug Best Practice introduced by
The expression self::tablealiases 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...
945
		{
946
			$table = self::$tablealiases[$table];
947
		}
948
		$id = $this->Link_ID->PO_Insert_ID($table,$field);	// simulates Insert_ID with "SELECT MAX($field) FROM $table" if not native availible
949
950
		if ($id === False)	// function not supported
951
		{
952
			echo "<p>db::get_last_insert_id(table='$table',field='$field') not yet implemented for db-type '$this->Type' OR no insert operation before</p>\n";
953
			echo '<p>'.function_backtrace()."</p>\n";
954
			return -1;
955
		}
956
		return $id;
957
	}
958
959
	/**
960
	* Get the number of rows affected by last update or delete
961
	*
962
	* @return int number of rows
963
	*/
964
	function affected_rows()
965
	{
966
		if ($this->log_updates) return 0;
967
968
		if (!$this->Link_ID && !$this->connect())
969
		{
970
			return False;
971
		}
972
		return $this->Link_ID->Affected_Rows();
973
	}
974
975
	/**
976
	* Get description of a table
977
	*
978
	* Beside the column-name all other data depends on the db-type !!!
979
	*
980
	* @param string $table name of table to describe
981
	* @param bool $full optional, default False summary information, True full information
982
	* @return array table meta data
983
	*/
984
	function metadata($table='',$full=false)
985
	{
986
		if (!$this->Link_ID && !$this->connect())
987
		{
988
			return False;
989
		}
990
		$columns = $this->Link_ID->MetaColumns($table);
991
		//$columns = $this->Link_ID->MetaColumnsSQL($table);
992
		//echo "<b>metadata</b>('$table')=<pre>\n".print_r($columns,True)."</pre>\n";
993
994
		$metadata = array();
995
		$i = 0;
996
		foreach($columns as $column)
997
		{
998
			// for backwards compatibilty (depreciated)
999
			$flags = null;
1000
			if($column->auto_increment) $flags .= "auto_increment ";
1001
			if($column->primary_key) $flags .= "primary_key ";
1002
			if($column->binary) $flags .= "binary ";
1003
1004
			$metadata[$i] = array(
1005
				'table' => $table,
1006
				'name'  => $column->name,
1007
				'type'  => $column->type,
1008
				'len'   => $column->max_length,
1009
				'flags' => $flags, // for backwards compatibilty (depreciated) used by JiNN atm
1010
				'not_null' => $column->not_null,
1011
				'auto_increment' => $column->auto_increment,
1012
				'primary_key' => $column->primary_key,
1013
				'binary' => $column->binary,
1014
				'has_default' => $column->has_default,
1015
				'default'  => $column->default_value,
1016
			);
1017
			$metadata[$i]['table'] = $table;
1018
			if ($full)
1019
			{
1020
				$metadata['meta'][$column->name] = $i;
1021
			}
1022
			++$i;
1023
		}
1024
		if ($full)
1025
		{
1026
			$metadata['num_fields'] = $i;
1027
		}
1028
		return $metadata;
1029
	}
1030
1031
	/**
1032
	 * Get a list of table names in the current database
1033
	 *
1034
	 * @param boolean $just_name =false true return array of table-names, false return old format
1035
	 * @return array list of the tables
1036
	 */
1037
	function table_names($just_name=false)
1038
	{
1039
		if (!$this->Link_ID) $this->connect();
1040
		if (!$this->Link_ID)
1041
		{
1042
			return False;
1043
		}
1044
		$result = array();
1045
		$tables = $this->Link_ID->MetaTables('TABLES');
1046
		if (is_array($tables))
1047
		{
1048
			foreach($tables as $table)
1049
			{
1050
				if ($this->capabilities[self::CAPABILITY_NAME_CASE] == 'upper')
1051
				{
1052
					$table = strtolower($table);
1053
				}
1054
				$result[] = $just_name ? $table : array(
1055
					'table_name'      => $table,
1056
					'tablespace_name' => $this->Database,
1057
					'database'        => $this->Database
1058
				);
1059
			}
1060
		}
1061
		return $result;
1062
	}
1063
1064
	/**
1065
	* Return a list of indexes in current database
1066
	*
1067
	* @return array list of indexes
1068
	*/
1069
	function index_names()
1070
	{
1071
		$indices = array();
1072
		if ($this->Type != 'pgsql')
1073
		{
1074
			echo "<p>db::index_names() not yet implemented for db-type '$this->Type'</p>\n";
1075
			return $indices;
1076
		}
1077
		foreach($this->query("SELECT relname FROM pg_class WHERE NOT relname ~ 'pg_.*' AND relkind ='i' ORDER BY relname") as $row)
1078
		{
1079
			$indices[] = array(
1080
				'index_name'      => $row[0],
1081
				'tablespace_name' => $this->Database,
1082
				'database'        => $this->Database,
1083
			);
1084
		}
1085
		return $indices;
1086
	}
1087
1088
	/**
1089
	* Returns an array containing column names that are the primary keys of $tablename.
1090
	*
1091
	* @return array of columns
1092
	*/
1093
	function pkey_columns($tablename)
1094
	{
1095
		if (!$this->Link_ID && !$this->connect())
1096
		{
1097
			return False;
1098
		}
1099
		return $this->Link_ID->MetaPrimaryKeys($tablename);
1100
	}
1101
1102
	/**
1103
	* Create a new database
1104
	*
1105
	* @param string $adminname name of database administrator user (optional)
1106
	* @param string $adminpasswd password for the database administrator user (optional)
1107
	* @param string $charset default charset for the database
1108
	* @param string $grant_host ='localhost' host/ip of the webserver
1109
	*/
1110
	function create_database($adminname = '', $adminpasswd = '', $charset='', $grant_host='localhost')
1111
	{
1112
		$currentUser = $this->User;
1113
		$currentPassword = $this->Password;
1114
		$currentDatabase = $this->Database;
1115
1116
		if ($adminname != '')
1117
		{
1118
			$this->User = $adminname;
1119
			$this->Password = $adminpasswd;
1120
			$this->Database = $this->Type == 'pgsql' ? 'template1' : 'mysql';
1121
		}
1122
		$this->disconnect();
1123
1124
		$sqls = array();
1125
		switch ($this->Type)
1126
		{
1127
			case 'pgsql':
1128
				$sqls[] = "CREATE DATABASE $currentDatabase";
1129
				break;
1130
			case 'mysql':
1131
			case 'mysqli':
1132
			case 'mysqlt':
1133
				$create = "CREATE DATABASE `$currentDatabase`";
1134
				if ($charset && isset($this->Link_ID->charset2mysql[$charset]) && (float) $this->ServerInfo['version'] >= 4.1)
0 ignored issues
show
Bug introduced by
The property charset2mysql does not exist on integer.
Loading history...
1135
				{
1136
					$create .= ' DEFAULT CHARACTER SET '.$this->Link_ID->charset2mysql[$charset].';';
1137
				}
1138
				$sqls[] = $create;
1139
				$sqls[] = "CREATE USER $currentUser@'$grant_host' IDENTIFIED BY ".$this->quote($currentPassword);
1140
				$sqls[] = "GRANT ALL PRIVILEGES ON `$currentDatabase`.* TO $currentUser@'$grant_host'";
1141
				break;
1142
			default:
1143
				throw new Exception\WrongParameter(__METHOD__."(user=$adminname, \$pw) not yet implemented for DB-type '$this->Type'");
1144
		}
1145
		//error_log(__METHOD__."() this->Type=$this->Type: sqls=".array2string($sqls));
1146
		foreach($sqls as $sql)
1147
		{
1148
			$this->query($sql,__LINE__,__FILE__);
1149
		}
1150
		$this->disconnect();
1151
1152
		$this->User = $currentUser;
1153
		$this->Password = $currentPassword;
1154
		$this->Database = $currentDatabase;
1155
		$this->connect();
1156
	}
1157
1158
	/**
1159
	 * concat a variable number of strings together, to be used in a query
1160
	 *
1161
	 * Example: $db->concat($db->quote('Hallo '),'username') would return
1162
	 *	for mysql "concat('Hallo ',username)" or "'Hallo ' || username" for postgres
1163
	 * @param string $str1 already quoted stringliteral or column-name, variable number of arguments
1164
	 * @return string to be used in a query
1165
	 */
1166
	function concat(/*$str1, ...*/)
1167
	{
1168
		$args = func_get_args();
1169
1170
		if (!$this->Link_ID && !$this->connect())
1171
		{
1172
			return False;
1173
		}
1174
		return call_user_func_array(array(&$this->Link_ID,'concat'),$args);
1175
	}
1176
1177
	/**
1178
	 * Concat grouped values of an expression with optional order and separator
1179
	 *
1180
	 * @param string $expr column-name or expression optional prefixed with "DISTINCT"
1181
	 * @param string $order_by ='' optional order
1182
	 * @param string $separator =',' optional separator, default is comma
1183
	 * @return string|boolean false if not supported by dbms
1184
	 */
1185
	function group_concat($expr, $order_by='', $separator=',')
1186
	{
1187
		switch($this->Type)
1188
		{
1189
			case 'mysqli':
1190
			case 'mysql':
1191
				$sql = 'GROUP_CONCAT('.$expr;
1192
				if ($order_by) $sql .= ' ORDER BY '.$order_by;
1193
				if ($separator != ',') $sql .= ' SEPARATOR '.$this->quote($separator);
1194
				$sql .= ')';
1195
				break;
1196
1197
			case 'pgsql':	// requires for Postgresql < 8.4 to have a custom ARRAY_AGG method installed!
1198
				if ($this->Type == 'pgsql' && $this->ServerInfo['version'] < 8.4)
1199
				{
1200
					return false;
1201
				}
1202
				$sql = 'ARRAY_TO_STRING(ARRAY_AGG('.$expr;
1203
				if ($order_by) $sql .= ' ORDER BY '.$order_by;
1204
				$sql .= '), '.$this->quote($separator).')';
1205
				break;
1206
1207
			default:	// probably gives an sql error anyway
1208
				return false;
1209
		}
1210
		return $sql;
1211
	}
1212
1213
	/**
1214
	 * SQL returning character (not byte!) positions for $substr in $str
1215
	 *
1216
	 * @param string $str
1217
	 * @param string $substr
1218
	 * @return string SQL returning character (not byte!) positions for $substr in $str
1219
	 */
1220
	function strpos($str, $substr)
1221
	{
1222
		switch($this->Type)
1223
		{
1224
			case 'mysql':
1225
				return "LOCATE($substr,$str)";
1226
			case 'pgsql':
1227
				return "STRPOS($str,$substr)";
1228
			case 'mssql':
1229
				return "CHARINDEX($substr,$str)";
1230
		}
1231
		die(__METHOD__." not implemented for DB type '$this->Type'!");
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1232
	}
1233
1234
	/**
1235
	 * Convert a DB specific timestamp in a unix timestamp stored as integer, like MySQL: UNIX_TIMESTAMP(ts)
1236
	 *
1237
	 * @param string $expr name of an integer column or integer expression
1238
	 * @return string SQL expression of type timestamp
1239
	 */
1240
	function unix_timestamp($expr)
1241
	{
1242
		switch($this->Type)
1243
		{
1244
			case 'mysql':
1245
				return "UNIX_TIMESTAMP($expr)";
1246
1247
			case 'pgsql':
1248
				return "EXTRACT(EPOCH FROM CAST($expr AS TIMESTAMP))";
1249
1250
			case 'mssql':
1251
				return "DATEDIFF(second,'1970-01-01',($expr))";
1252
		}
1253
	}
1254
1255
	/**
1256
	 * Convert a unix timestamp stored as integer in the db into a db timestamp, like MySQL: FROM_UNIXTIME(ts)
1257
	 *
1258
	 * @param string $expr name of an integer column or integer expression
1259
	 * @return string SQL expression of type timestamp
1260
	 */
1261
	function from_unixtime($expr)
1262
	{
1263
		switch($this->Type)
1264
		{
1265
			case 'mysql':
1266
				return "FROM_UNIXTIME($expr)";
1267
1268
			case 'pgsql':
1269
				return "(TIMESTAMP WITH TIME ZONE 'epoch' + ($expr) * INTERVAL '1 sec')";
1270
1271
			case 'mssql':	// we use date(,0) as we store server-time
1272
				return "DATEADD(second,($expr),'".date('Y-m-d H:i:s',0)."')";
1273
		}
1274
		return false;
1275
	}
1276
1277
	/**
1278
	 * format a timestamp as string, like MySQL: DATE_FORMAT(ts)
1279
	 *
1280
	 * Please note: only a subset of the MySQL formats are implemented
1281
	 *
1282
	 * @param string $expr name of a timestamp column or timestamp expression
1283
	 * @param string $format format specifier like '%Y-%m-%d %H:%i:%s' or '%V%X' ('%v%x') weeknumber & year with Sunday (Monday) as first day
1284
	 * @return string SQL expression of type timestamp
1285
	 */
1286
	function date_format($expr,$format)
1287
	{
1288
		switch($this->Type)
1289
		{
1290
			case 'mysql':
1291
				return "DATE_FORMAT($expr,'$format')";
1292
1293
			case 'pgsql':
1294
				$format = str_replace(
1295
					array('%Y',  '%y','%m','%d','%H',  '%h','%i','%s','%V','%v','%X',  '%x'),
1296
					array('YYYY','YY','MM','DD','HH24','HH','MI','SS','IW','IW','YYYY','YYYY'),
1297
					$format);
1298
				return "TO_CHAR($expr,'$format')";
1299
1300
			case 'mssql':
1301
				$from = $to = array();
1302
				foreach(array('%Y'=>'yyyy','%y'=>'yy','%m'=>'mm','%d'=>'dd','%H'=>'hh','%i'=>'mi','%s'=>'ss','%V'=>'wk','%v'=>'wk','%X'=>'yyyy','%x'=>'yyyy') as $f => $t)
1303
				{
1304
					$from[] = $f;
1305
					$to[] = "'+DATEPART($t,($expr))+'";
1306
				}
1307
				$from[] = "''+"; $to[] = '';
1308
				$from[] = "+''"; $to[] = '';
1309
				return str_replace($from,$to,$format);
1310
		}
1311
		return false;
1312
	}
1313
1314
	/**
1315
	 * Cast a column or sql expression to integer, necessary at least for postgreSQL or MySQL for sorting
1316
	 *
1317
	 * @param string $expr
1318
	 * @return string
1319
	 */
1320
	function to_double($expr)
1321
	{
1322
		switch($this->Type)
1323
		{
1324
			case 'pgsql':
1325
				return $expr.'::double';
1326
			case 'mysql':
1327
				return 'CAST('.$expr.' AS DECIMAL(24,3))';
1328
		}
1329
		return $expr;
1330
	}
1331
1332
	/**
1333
	 * Cast a column or sql expression to integer, necessary at least for postgreSQL
1334
	 *
1335
	 * @param string $expr
1336
	 * @return string
1337
	 */
1338
	function to_int($expr)
1339
	{
1340
		switch($this->Type)
1341
		{
1342
			case 'pgsql':
1343
				return $expr.'::integer';
1344
			case 'mysql':
1345
				return 'CAST('.$expr.' AS SIGNED)';
1346
		}
1347
		return $expr;
1348
	}
1349
1350
	/**
1351
	 * Cast a column or sql expression to varchar, necessary at least for postgreSQL
1352
	 *
1353
	 * @param string $expr
1354
	 * @return string
1355
	 */
1356
	function to_varchar($expr)
1357
	{
1358
		switch($this->Type)
1359
		{
1360
			case 'pgsql':
1361
				return 'CAST('.$expr.' AS varchar)';
1362
		}
1363
		return $expr;
1364
	}
1365
1366
	/**
1367
	* Correctly Quote Identifiers like table- or colmnnames for use in SQL-statements
1368
	*
1369
	* This is mostly copy & paste from adodb's datadict class
1370
	* @param string $_name
1371
	* @return string quoted string
1372
	*/
1373
	function name_quote($_name = NULL)
1374
	{
1375
		if (!is_string($_name))
1376
		{
1377
			return false;
1378
		}
1379
1380
		$name = trim($_name);
1381
1382
		if (!$this->Link_ID && !$this->connect())
1383
		{
1384
			return false;
1385
		}
1386
1387
		$quote = $this->Link_ID->nameQuote;
1388
		$type = $this->Type;
1389
1390
		// if name is of the form `name`, remove MySQL quotes and leave it to automatic below
1391
		if ($name[0] === '`' && substr($name, -1) === '`')
1392
		{
1393
			$name = substr($name, 1, -1);
1394
		}
1395
1396
		$quoted = array_map(function($name) use ($quote, $type)
1397
		{
1398
			// if name contains special characters, quote it
1399
			// always quote for postgreSQL, as this is the only way to support mixed case names
1400
			if (preg_match('/\W/', $name) || $type == 'pgsql' && preg_match('/[A-Z]+/', $name) || $name == 'index')
1401
			{
1402
				return $quote . $name . $quote;
1403
			}
1404
			return $name;
1405
		}, explode('.', $name));
1406
1407
		return implode('.', $quoted);
1408
	}
1409
1410
	/**
1411
	* Escape values before sending them to the database - prevents SQL injection and SQL errors ;-)
1412
	*
1413
	* Please note that the quote function already returns necessary quotes: quote('Hello') === "'Hello'".
1414
	* Int and Auto types are casted to int: quote('1','int') === 1, quote('','int') === 0, quote('Hello','int') === 0
1415
	* Arrays of id's stored in strings: quote(array(1,2,3),'string') === "'1,2,3'"
1416
	*
1417
	* @param mixed $value the value to be escaped
1418
	* @param string|boolean $type =false string the type of the db-column, default False === varchar
1419
	* @param boolean $not_null =true is column NOT NULL, default true, else php null values are written as SQL NULL
1420
	* @param int $length =null length of the varchar column, to truncate it if the database requires it (eg. Postgres)
1421
	* @param string $glue =',' used to glue array values together for the string type
1422
	* @return string escaped sting
1423
	*/
1424
	function quote($value,$type=False,$not_null=true,$length=null,$glue=',')
1425
	{
1426
		if ($this->Debug) echo "<p>db::quote(".(is_null($value)?'NULL':"'$value'").",'$type','$not_null')</p>\n";
1427
1428
		if (!$not_null && is_null($value))	// writing unset php-variables and those set to NULL now as SQL NULL
1429
		{
1430
			return 'NULL';
1431
		}
1432
		switch($type)
1433
		{
1434
			case 'int':
1435
				// if DateTime object given, convert it to a unix timestamp (NOT converting the timezone!)
1436
				if (is_object($value) && ($value instanceof \DateTime))
1437
				{
1438
					return ($value instanceof DateTime) ? $value->format('ts') : DateTime::to($value,'ts');
1439
				}
1440
			case 'auto':
1441
				// atm. (php5.2) php has only 32bit integers, it converts everything else to float.
1442
				// Casting it to int gives a negative number instead of the big 64bit integer!
1443
				// There for we have to keep it as float by using round instead the int cast.
1444
				return is_float($value) ? round($value) : (int) $value;
1445
			case 'bool':
1446
				if ($this->Type == 'mysql')		// maybe it's not longer necessary with mysql5
1447
				{
1448
					return $value ? 1 : 0;
1449
				}
1450
				return $value ? 'true' : 'false';
1451
			case 'float':
1452
			case 'decimal':
1453
				return (double) $value;
1454
		}
1455
		if (!$this->Link_ID && !$this->connect())
1456
		{
1457
			return False;
1458
		}
1459
		switch($type)
1460
		{
1461
			case 'blob':
1462
				switch ($this->Link_ID->blobEncodeType)
1463
				{
1464
					case 'C':	// eg. postgres
1465
						return "'" . $this->Link_ID->BlobEncode($value) . "'";
1466
					case 'I':
1467
						return $this->Link_ID->BlobEncode($value);
1468
				}
1469
				break;	// handled like strings
1470
			case 'date':
1471
				// if DateTime object given, convert it (NOT converting the timezone!)
1472
				if (is_object($value) && ($value instanceof \DateTime))
1473
				{
1474
					return $this->Link_ID->qstr($value->format('Y-m-d'));
1475
				}
1476
				return $this->Link_ID->DBDate($value);
1477
			case 'timestamp':
1478
				// if DateTime object given, convert it (NOT converting the timezone!)
1479
				if (is_object($value) && ($value instanceof \DateTime))
1480
				{
1481
					return $this->Link_ID->qstr($value->format('Y-m-d H:i:s'));
1482
				}
1483
				return $this->Link_ID->DBTimeStamp($value);
1484
		}
1485
		if (is_array($value))
1486
		{
1487
			$value = implode($glue,$value);
1488
		}
1489
		// truncate to long strings for varchar(X) columns as PostgreSQL and newer MySQL/MariaDB given an error otherwise
1490
		if (!is_null($length) && mb_strlen($value) > $length)
1491
		{
1492
			$value = mb_substr($value, 0, $length);
1493
		}
1494
		// casting boolean explicitly to string, as ADODB_postgres64::qstr() has an unwanted special handling
1495
		// for boolean types, causing it to return "true" or "false" and not a quoted string like "'1'"!
1496
		if (is_bool($value)) $value = (string)$value;
1497
1498
		// MySQL and MariaDB not 10.1 need 4-byte utf8 chars replaced with our default utf8 charset
1499
		// (MariaDB 10.1 does the replacement automatic, 10.0 cuts everything off behind and MySQL gives an error)
1500
		// (MariaDB 10.3 gives an error too: Incorrect string value: '\xF0\x9F\x98\x8A\x0AW...')
1501
		// Changing charset to utf8mb4 requires schema update, shortening of some indexes and probably have negative impact on performace!
1502
		if (substr($this->Type, 0, 5) == 'mysql')
1503
		{
1504
			$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
1505
		}
1506
1507
		// need to cast to string, as ADOdb 5.20 would return NULL instead of '' for NULL, causing us to write that into NOT NULL columns
1508
		return $this->Link_ID->qstr((string)$value);
1509
	}
1510
1511
	/**
1512
	* Implodes an array of column-value pairs for the use in sql-querys.
1513
	* All data is run through quote (does either addslashes() or (int)) - prevents SQL injunction and SQL errors ;-).
1514
	*
1515
	* @author RalfBecker<at>outdoor-training.de
1516
	*
1517
	* @param string $glue in most cases this will be either ',' or ' AND ', depending you your query
1518
	* @param array $array column-name / value pairs, if the value is an array all its array-values will be quoted
1519
	*	according to the type of the column, and the whole array with be formatted like (val1,val2,...)
1520
	*	If $use_key == True, an ' IN ' instead a '=' is used. Good for category- or user-lists.
1521
	*	If the key is numerical (no key given in the array-definition) the value is used as is, eg.
1522
	*	array('visits=visits+1') gives just "visits=visits+1" (no quoting at all !!!)
1523
	* @param boolean|string $use_key If $use_key===True a "$key=" prefix each value (default), typically set to False
1524
	*	or 'VALUES' for insert querys, on 'VALUES' "(key1,key2,...) VALUES (val1,val2,...)" is returned
1525
	* @param array|boolean $only if set to an array only colums which are set (as data !!!) are written
1526
	*	typicaly used to form a WHERE-clause from the primary keys.
1527
	*	If set to True, only columns from the colum_definitons are written.
1528
	* @param array|boolean $column_definitions this can be set to the column-definitions-array
1529
	*	of your table ($tables_baseline[$table]['fd'] of the setup/tables_current.inc.php file).
1530
	*	If its set, the column-type-data determinates if (int) or addslashes is used.
1531
	* @return string SQL
1532
	*/
1533
	function column_data_implode($glue,$array,$use_key=True,$only=False,$column_definitions=False)
1534
	{
1535
		if (!is_array($array))	// this allows to give an SQL-string for delete or update
0 ignored issues
show
introduced by
The condition is_array($array) is always true.
Loading history...
1536
		{
1537
			return $array;
1538
		}
1539
		if (!$column_definitions)
1540
		{
1541
			$column_definitions = $this->column_definitions;
1542
		}
1543
		if ($this->Debug) echo "<p>db::column_data_implode('$glue',".print_r($array,True).",'$use_key',".print_r($only,True).",<pre>".print_r($column_definitions,True)."</pre>\n";
1544
1545
		// do we need to truncate varchars to their max length (INSERT and UPDATE on Postgres)
1546
		$truncate_varchar = $glue == ',' && $this->capabilities[self::CAPABILITY_REQUIRE_TRUNCATE_VARCHAR];
1547
1548
		$keys = $values = array();
1549
		foreach($array as $key => $data)
1550
		{
1551
			if (is_int($key) && $use_key !== 'VALUES' || !$only || $only === True && isset($column_definitions[$key]) ||
1552
				is_array($only) && in_array($key,$only))
1553
			{
1554
				$keys[] = $this->name_quote($key);
1555
1556
				$col = $key;
1557
				// fix "table.column" expressions, to not trigger exception, if column alone would work
1558
				if (!is_int($key) && is_array($column_definitions) && !isset($column_definitions[$key]))
1559
				{
1560
					if (strpos($key, '.') !== false) list(, $col) = explode('.', $key);
1561
					if (!isset($column_definitions[$col]))
1562
					{
1563
						throw new Db\Exception\InvalidSql("db::column_data_implode('$glue',".print_r($array,True).",'$use_key',".print_r($only,True).",<pre>".print_r($column_definitions,True)."</pre><b>nothing known about column '$key'!</b>");
1564
					}
1565
				}
1566
				$column_type = is_array($column_definitions) ? @$column_definitions[$col]['type'] : False;
1567
				$not_null = is_array($column_definitions) && isset($column_definitions[$col]['nullable']) ? !$column_definitions[$col]['nullable'] : false;
1568
1569
				$maxlength = null;
1570
				if ($truncate_varchar)
1571
				{
1572
					$maxlength = in_array($column_definitions[$col]['type'], array('varchar','ascii')) ? $column_definitions[$col]['precision'] : null;
1573
				}
1574
				// dont use IN ( ), if there's only one value, it's slower for MySQL
1575
				if (is_array($data) && count($data) <= 1)
1576
				{
1577
					$data = array_shift($data);
1578
				}
1579
				// array for SET or VALUES, not WHERE --> automatic store comma-separated
1580
				if (is_array($data) && $glue === ',' && in_array($column_type, ['varchar','ascii']))
1581
				{
1582
					$data = implode(',', $data);
1583
				}
1584
				if (is_array($data))
1585
				{
1586
					$or_null = '';
1587
					foreach($data as $k => $v)
1588
					{
1589
						if (!$not_null && $use_key===True && is_null($v))
1590
						{
1591
							$or_null = $this->name_quote($key).' IS NULL)';
1592
							unset($data[$k]);
1593
							continue;
1594
						}
1595
						$data[$k] = $this->quote($v,$column_type,$not_null,$maxlength);
1596
					}
1597
					$values[] = ($or_null?'(':'').(!count($data) ?
1598
						// empty array on insert/update, store as NULL, or if not allowed whatever value NULL is casted to
1599
						$this->quote(null, $column_type, $not_null) :
1600
						($use_key===True ? $this->name_quote($key).' IN ' : '') .
1601
						'('.implode(',',$data).')'.($or_null ? ' OR ' : '')).$or_null;
1602
				}
1603
				elseif (is_int($key) && $use_key===True)
1604
				{
1605
					if (empty($data)) continue;	// would give SQL error
1606
					$values[] = $data;
1607
				}
1608
				elseif ($glue != ',' && $use_key === True && !$not_null && is_null($data))
1609
				{
1610
					$values[] = $this->name_quote($key) .' IS NULL';
1611
				}
1612
				else
1613
				{
1614
					$values[] = ($use_key===True ? $this->name_quote($key) . '=' : '') . $this->quote($data,$column_type,$not_null,$maxlength);
1615
				}
1616
			}
1617
		}
1618
		return ($use_key==='VALUES' ? '('.implode(',',$keys).') VALUES (' : '').
1619
			implode($glue,$values) . ($use_key==='VALUES' ? ')' : '');
1620
	}
1621
1622
	/**
1623
	* Sets the default column-definitions for use with column_data_implode()
1624
	*
1625
	* @author RalfBecker<at>outdoor-training.de
1626
	*
1627
	* @param array|boolean $column_definitions this can be set to the column-definitions-array
1628
	*	of your table ($tables_baseline[$table]['fd'] of the setup/tables_current.inc.php file).
1629
	*	If its set, the column-type-data determinates if (int) or addslashes is used.
1630
	*/
1631
	function set_column_definitions($column_definitions=False)
1632
	{
1633
		$this->column_definitions=$column_definitions;
0 ignored issues
show
Bug Best Practice introduced by
The property column_definitions does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1634
	}
1635
1636
	/**
1637
	 * Application name used by the API
1638
	 *
1639
	 */
1640
	const API_APPNAME = 'api';
1641
	/**
1642
	 * Default app, if no app specified in select, insert, delete, ...
1643
	 *
1644
	 * @var string
1645
	 */
1646
	private $app=self::API_APPNAME;
1647
1648
	/**
1649
	 * Sets the application in which the db-class looks for table-defintions
1650
	 *
1651
	 * Used by table_definitions, insert, update, select, expression and delete. If the app is not set via set_app,
1652
	 * it need to be set for these functions on every call
1653
	 *
1654
	 * @param string $app the app-name
1655
	 */
1656
	function set_app($app)
1657
	{
1658
		// ease the transition to api
1659
		if ($app == 'phpgwapi') $app = 'api';
1660
1661
		if ($this === $GLOBALS['egw']->db && $app != self::API_APPNAME)
1662
		{
1663
			// prevent that anyone switches the global db object to an other app
1664
			throw new Exception\WrongParameter('You are not allowed to call set_app for $GLOBALS[egw]->db or a refence to it, you have to clone it!');
1665
		}
1666
		$this->app = $app;
1667
	}
1668
1669
	/**
1670
	 * Data used by (get|set)_table_defintion and get_column_attribute
1671
	 *
1672
	 * @var array
1673
	 */
1674
	protected static $all_app_data = array();
1675
1676
	/**
1677
	 * Set/changes definition of one table
1678
	 *
1679
	 * If you set or change defition of a single table of an app, other tables
1680
	 * are not loaded from $app/setup/tables_current.inc.php!
1681
	 *
1682
	 * @param string $app name of the app $table belongs too
1683
	 * @param string $table table name
1684
	 * @param array $definition table definition
1685
	 */
1686
	public static function set_table_definitions($app, $table, array $definition)
1687
	{
1688
		self::$all_app_data[$app][$table] = $definition;
1689
	}
1690
1691
	/**
1692
	* reads the table-definitions from the app's setup/tables_current.inc.php file
1693
	*
1694
	* The already read table-definitions are shared between all db-instances via a static var.
1695
	*
1696
	* @author RalfBecker<at>outdoor-training.de
1697
	*
1698
	* @param bool|string $app name of the app or default False to use the app set by db::set_app or the current app,
1699
	*	true to search the already loaded table-definitions for $table and then search all existing apps for it
1700
	* @param bool|string $table if set return only defintions of that table, else return all defintions
1701
	* @return mixed array with table-defintions or False if file not found
1702
	*/
1703
	function get_table_definitions($app=False,$table=False)
1704
	{
1705
		// ease the transition to api
1706
		if ($app === 'phpgwapi') $app = 'api';
1707
1708
		if ($app === true && $table)
1709
		{
1710
			foreach(self::$all_app_data as $app => &$app_data)
1711
			{
1712
				if (isset($app_data[$table]))
1713
				{
1714
					return $app_data[$table];
1715
				}
1716
			}
1717
			// $table not found in loaded apps, check not yet loaded ones
1718
			foreach(scandir(EGW_INCLUDE_ROOT) as $app)
1719
			{
1720
				if ($app[0] == '.' || !is_dir(EGW_INCLUDE_ROOT.'/'.$app) || isset(self::$all_app_data[$app]))
1721
				{
1722
					continue;
1723
				}
1724
				$tables_current = EGW_INCLUDE_ROOT . "/$app/setup/tables_current.inc.php";
1725
				if (!@file_exists($tables_current))
1726
				{
1727
					self::$all_app_data[$app] = False;
1728
				}
1729
				else
1730
				{
1731
					$phpgw_baseline = null;
1732
					include($tables_current);
1733
					self::$all_app_data[$app] =& $phpgw_baseline;
1734
					unset($phpgw_baseline);
1735
1736
					if (isset(self::$all_app_data[$app][$table]))
1737
					{
1738
						return self::$all_app_data[$app][$table];
1739
					}
1740
				}
1741
			}
1742
			$app = false;
1743
		}
1744
		if (!$app)
1745
		{
1746
			$app = $this->app ? $this->app : $GLOBALS['egw_info']['flags']['currentapp'];
1747
		}
1748
		$app_data =& self::$all_app_data[$app];
1749
1750
		if (!isset($app_data))
1751
		{
1752
			$tables_current = EGW_INCLUDE_ROOT . "/$app/setup/tables_current.inc.php";
1753
			if (!@file_exists($tables_current))
1754
			{
1755
				return $app_data = False;
1756
			}
1757
			include($tables_current);
1758
			$app_data =& $phpgw_baseline;
1759
			unset($phpgw_baseline);
1760
		}
1761
		if ($table && (!$app_data || !isset($app_data[$table])))
1762
		{
1763
			if ($this->Debug) echo "<p>!!!get_table_definitions($app,$table) failed!!!</p>\n";
1764
			return False;
1765
		}
1766
		if ($this->Debug) echo "<p>get_table_definitions($app,$table) succeeded</p>\n";
1767
		return $table ? $app_data[$table] : $app_data;
1768
	}
1769
1770
	/**
1771
	 * Get specified attribute (default comment) of a colum or whole definition (if $attribute === null)
1772
	 *
1773
	 * Can be used static, in which case the global db object is used ($GLOBALS['egw']->db) and $app should be specified
1774
	 *
1775
	 * @param string $column name of column
1776
	 * @param string $table name of table
1777
	 * @param string $app=null app name or NULL to use $this->app, set via self::set_app()
1778
	 * @param string $attribute='comment' what field to return, NULL for array with all fields, default 'comment' to return the comment
1779
	 * @return string|array NULL if table or column or attribute not found
1780
	 */
1781
	/* static */ function get_column_attribute($column,$table,$app=null,$attribute='comment')
1782
	{
1783
		static $cached_columns=null,$cached_table=null;	// some caching
1784
1785
		if ($cached_table !== $table || is_null($cached_columns))
1786
		{
1787
			$db = isset($this) && is_a($this, __CLASS__) ? $this : $GLOBALS['egw']->db;
1788
			$table_def = $db->get_table_definitions($app,$table);
1789
			$cached_columns = is_array($table_def) ? $table_def['fd'] : false;
1790
		}
1791
		if ($cached_columns === false) return null;
1792
1793
		return is_null($attribute) ? $cached_columns[$column] : $cached_columns[$column][$attribute];
1794
	}
1795
1796
	/**
1797
	* Insert a row of data into a table or updates it if $where is given, all data is quoted according to it's type
1798
	*
1799
	* @author RalfBecker<at>outdoor-training.de
1800
	*
1801
	* @param string $table name of the table
1802
	* @param array $data with column-name / value pairs
1803
	* @param mixed $where string with where clause or array with column-name / values pairs to check if a row with that keys already exists, or false for an unconditional insert
1804
	*	if the row exists db::update is called else a new row with $date merged with $where gets inserted (data has precedence)
1805
	* @param int $line line-number to pass to query
1806
	* @param string $file file-name to pass to query
1807
	* @param string|boolean $app string with name of app or False to use the current-app
1808
	* @param bool $use_prepared_statement use a prepared statement
1809
	* @param array|bool $table_def use this table definition. If False, the table definition will be read from tables_baseline
1810
	* @return ADORecordSet or false, if the query fails
1811
	*/
1812
	function insert($table,$data,$where,$line,$file,$app=False,$use_prepared_statement=false,$table_def=False)
1813
	{
1814
		if ($this->Debug) echo "<p>db::insert('$table',".print_r($data,True).",".print_r($where,True).",$line,$file,'$app')</p>\n";
1815
1816
		if (!$table_def) $table_def = $this->get_table_definitions($app,$table);
1817
1818
		$sql_append = '';
1819
		$cmd = 'INSERT';
1820
		if (is_array($where) && count($where))
1821
		{
1822
			switch($this->Type)
1823
			{
1824
				case 'sapdb': case 'maxdb':
1825
					$sql_append = ' UPDATE DUPLICATES';
1826
					break;
1827
				case 'mysql':
1828
					// use replace if primary keys are included
1829
					if (count(array_intersect(array_keys($where),(array)$table_def['pk'])) == count($table_def['pk']))
1830
					{
1831
						$cmd = 'REPLACE';
1832
						break;
1833
					}
1834
					// fall through !!!
1835
				default:
1836
					if ($this->select($table,'count(*)',$where,$line,$file)->fetchColumn())
1837
					{
1838
						return !!$this->update($table,$data,$where,$line,$file,$app,$use_prepared_statement,$table_def);
1839
					}
1840
					break;
1841
			}
1842
			// the checked values need to be inserted too, value in data has precedence, also cant insert sql strings (numerical id)
1843
			foreach($where as $column => $value)
1844
			{
1845
				if (!is_numeric($column) && !isset($data[$column]) &&
1846
					// skip auto-id of 0 or NULL, as PostgreSQL does NOT create an auto-id, if they are given
1847
					!(!$value && count($table_def['pk']) == 1 && $column == $table_def['pk'][0]))
1848
				{
1849
					$data[$column] = $value;
1850
				}
1851
			}
1852
		}
1853
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
0 ignored issues
show
Bug Best Practice introduced by
The expression self::tablealiases 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...
1854
		{
1855
			$table = self::$tablealiases[$table];
1856
		}
1857
		$inputarr = false;
1858
		if (isset($data[0]) && is_array($data[0]))	// multiple data rows
1859
		{
1860
			if ($where) throw new Exception\WrongParameter('Can NOT use $where together with multiple data rows in $data!');
1861
1862
			$sql = "$cmd INTO $table ";
1863
			foreach($data as $k => $d)
1864
			{
1865
				if (!$k)
1866
				{
1867
					$sql .= $this->column_data_implode(',',$d,'VALUES',true,$table_def['fd']);
1868
				}
1869
				else
1870
				{
1871
					$sql .= ",\n(".$this->column_data_implode(',',$d,false,true,$table_def['fd']).')';
1872
				}
1873
			}
1874
			$sql .= $sql_append;
1875
		}
1876
		elseif ($use_prepared_statement && $this->Link_ID->_bindInputArray)	// eg. MaxDB
1877
		{
1878
			$this->Link_ID->Param(false);	// reset param-counter
1879
			$cols = array_keys($data);
1880
			foreach($cols as $k => $col)
1881
			{
1882
				if (!isset($table_def['fd'][$col]))	// ignore columns not in this table
1883
				{
1884
					unset($cols[$k]);
1885
					continue;
1886
				}
1887
				$params[] = $this->Link_ID->Param($col);
1888
			}
1889
			$sql = "$cmd INTO $table (".implode(',',$cols).') VALUES ('.implode(',',$params).')'.$sql_append;
1890
			// check if we already prepared that statement
1891
			if (!isset($this->prepared_sql[$sql]))
1892
			{
1893
				$this->prepared_sql[$sql] = $this->Link_ID->Prepare($sql);
1894
			}
1895
			$sql = $this->prepared_sql[$sql];
1896
			$inputarr = &$data;
1897
		}
1898
		else
1899
		{
1900
			$sql = "$cmd INTO $table ".$this->column_data_implode(',',$data,'VALUES',true,$table_def['fd']).$sql_append;
1901
		}
1902
		if ($this->Debug) echo "<p>db::insert('$table',".print_r($data,True).",".print_r($where,True).",$line,$file,'$app') sql='$sql'</p>\n";
1903
		return $this->query($sql,$line,$file,0,-1,$inputarr);
1904
	}
1905
1906
	/**
1907
	* Updates the data of one or more rows in a table, all data is quoted according to it's type
1908
	*
1909
	* @author RalfBecker<at>outdoor-training.de
1910
	*
1911
	* @param string $table name of the table
1912
	* @param array $data with column-name / value pairs
1913
	* @param array $where column-name / values pairs and'ed together for the where clause
1914
	* @param int $line line-number to pass to query
1915
	* @param string $file file-name to pass to query
1916
	* @param string|boolean $app string with name of app or False to use the current-app
1917
	* @param bool $use_prepared_statement use a prepared statement
1918
	* @param array|bool $table_def use this table definition. If False, the table definition will be read from tables_baseline
1919
	* @return ADORecordSet or false, if the query fails
1920
	*/
1921
	function update($table,$data,$where,$line,$file,$app=False,$use_prepared_statement=false,$table_def=False)
1922
	{
1923
		if ($this->Debug) echo "<p>db::update('$table',".print_r($data,true).','.print_r($where,true).",$line,$file,'$app')</p>\n";
1924
		if (!$table_def) $table_def = $this->get_table_definitions($app,$table);
1925
1926
		$blobs2update = array();
1927
		// SapDB/MaxDB cant update LONG columns / blob's: if a blob-column is included in the update we remember it in $blobs2update
1928
		// and remove it from $data
1929
		switch ($this->Type)
1930
		{
1931
			case 'sapdb':
1932
			case 'maxdb':
1933
				if ($use_prepared_statement) break;
1934
				// check if data contains any LONG columns
1935
				foreach($data as $col => $val)
1936
				{
1937
					switch ($table_def['fd'][$col]['type'])
1938
					{
1939
						case 'text':
1940
						case 'longtext':
1941
						case 'blob':
1942
							$blobs2update[$col] = &$data[$col];
1943
							unset($data[$col]);
1944
							break;
1945
					}
1946
				}
1947
				break;
1948
		}
1949
		$where_str = $this->column_data_implode(' AND ',$where,True,true,$table_def['fd']);
1950
1951
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
0 ignored issues
show
Bug Best Practice introduced by
The expression self::tablealiases 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...
1952
		{
1953
			$table = self::$tablealiases[$table];
1954
		}
1955
		if (!empty($data))
1956
		{
1957
			$inputarr = false;
1958
			if ($use_prepared_statement && $this->Link_ID->_bindInputArray)	// eg. MaxDB
1959
			{
1960
				$this->Link_ID->Param(false);	// reset param-counter
1961
				foreach($data as $col => $val)
1962
				{
1963
					if (!isset($table_def['fd'][$col])) continue;	// ignore columns not in this table
1964
					$params[] = $this->name_quote($col).'='.$this->Link_ID->Param($col);
1965
				}
1966
				$sql = "UPDATE $table SET ".implode(',',$params).' WHERE '.$where_str;
1967
				// check if we already prepared that statement
1968
				if (!isset($this->prepared_sql[$sql]))
1969
				{
1970
					$this->prepared_sql[$sql] = $this->Link_ID->Prepare($sql);
1971
				}
1972
				$sql = $this->prepared_sql[$sql];
1973
				$inputarr = &$data;
1974
			}
1975
			else
1976
			{
1977
				$sql = "UPDATE $table SET ".
1978
					$this->column_data_implode(',',$data,True,true,$table_def['fd']).' WHERE '.$where_str;
1979
			}
1980
			$ret = $this->query($sql,$line,$file,0,-1,$inputarr);
1981
			if ($this->Debug) echo "<p>db::query('$sql',$line,$file)</p>\n";
1982
		}
1983
		// if we have any blobs to update, we do so now
1984
		if (($ret || !count($data)) && count($blobs2update))
1985
		{
1986
			foreach($blobs2update as $col => $val)
1987
			{
1988
				$ret = $this->Link_ID->UpdateBlob($table,$col,$val,$where_str,$table_def['fd'][$col]['type'] == 'blob' ? 'BLOB' : 'CLOB');
1989
				if ($this->Debug) echo "<p>adodb::UpdateBlob('$table','$col','$val','$where_str') = '$ret'</p>\n";
1990
				if (!$ret) throw new Db\Exception\InvalidSql("Error in UpdateBlob($table,$col,\$val,$where_str)",$line,$file);
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\Api\Db\Except...validSql::__construct() has too many arguments starting with $file. ( Ignorable by Annotation )

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

1990
				if (!$ret) throw /** @scrutinizer ignore-call */ new Db\Exception\InvalidSql("Error in UpdateBlob($table,$col,\$val,$where_str)",$line,$file);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1991
			}
1992
		}
1993
		return $ret;
1994
	}
1995
1996
	/**
1997
	* Deletes one or more rows in table, all data is quoted according to it's type
1998
	*
1999
	* @author RalfBecker<at>outdoor-training.de
2000
	*
2001
	* @param string $table name of the table
2002
	* @param array $where column-name / values pairs and'ed together for the where clause
2003
	* @param int $line line-number to pass to query
2004
	* @param string $file file-name to pass to query
2005
	* @param string|boolean $app string with name of app or False to use the current-app
2006
	* @param array|bool $table_def use this table definition. If False, the table definition will be read from tables_baseline
2007
	* @return ADORecordSet or false, if the query fails
2008
	*/
2009
	function delete($table,$where,$line,$file,$app=False,$table_def=False)
2010
	{
2011
		if (!$table_def) $table_def = $this->get_table_definitions($app,$table);
2012
2013
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
0 ignored issues
show
Bug Best Practice introduced by
The expression self::tablealiases 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...
2014
		{
2015
			$table = self::$tablealiases[$table];
2016
		}
2017
		$sql = "DELETE FROM $table WHERE ".
2018
			$this->column_data_implode(' AND ',$where,True,False,$table_def['fd']);
2019
2020
		return $this->query($sql,$line,$file);
2021
	}
2022
2023
	/**
2024
	 * Formats and quotes a sql expression to be used eg. as where-clause
2025
	 *
2026
	 * The function has a variable number of arguments, from which the expession gets constructed
2027
	 * eg. db::expression('my_table','(',array('name'=>"test'ed",'lang'=>'en'),') OR ',array('owner'=>array('',4,10)))
2028
	 * gives "(name='test\'ed' AND lang='en') OR 'owner' IN (0,4,5,6,10)" if name,lang are strings and owner is an integer
2029
	 *
2030
	 * @param string|array $table_def table-name or definition array
2031
	 * @param mixed $args variable number of arguments of the following types:
2032
	 *	string: get's as is into the result
2033
	 *	array:	column-name / value pairs: the value gets quoted according to the type of the column and prefixed
2034
	 *		with column-name=, multiple pairs are AND'ed together, see db::column_data_implode
2035
	 *	bool: If False or is_null($arg): the next 2 (!) arguments gets ignored
2036
	 *
2037
	 * Please note: As the function has a variable number of arguments, you CAN NOT add further parameters !!!
2038
	 *
2039
	 * @return string the expression generated from the arguments
2040
	 */
2041
	function expression($table_def/*,$args, ...*/)
2042
	{
2043
		if (!is_array($table_def)) $table_def = $this->get_table_definitions(true,$table_def);
2044
		$sql = '';
2045
		$ignore_next = 0;
2046
		foreach(func_get_args() as $n => $arg)
2047
		{
2048
			if ($n < 1) continue;	// table-name
2049
2050
			if ($ignore_next)
2051
			{
2052
				--$ignore_next;
2053
				continue;
2054
			}
2055
			if (is_null($arg)) $arg = False;
2056
2057
			switch(gettype($arg))
2058
			{
2059
				case 'string':
2060
					$sql .= $arg;
2061
					break;
2062
				case 'boolean':
2063
					$ignore_next += !$arg ? 2 : 0;
2064
					break;
2065
				case 'array':
2066
					$sql .= $this->column_data_implode(' AND ',$arg,True,False,$table_def['fd']);
2067
					break;
2068
			}
2069
		}
2070
		return $sql;
2071
	}
2072
2073
	/**
2074
	* Selects one or more rows in table depending on where, all data is quoted according to it's type
2075
	*
2076
	* @author RalfBecker<at>outdoor-training.de
2077
	*
2078
	* @param string $table name of the table
2079
	* @param array|string $cols string or array of column-names / select-expressions
2080
	* @param array|string $where string or array with column-name / values pairs AND'ed together for the where clause
2081
	* @param int $line line-number to pass to query
2082
	* @param string $file file-name to pass to query
2083
	* @param int|bool $offset offset for a limited query or False (default)
2084
	* @param string $append string to append to the end of the query, eg. ORDER BY ...
2085
	* @param string|boolean $app string with name of app or False to use the current-app
2086
	* @param int $num_rows number of rows to return if offset set, default 0 = use default in user prefs
2087
	* @param string $join =null sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
2088
	*	"LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
2089
	* @param array|bool $table_def use this table definition. If False, the table definition will be read from tables_baseline
2090
	* @param int $fetchmode =self::FETCH_ASSOC self::FETCH_ASSOC (default), self::FETCH_BOTH or self::FETCH_NUM
2091
	* @return ADORecordSet or false, if the query fails
2092
	*/
2093
	function select($table,$cols,$where,$line,$file,$offset=False,$append='',$app=False,$num_rows=0,$join='',$table_def=False,$fetchmode=self::FETCH_ASSOC)
2094
	{
2095
		if ($this->Debug) echo "<p>db::select('$table',".print_r($cols,True).",".print_r($where,True).",$line,$file,$offset,'$app',$num_rows,'$join')</p>\n";
2096
2097
		if (!$table_def) $table_def = $this->get_table_definitions($app,$table);
2098
		if (is_array($cols))
2099
		{
2100
			$cols = implode(',',$cols);
2101
		}
2102
		if (is_array($where))
2103
		{
2104
			$where = $this->column_data_implode(' AND ',$where,True,False,$table_def['fd']);
2105
		}
2106
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
0 ignored issues
show
Bug Best Practice introduced by
The expression self::tablealiases 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...
2107
		{
2108
			$table = self::$tablealiases[$table];
2109
		}
2110
		$sql = "SELECT $cols FROM $table $join";
2111
2112
		// if we have a where clause, we need to add it together with the WHERE statement, if thats not in the join
2113
		if ($where) $sql .= (strpos($join,"WHERE")!==false) ? ' AND ('.$where.')' : ' WHERE '.$where;
2114
2115
		if ($append) $sql .= ' '.$append;
2116
2117
		if ($this->Debug) echo "<p>sql='$sql'</p>";
2118
2119
		if ($line === false && $file === false)	// call by union, to return the sql rather then run the query
0 ignored issues
show
introduced by
The condition $line === false is always false.
Loading history...
2120
		{
2121
			return $sql;
2122
		}
2123
		return $this->query($sql,$line,$file,$offset,$offset===False ? -1 : (int)$num_rows,false,$fetchmode);
2124
	}
2125
2126
	/**
2127
	* Does a union over multiple selects
2128
	*
2129
	* @author RalfBecker<at>outdoor-training.de
2130
	*
2131
	* @param array $selects array of selects, each select is an array with the possible keys/parameters: table, cols, where, append, app, join, table_def
2132
	*	For further info about parameters see the definition of the select function, beside table, cols and where all other params are optional
2133
	* @param int $line line-number to pass to query
2134
	* @param string $file file-name to pass to query
2135
	* @param string $order_by ORDER BY statement for the union
2136
	* @param int|bool $offset offset for a limited query or False (default)
2137
	* @param int $num_rows number of rows to return if offset set, default 0 = use default in user prefs
2138
	* @param int $fetchmode =self::FETCH_ASSOC self::FETCH_ASSOC (default), self::FETCH_BOTH or self::FETCH_NUM
2139
	* @return ADORecordSet or false, if the query fails
2140
	*/
2141
	function union($selects,$line,$file,$order_by='',$offset=false,$num_rows=0,$fetchmode=self::FETCH_ASSOC)
2142
	{
2143
		if ($this->Debug) echo "<p>db::union(".print_r($selects,True).",$line,$file,$order_by,$offset,$num_rows)</p>\n";
2144
2145
		$union = array();
2146
		foreach($selects as $select)
2147
		{
2148
			$union[] = call_user_func_array(array($this,'select'),array(
2149
				$select['table'],
2150
				$select['cols'],
2151
				$select['where'],
2152
				false,	// line
2153
				false,	// file
2154
				false,	// offset
2155
				$select['append'],
2156
				$select['app'],
2157
				0,		// num_rows,
2158
				$select['join'],
2159
				$select['table_def'],
2160
			));
2161
		}
2162
		$sql = count($union) > 1 ? '(' . implode(")\nUNION\n(",$union).')' : 'SELECT DISTINCT'.substr($union[0],6);
2163
2164
		if ($order_by) $sql .=  (!stristr($order_by,'ORDER BY') ? "\nORDER BY " : '').$order_by;
2165
2166
		if ($this->Debug) echo "<p>sql='$sql'</p>";
2167
2168
		return $this->query($sql,$line,$file,$offset,$offset===False ? -1 : (int)$num_rows,false,$fetchmode);
2169
	}
2170
2171
	/**
2172
	 * Strip eg. a prefix from the keys of an array
2173
	 *
2174
	 * @param array $arr
2175
	 * @param string|array $strip
2176
	 * @return array
2177
	 */
2178
	static function strip_array_keys($arr,$strip)
2179
	{
2180
		$keys = array_keys($arr);
2181
2182
		return array_walk($keys, function(&$v, $k, $strip)
2183
		{
2184
			unset($k);	// not used, but required by function signature
2185
			$v = str_replace($strip, '', $v);
2186
		}, $strip) ?
2187
			array_combine($keys,$arr) : $arr;
2188
	}
2189
}
2190