Completed
Push — 16.1 ( ffd2d1...0b384f )
by Ralf
36:16 queued 20:27
created

Db::query()   D

Complexity

Conditions 24
Paths 298

Size

Total Lines 62
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 35
nc 298
nop 8
dl 0
loc 62
rs 4.515
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-17 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api;
15
16
if(empty($GLOBALS['egw_info']['server']['db_type']))
17
{
18
	$GLOBALS['egw_info']['server']['db_type'] = 'mysql';
19
}
20
include_once(__DIR__.'/Db/ADOdb/adodb.inc.php');
21
22
/**
23
 * Database abstraction library
24
 *
25
 * This allows eGroupWare to use multiple database backends via ADOdb or in future with PDO
26
 *
27
 * You only need to clone the global database object $GLOBALS['egw']->db if:
28
 * - you access an application table (non phpgwapi) and you want to call set_app()
29
 *
30
 * Otherwise you can simply use $GLOBALS['egw']->db or a reference to it.
31
 *
32
 * a) foreach($db->query("SELECT * FROM $table",__LINE__,__FILE__) as $row)
33
 *
34
 * b) foreach($db->select($api_table,'*',$where,__LINE__,__FILE__) as $row)
35
 *
36
 * c) foreach($db->select($table,'*',$where,__LINE__,__FILE__,false,'',$app) as $row)
37
 *
38
 * To fetch only a single column (of the next row):
39
 *		$cnt = $db->query("SELECT COUNT(*) FROM ...")->fetchColumn($column_num=0);
40
 *
41
 * To fetch a next (single) row, you can use:
42
 *		$row = $db->query("SELECT COUNT(*) FROM ...")->fetch($fetchmod=null);
43
 *
44
 * Api\Db allows to use exceptions to catch sql-erros, not existing tables or failure to connect to the database, eg.:
45
 *		try {
46
 *			$this->db->connect();
47
 *			$num_config = $this->db->select(config::TABLE,'COUNT(config_name)',false,__LINE__,__FILE__)->fetchColumn();
48
 *		}
49
 *		catch(Exception $e) {
50
 *			echo "Connection to DB failed (".$e->getMessage().")!\n";
51
 *		}
52
 */
53
class Db
54
{
55
	/**
56
	 * Fetchmode to fetch only as associative array with $colname => $value pairs
57
	 *
58
	 * Use the FETCH_* constants to be compatible, if we replace ADOdb ...
59
	 */
60
	const FETCH_ASSOC = ADODB_FETCH_ASSOC;
61
	/**
62
	 * Fetchmode to fetch only as (numeric indexed) array: array($val1,$val2,...)
63
	 */
64
	const FETCH_NUM = ADODB_FETCH_NUM;
65
	/**
66
	 * Fetchmode to have both numeric and column-name indexes
67
	 */
68
	const FETCH_BOTH = ADODB_FETCH_BOTH;
69
	/**
70
	* @var string $type translated database type: mysqlt+mysqli ==> mysql, same for odbc-types
71
	*/
72
	var $Type     = '';
73
74
	/**
75
	* @var string $type database type as defined in the header.inc.php, eg. mysqlt
76
	*/
77
	var $setupType     = '';
78
79
	/**
80
	* @var string $Host database host to connect to
81
	*/
82
	var $Host     = '';
83
84
	/**
85
	* @var string $Port port number of database to connect to
86
	*/
87
	var $Port     = '';
88
89
	/**
90
	* @var string $Database name of database to use
91
	*/
92
	var $Database = '';
93
94
	/**
95
	* @var string $User name of database user
96
	*/
97
	var $User     = '';
98
99
	/**
100
	* @var string $Password password for database user
101
	*/
102
	var $Password = '';
103
104
	/**
105
	 * @var boolean $readonly only allow readonly access to database
106
	 */
107
	var $readonly = false;
108
109
	/**
110
	* @var int $Debug enable debuging - 0 no, 1 yes
111
	*/
112
	var $Debug         = 0;
113
114
	/**
115
	 * Log update querys to error_log
116
	 *
117
	 * @var boolean
118
	 */
119
	var $log_updates = false;
120
121
	/**
122
	* @var array $Record current record
123
	*/
124
	var $Record   = array();
125
126
	/**
127
	* @var int row number for current record
128
	*/
129
	var $Row;
130
131
	/**
132
	* @var int $Errno internal rdms error number for last error
133
	*/
134
	var $Errno    = 0;
135
136
	/**
137
	* @var string descriptive text from last error
138
	*/
139
	var $Error    = '';
140
141
	/**
142
	 * eGW's own query log, independent of the db-type, eg. /tmp/query.log
143
	 *
144
	 * @var string
145
	 */
146
	var $query_log;
147
148
	/**
149
	 * ADOdb connection
150
	 *
151
	 * @var ADOConnection
152
	 */
153
	var $Link_ID = 0;
154
	/**
155
	 * ADOdb connection
156
	 *
157
	 * @var ADOConnection
158
	 */
159
	var $privat_Link_ID = False;	// do we use a privat Link_ID or a reference to the global ADOdb object
160
161
	/**
162
	 * Can be used to transparently convert tablenames, eg. 'mytable' => 'otherdb.othertable'
163
	 *
164
	 * Can be set eg. at the *end* of header.inc.php.
165
	 * Only works with new Api\Db methods (select, insert, update, delete) not query!
166
	 *
167
	 * @var array
168
	 */
169
	static $tablealiases = array();
170
171
	/**
172
	 * Callback to check if selected node is healty / should be used
173
	 *
174
	 * @var callback throwing Db\Exception\Connection, if connected node should NOT be used
175
	 */
176
	static $health_check;
177
178
	/**
179
	 * db allows sub-queries, true for everything but mysql < 4.1
180
	 *
181
	 * use like: if ($db->capabilities[self::CAPABILITY_SUB_QUERIES]) ...
182
	 */
183
	const CAPABILITY_SUB_QUERIES = 'sub_queries';
184
	/**
185
	 * db allows union queries, true for everything but mysql < 4.0
186
	 */
187
	const CAPABILITY_UNION = 'union';
188
	/**
189
	 * db allows an outer join, will be set eg. for postgres
190
	 */
191
	const CAPABILITY_OUTER_JOIN = 'outer_join';
192
	/**
193
	 * db is able to use DISTINCT on text or blob columns
194
	 */
195
	const CAPABILITY_DISTINCT_ON_TEXT =	'distinct_on_text';
196
	/**
197
	 * DB is able to use LIKE on text columns
198
	 */
199
	const CAPABILITY_LIKE_ON_TEXT =	'like_on_text';
200
	/**
201
	 * DB allows ORDER on text columns
202
	 *
203
	 * boolean or string for sprintf for a cast (eg. 'CAST(%s AS varchar)
204
	 */
205
	const CAPABILITY_ORDER_ON_TEXT = 'order_on_text';
206
	/**
207
	 * case of returned column- and table-names: upper, lower(pgSql), preserv(MySQL)
208
	 */
209
	const CAPABILITY_NAME_CASE = 'name_case';
210
	/**
211
	 * does DB supports a changeable client-encoding
212
	 */
213
	const CAPABILITY_CLIENT_ENCODING = 'client_encoding';
214
	/**
215
	 * case insensitiv like statement (in $db->capabilities[self::CAPABILITY_CASE_INSENSITIV_LIKE]), default LIKE, ILIKE for postgres
216
	 */
217
	const CAPABILITY_CASE_INSENSITIV_LIKE = 'case_insensitive_like';
218
	/**
219
	 * DB requires varchar columns to be truncated to the max. size (eg. Postgres)
220
	 */
221
	const CAPABILITY_REQUIRE_TRUNCATE_VARCHAR = 'require_truncate_varchar';
222
	/**
223
	 * How to cast a column to varchar: CAST(%s AS varchar)
224
	 *
225
	 * MySQL requires to use CAST(%s AS char)!
226
	 *
227
	 * Use as: $sql = sprintf($GLOBALS['egw']->db->capabilities[self::CAPABILITY_CAST_AS_VARCHAR],$expression);
228
	 */
229
	const CAPABILITY_CAST_AS_VARCHAR = 'cast_as_varchar';
230
	/**
231
	 * default capabilities will be changed by method set_capabilities($ado_driver,$db_version)
232
	 *
233
	 * should be used with the CAPABILITY_* constants as key
234
	 *
235
	 * @var array
236
	 */
237
	var $capabilities = array(
238
		self::CAPABILITY_SUB_QUERIES      => true,
239
		self::CAPABILITY_UNION            => true,
240
		self::CAPABILITY_OUTER_JOIN       => false,
241
		self::CAPABILITY_DISTINCT_ON_TEXT => true,
242
		self::CAPABILITY_LIKE_ON_TEXT     => true,
243
		self::CAPABILITY_ORDER_ON_TEXT    => true,
244
		self::CAPABILITY_NAME_CASE        => 'upper',
245
		self::CAPABILITY_CLIENT_ENCODING  => false,
246
		self::CAPABILITY_CASE_INSENSITIV_LIKE => 'LIKE',
247
		self::CAPABILITY_REQUIRE_TRUNCATE_VARCHAR => true,
248
		self::CAPABILITY_CAST_AS_VARCHAR   => 'CAST(%s AS varchar)',
249
	);
250
251
	var $prepared_sql = array();	// sql is the index
252
253
	/**
254
	 * Constructor
255
	 *
256
	 * @param array $db_data =null values for keys 'db_name', 'db_host', 'db_port', 'db_user', 'db_pass', 'db_type', 'db_readonly'
257
	 */
258
	function __construct(array $db_data=null)
259
	{
260
		if (!is_null($db_data))
261
		{
262
			foreach(array(
263
				'Database' => 'db_name',
264
				'Host'     => 'db_host',
265
				'Port'     => 'db_port',
266
				'User'     => 'db_user',
267
				'Password' => 'db_pass',
268
				'Type'     => 'db_type',
269
				'readonly' => 'db_readonly',
270
			) as $var => $key)
271
			{
272
				$this->$var = $db_data[$key];
273
			}
274
		}
275
//if ($GLOBALS['egw_info']['server']['default_domain'] == 'ralfsmacbook.local') $this->query_log = '/tmp/query.log';
276
	}
277
278
	/**
279
	* @param string $query query to be executed (optional)
280
	*/
281
282
	function db($query = '')
283
	{
284
		$this->query($query);
285
	}
286
287
	/**
288
	* @return int current connection id
289
	*/
290
	function link_id()
291
	{
292
		return $this->Link_ID;
293
	}
294
295
	/**
296
	 * Open a connection to a database
297
	 *
298
	 * @param string $Database name of database to use (optional)
299
	 * @param string $Host database host to connect to (optional)
300
	 * @param string $Port database port to connect to (optional)
301
	 * @param string $User name of database user (optional)
302
	 * @param string $Password password for database user (optional)
303
	 * @param string $Type type of database (optional)
304
	 * @throws Db\Exception\Connection
305
	 * @return ADOConnection
306
	 */
307
	function connect($Database = NULL, $Host = NULL, $Port = NULL, $User = NULL, $Password = NULL, $Type = NULL)
308
	{
309
		/* Handle defaults */
310
		if (!is_null($Database) && $Database)
311
		{
312
			$this->Database = $Database;
313
		}
314
		if (!is_null($Host) && $Host)
315
		{
316
			$this->Host     = $Host;
317
		}
318
		if (!is_null($Port) && $Port)
319
		{
320
			$this->Port     = $Port;
321
		}
322
		if (!is_null($User) && $User)
323
		{
324
			$this->User     = $User;
325
		}
326
		if (!is_null($Password) && $Password)
327
		{
328
			$this->Password = $Password;
329
		}
330
		if (!is_null($Type) && $Type)
331
		{
332
			$this->Type = $Type;
333
		}
334
		elseif (!$this->Type)
335
		{
336
			$this->Type = $GLOBALS['egw_info']['server']['db_type'];
337
		}
338
		// on connection failure re-try with an other host
339
		// remembering in session which host we used last time
340
		$use_host_from_session = true;
341
		while(($host = $this->get_host(!$use_host_from_session)))
342
		{
343
			try {
344
				//error_log(__METHOD__."() this->Host(s)=$this->Host, n=$n --> host=$host");
345
				$new_connection = !$this->Link_ID || !$this->Link_ID->IsConnected();
346
				$this->_connect($host);
347
				// check if connected node is healty
348
				if ($new_connection && self::$health_check)
349
				{
350
					call_user_func(self::$health_check, $this);
351
				}
352
				//error_log(__METHOD__."() host=$host, new_connection=$new_connection, this->Type=$this->Type, this->Host=$this->Host, wsrep_local_state=".array2string($state));
353
				return $this->Link_ID;
354
			}
355
			catch(Db\Exception\Connection $e) {
356
				//_egw_log_exception($e);
357
				$this->disconnect();	// force a new connect
358
				$this->Type = $this->setupType;	// get set to "mysql" for "mysqli"
359
				$use_host_from_session = false;	// re-try with next host from list
360
			}
361
		}
362
		if (!isset($e))
363
		{
364
			$e = new Db\Exception\Connection('No DB host set!');
365
		}
366
		throw $e;
367
	}
368
369
	/**
370
	 * Check if just connected Galera cluster node is healthy / fully operational
371
	 *
372
	 * A node in state "Donor/Desynced" will block updates at the end of a SST.
373
	 * Therefore we try to avoid that node, if we have an alternative.
374
	 *
375
	 * To enable this check add the following to your header.inc.php:
376
	 *
377
	 * require_once(EGW_INCLUDE_ROOT.'/api/src/Db.php');
378
	 * EGroupware\Api\Db::$health_check = array('EGroupware\Api\Db', 'galera_cluster_health');
379
	 *
380
	 * @param Api\Db $db already connected Api\Db instance to check
381
	 * @throws Db\Exception\Connection if node should NOT be used
382
	 */
383
	static function galera_cluster_health(Db $db)
384
	{
385
		if (($state = $db->query("SHOW STATUS WHERE Variable_name in ('wsrep_cluster_size','wsrep_local_state','wsrep_local_state_comment')",
386
			// GetAssoc in ADOdb 5.20 does not work with our default self::FETCH_BOTH
387
			__LINE__, __FILE__, 0, -1, false, self::FETCH_ASSOC)->GetAssoc()))
388
		{
389
			if ($state['wsrep_local_state_comment'] == 'Synced' ||
390
				// if we have only 2 nodes (2. one starting), we can only use the donor
391
				$state['wsrep_local_state_comment'] == 'Donor/Desynced' &&
392
					$state['wsrep_cluster_size'] == 2) return;
393
394
			throw new Db\Exception\Connection('Node is NOT Synced! '.array2string($state));
395
		}
396
	}
397
398
	/**
399
	 * Get one of multiple (semicolon-separated) DB-hosts to use
400
	 *
401
	 * Which host to use is cached in session, default is first one.
402
	 *
403
	 * @param boolean $next =false	true: move to next host
404
	 * @return boolean|string hostname or false, if already number-of-hosts plus 2 times called with $next == true
405
	 */
406
	public function get_host($next = false)
407
	{
408
		$hosts = explode(';', $this->Host[0] == '@' ? getenv(substr($this->Host, 1)) : $this->Host);
409
		$num_hosts = count($hosts);
410
		$n =& Cache::getSession(__CLASS__, $this->Host);
411
		if (!isset($n)) $n = 0;
412
413
		if ($next && ++$n >= $num_hosts+2)
414
		{
415
			$n = 0;	// start search again with default on next request
416
			$ret = false;
417
		}
418
		else
419
		{
420
			$ret = $hosts[$n % $num_hosts];
421
		}
422
		//error_log(__METHOD__."(next=".array2string($next).") n=$n returning ".array2string($ret));
423
		return $ret;
424
	}
425
426
	/**
427
	 * Connect to given host
428
	 *
429
	 * @param string $Host host to connect to
430
	 * @return ADOConnection
431
	 * @throws Db\Exception\Connection
432
	 */
433
	protected function _connect($Host)
434
	{
435
		if (!$this->Link_ID)
436
		{
437
			$Database = $User = $Password = $Port = $Type = '';
438
			foreach(array('Database','User','Password','Port','Type') as $name)
439
			{
440
				$$name = $this->$name;
441
				if (${$name}[0] == '@' && $name != 'Password') $$name = getenv(substr($$name, 1));
442
			}
443
			$this->setupType = $php_extension = $Type;
444
445
			switch($Type)	// convert to ADO db-type-names
446
			{
447
				case 'pgsql':
448
					$Type = 'postgres'; // name in ADOdb
449
					// create our own pgsql connection-string, to allow unix domain soccets if !$Host
450
					$Host = "dbname=$Database".($Host ? " host=$Host".($Port ? " port=$Port" : '') : '').
451
						" user=$User".($Password ? " password='".addslashes($Password)."'" : '');
452
					$User = $Password = $Database = '';	// to indicate $Host is a connection-string
453
					break;
454
455
				case 'odbc_mssql':
456
					$php_extension = 'odbc';
457
					$Type = 'mssql';
458
					// fall through
459
				case 'mssql':
460
					if ($Port) $Host .= ','.$Port;
461
					break;
462
463
				case 'odbc_oracle':
464
					$php_extension = 'odbc';
465
					$Type = 'oracle';
466
					break;
467
				case 'oracle':
468
					$php_extension = $Type = 'oci8';
469
					break;
470
471
				case 'sapdb':
472
					$Type = 'maxdb';
473
					// fall through
474
				case 'maxdb':
475
					$Type ='sapdb';	// name in ADOdb
476
					$php_extension = 'odbc';
477
					break;
478
479
				case 'mysqlt':
480
				case 'mysql':
481
					// if mysqli is available silently switch to it, mysql extension is deprecated and no longer available in php7+
482
					if (check_load_extension('mysqli'))
483
					{
484
						$php_extension = $Type = 'mysqli';
485
					}
486
					else
487
					{
488
						$php_extension = 'mysql';	// you can use $this->setupType to determine if it's mysqlt or mysql
489
					}
490
					// fall through
491
				case 'mysqli':
492
					$this->Type = 'mysql';		// need to be "mysql", so apps can check just for "mysql"!
493
					// fall through
494
				default:
495
					if ($Port) $Host .= ':'.$Port;
496
					break;
497
			}
498
			if (!isset($GLOBALS['egw']->ADOdb) ||	// we have no connection so far
499
				(is_object($GLOBALS['egw']->db) &&	// we connect to a different db, then the global one
500
					($this->Type != $GLOBALS['egw']->db->Type ||
501
					$this->Database != $GLOBALS['egw']->db->Database ||
502
					$this->User != $GLOBALS['egw']->db->User ||
503
					$this->Host != $GLOBALS['egw']->db->Host ||
504
					$this->Port != $GLOBALS['egw']->db->Port)))
505
			{
506
				if (!check_load_extension($php_extension))
507
				{
508
					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 !!!");
509
				}
510
				if (!isset($GLOBALS['egw']->ADOdb))	// use the global object to store the connection
511
				{
512
					$this->Link_ID =& $GLOBALS['egw']->ADOdb;
513
				}
514
				else
515
				{
516
					$this->privat_Link_ID = True;	// remember that we use a privat Link_ID for disconnect
517
				}
518
				$this->Link_ID = ADONewConnection($Type);
519
				if (!$this->Link_ID)
520
				{
521
					throw new Db\Exception\Connection("No ADOdb support for '$Type' ($this->Type) !!!");
522
				}
523
				if ($Type == 'mysqli')
524
				{
525
					// set a connection timeout of 1 second, to allow quicker failover to other db-nodes (default is 20s)
526
					$this->Link_ID->setConnectionParameter(MYSQLI_OPT_CONNECT_TIMEOUT, 1);
527
				}
528
				$connect = $GLOBALS['egw_info']['server']['db_persistent'] ? 'PConnect' : 'Connect';
529
				if (($Ok = $this->Link_ID->$connect($Host, $User, $Password, $Database)))
530
				{
531
					$this->ServerInfo = $this->Link_ID->ServerInfo();
532
					$this->set_capabilities($Type,$this->ServerInfo['version']);
533
534
					// switch off MySQL 5.7+ ONLY_FULL_GROUP_BY sql_mode
535
					if (substr($this->Type, 0, 5) == 'mysql' && $this->ServerInfo['version'] >= 5.7 && $this->ServerInfo['version'] < 10.0)
536
					{
537
						$this->query("SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''))", __LINE__, __FILE__);
538
					}
539
				}
540
				if (!$Ok)
541
				{
542
					$Host = preg_replace('/password=[^ ]+/','password=$Password',$Host);	// eg. postgres dsn contains password
543
					throw new Db\Exception\Connection("ADOdb::$connect($Host, $User, \$Password, $Database) failed.");
544
				}
545
				if ($this->Debug)
546
				{
547
					echo function_backtrace();
548
					echo "<p>new ADOdb connection to $Type://$Host/$Database: Link_ID".($this->Link_ID === $GLOBALS['egw']->ADOdb ? '===' : '!==')."\$GLOBALS[egw]->ADOdb</p>";
549
					//echo "<p>".print_r($this->Link_ID->ServerInfo(),true)."</p>\n";
550
					_debug_array($this);
551
					echo "\$GLOBALS[egw]->db="; _debug_array($GLOBALS[egw]->db);
552
				}
553
				if ($Type == 'mssql')
554
				{
555
					// this is the format ADOdb expects
556
					$this->Link_ID->Execute('SET DATEFORMAT ymd');
557
					// sets the limit to the maximum
558
					ini_set('mssql.textlimit',2147483647);
559
					ini_set('mssql.sizelimit',2147483647);
560
				}
561
				// set our default charset
562
				$this->Link_ID->SetCharSet($this->Type == 'mysql' ? 'utf8' : 'utf-8');
563
564
				$new_connection = true;
565
			}
566
			else
567
			{
568
				$this->Link_ID =& $GLOBALS['egw']->ADOdb;
569
			}
570
		}
571
		if (!$this->Link_ID->isConnected() && !$this->Link_ID->Connect())
572
		{
573
			$Host = preg_replace('/password=[^ ]+/','password=$Password',$Host);	// eg. postgres dsn contains password
574
			throw new Db\Exception\Connection("ADOdb::$connect($Host, $User, \$Password, $Database) reconnect failed.");
575
		}
576
		// fix due to caching and reusing of connection not correctly set $this->Type == 'mysql'
577
		if ($this->Type == 'mysqli')
578
		{
579
			$this->setupType = $this->Type;
580
			$this->Type = 'mysql';
581
		}
582
		if ($new_connection)
583
		{
584
			foreach(get_included_files() as $file)
585
			{
586
				if (strpos($file,'adodb') !== false && !in_array($file,(array)$_SESSION['egw_required_files']))
587
				{
588
					$_SESSION['egw_required_files'][] = $file;
589
					//error_log(__METHOD__."() egw_required_files[] = $file");
590
				}
591
			}
592
		}
593
		//echo "<p>".print_r($this->Link_ID->ServerInfo(),true)."</p>\n";
594
		return $this->Link_ID;
595
	}
596
597
	/**
598
	 * Magic method to re-connect with the database, if the object get's restored from the session
599
	 */
600
	function __wakeup()
601
	{
602
		$this->connect();	// we need to re-connect
603
	}
604
605
	/**
606
	 * Magic method called when object get's serialized
607
	 *
608
	 * We do NOT store Link_ID and private_Link_ID, as we need to reconnect anyway.
609
	 * This also ensures reevaluating environment-data or multiple hosts in connection-data!
610
	 *
611
	 * @return array
612
	 */
613
	function __sleep()
614
	{
615
		if (!empty($this->setupType)) $this->Type = $this->setupType;	// restore Type eg. to mysqli
616
617
		$vars = get_object_vars($this);
618
		unset($vars['Link_ID'], $vars['Query_ID'], $vars['privat_Link_ID']);
619
		return array_keys($vars);
620
	}
621
622
	/**
623
	 * changes defaults set in class-var $capabilities depending on db-type and -version
624
	 *
625
	 * @param string $adodb_driver mysql, postgres, mssql, sapdb, oci8
626
	 * @param string $db_version version-number of connected db-server, as reported by ServerInfo
627
	 */
628
	function set_capabilities($adodb_driver,$db_version)
629
	{
630
		switch($adodb_driver)
631
		{
632
			case 'mysql':
633
			case 'mysqlt':
634
			case 'mysqli':
635
				$this->capabilities[self::CAPABILITY_SUB_QUERIES] = (float) $db_version >= 4.1;
636
				$this->capabilities[self::CAPABILITY_UNION] = (float) $db_version >= 4.0;
637
				$this->capabilities[self::CAPABILITY_NAME_CASE] = 'preserv';
638
				$this->capabilities[self::CAPABILITY_CLIENT_ENCODING] = (float) $db_version >= 4.1;
639
				$this->capabilities[self::CAPABILITY_CAST_AS_VARCHAR] = 'CAST(%s AS char)';
640
				break;
641
642
			case 'postgres':
643
				$this->capabilities[self::CAPABILITY_NAME_CASE] = 'lower';
644
				$this->capabilities[self::CAPABILITY_CLIENT_ENCODING] = (float) $db_version >= 7.4;
645
				$this->capabilities[self::CAPABILITY_OUTER_JOIN] = true;
646
				$this->capabilities[self::CAPABILITY_CASE_INSENSITIV_LIKE] = '::text ILIKE';
647
				$this->capabilities[self::CAPABILITY_REQUIRE_TRUNCATE_VARCHAR] = true;
648
				break;
649
650
			case 'mssql':
651
				$this->capabilities[self::CAPABILITY_DISTINCT_ON_TEXT] = false;
652
				$this->capabilities[self::CAPABILITY_ORDER_ON_TEXT] = 'CAST (%s AS varchar)';
653
				break;
654
655
			case 'maxdb':	// if Lim ever changes it to maxdb ;-)
656
			case 'sapdb':
657
				$this->capabilities[self::CAPABILITY_DISTINCT_ON_TEXT] = false;
658
				$this->capabilities[self::CAPABILITY_LIKE_ON_TEXT] = $db_version >= 7.6;
659
				$this->capabilities[self::CAPABILITY_ORDER_ON_TEXT] = false;
660
				break;
661
		}
662
		//echo "db::set_capabilities('$adodb_driver',$db_version)"; _debug_array($this->capabilities);
663
	}
664
665
	/**
666
	* Close a connection to a database
667
	*/
668
	function disconnect()
669
	{
670
		if (!$this->privat_Link_ID)
671
		{
672
			unset($GLOBALS['egw']->ADOdb);
673
		}
674
		unset($this->Link_ID);
675
		$this->Link_ID = 0;
676
677
		if (!empty($this->setupType)) $this->Type = $this->setupType;
678
	}
679
680
	/**
681
	* Convert a unix timestamp to a rdms specific timestamp
682
	*
683
	* @param int unix timestamp
684
	* @return string rdms specific timestamp
685
	*/
686
	function to_timestamp($epoch)
687
	{
688
		if (!$this->Link_ID && !$this->connect())
689
		{
690
			return False;
691
		}
692
		// the substring is needed as the string is already in quotes
693
		return substr($this->Link_ID->DBTimeStamp($epoch),1,-1);
694
	}
695
696
	/**
697
	* Convert a rdms specific timestamp to a unix timestamp
698
	*
699
	* @param string rdms specific timestamp
700
	* @return int unix timestamp
701
	*/
702
	function from_timestamp($timestamp)
703
	{
704
		if (!$this->Link_ID && !$this->connect())
705
		{
706
			return False;
707
		}
708
		return $this->Link_ID->UnixTimeStamp($timestamp);
709
	}
710
711
	/**
712
	 * convert a rdbms specific boolean value
713
	 *
714
	 * @param string $val boolean value in db-specfic notation
715
	 * @return boolean
716
	 */
717
	public static function from_bool($val)
718
	{
719
		return $val && $val[0] !== 'f';	// everthing other then 0 or f[alse] is returned as true
720
	}
721
722
	/**
723
	* Execute a query
724
	*
725
	* @param string $Query_String the query to be executed
726
	* @param int $line the line method was called from - use __LINE__
727
	* @param string $file the file method was called from - use __FILE__
728
	* @param int $offset row to start from, default 0
729
	* @param int $num_rows number of rows to return (optional), default -1 = all, 0 will use $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs']
730
	* @param array|boolean $inputarr array for binding variables to parameters or false (default)
731
	* @param int $fetchmode =self::FETCH_BOTH self::FETCH_BOTH (default), self::FETCH_ASSOC or self::FETCH_NUM
732
	* @param boolean $reconnect =true true: try reconnecting if server closes connection, false: dont (mysql only!)
733
	* @return ADORecordSet or false, if the query fails
734
	* @throws Db\Exception\InvalidSql with $this->Link_ID->ErrorNo() as code
735
	*/
736
	function query($Query_String, $line = '', $file = '', $offset=0, $num_rows=-1, $inputarr=false, $fetchmode=self::FETCH_BOTH, $reconnect=true)
737
	{
738
		unset($line, $file);	// not used anymore
739
740
		if ($Query_String == '')
741
		{
742
			return 0;
743
		}
744
		if (!$this->Link_ID && !$this->connect())
745
		{
746
			return False;
747
		}
748
749
		if ($this->Link_ID->fetchMode != $fetchmode)
750
		{
751
			$this->Link_ID->SetFetchMode($fetchmode);
752
		}
753
		if (!$num_rows)
754
		{
755
			$num_rows = $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs'];
756
		}
757
		if (($this->readonly || $this->log_updates) && !preg_match('/^\(?(SELECT|SET|SHOW)/i', $Query_String))
758
		{
759
			if ($this->log_updates) error_log($Query_String.': '.function_backtrace());
760
			if ($this->readonly) return 0;
761
		}
762
		if ($num_rows > 0)
763
		{
764
			$rs = $this->Link_ID->SelectLimit($Query_String,$num_rows,(int)$offset,$inputarr);
765
		}
766
		else
767
		{
768
			$rs = $this->Link_ID->Execute($Query_String,$inputarr);
769
		}
770
		$this->Row = 0;
771
		$this->Errno  = $this->Link_ID->ErrorNo();
772
		$this->Error  = $this->Link_ID->ErrorMsg();
773
774
		if ($this->query_log && ($f = @fopen($this->query_log,'a+')))
775
		{
776
			fwrite($f,'['.(isset($GLOBALS['egw_setup']) ? $GLOBALS['egw_setup']->ConfigDomain : $GLOBALS['egw_info']['user']['domain']).'] ');
777
			fwrite($f,date('Y-m-d H:i:s ').$Query_String.($inputarr ? "\n".print_r($inputarr,true) : '')."\n");
778
			if (!$rs)
779
			{
780
				fwrite($f,"*** Error $this->Errno: $this->Error\n".function_backtrace()."\n");
781
			}
782
			fclose($f);
783
		}
784
		if (!$rs)
785
		{
786
			if ($reconnect && $this->Type == 'mysql' && $this->Errno == 2006)	// Server has gone away
787
			{
788
				$this->disconnect();
789
				return $this->query($Query_String, $line, $file, $offset, $num_rows, $inputarr, $fetchmode, false);
790
			}
791
			throw new Db\Exception\InvalidSql("Invalid SQL: ".(is_array($Query_String)?$Query_String[0]:$Query_String).
792
				"\n$this->Error ($this->Errno)".
793
				($inputarr ? "\nParameters: '".implode("','",$inputarr)."'":''), $this->Errno);
794
		}
795
		elseif(empty($rs->sql)) $rs->sql = $Query_String;
796
		return $rs;
797
	}
798
799
	/**
800
	* Execute a query with limited result set
801
	*
802
	* @param string $Query_String the query to be executed
803
	* @param int $offset row to start from, default 0
804
	* @param int $line the line method was called from - use __LINE__
805
	* @param string $file the file method was called from - use __FILE__
806
	* @param int $num_rows number of rows to return (optional), default -1 = all, 0 will use $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs']
807
	* @param array|boolean $inputarr array for binding variables to parameters or false (default)
808
	* @return ADORecordSet or false, if the query fails
809
	*/
810
	function limit_query($Query_String, $offset, $line = '', $file = '', $num_rows = '',$inputarr=false)
811
	{
812
		return $this->query($Query_String,$line,$file,$offset,$num_rows,$inputarr);
813
	}
814
815
	/**
816
	* Begin Transaction
817
	*
818
	* @return int/boolean current transaction-id, of false if no connection
819
	*/
820
	function transaction_begin()
821
	{
822
		if (!$this->Link_ID && !$this->connect())
823
		{
824
			return False;
825
		}
826
		//return $this->Link_ID->BeginTrans();
827
		return $this->Link_ID->StartTrans();
828
	}
829
830
	/**
831
	* Complete the transaction
832
	*
833
	* @return bool True if sucessful, False if fails
834
	*/
835
	function transaction_commit()
836
	{
837
		if (!$this->Link_ID && !$this->connect())
838
		{
839
			return False;
840
		}
841
		//return $this->Link_ID->CommitTrans();
842
		return $this->Link_ID->CompleteTrans();
843
	}
844
845
	/**
846
	* Rollback the current transaction
847
	*
848
	* @return bool True if sucessful, False if fails
849
	*/
850
	function transaction_abort()
851
	{
852
		if (!$this->Link_ID && !$this->connect())
853
		{
854
			return False;
855
		}
856
		//return $this->Link_ID->RollbackTrans();
857
		return $this->Link_ID->FailTrans();
858
	}
859
860
	/**
861
	 * Lock a rows in table
862
	 *
863
	 * Will escalate and lock the table if row locking not supported.
864
	 * Will normally free the lock at the end of the transaction.
865
	 *
866
	 * @param string $table name of table to lock
867
	 * @param string $where ='true' where clause to use, eg: "WHERE row=12". Defaults to lock whole table.
868
	 * @param string $col ='1 as adodbignore'
869
	 */
870 View Code Duplication
	function row_lock($table, $where='true', $col='1 as adodbignore')
871
	{
872
		if (!$this->Link_ID && !$this->connect())
873
		{
874
			return False;
875
		}
876
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
877
		{
878
			$table = self::$tablealiases[$table];
879
		}
880
881
		return $this->Link_ID->RowLock($table, $where, $col);
882
	}
883
884
	/**
885
	 * Commit changed rows in table
886
	 *
887
	 * @param string $table
888
	 * @return boolean
889
	 */
890 View Code Duplication
	function commit_lock($table)
891
	{
892
		if (!$this->Link_ID && !$this->connect())
893
		{
894
			return False;
895
		}
896
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
897
		{
898
			$table = self::$tablealiases[$table];
899
		}
900
901
		return $this->Link_ID->CommitLock($table);
902
	}
903
904
	/**
905
	 * Unlock rows in table
906
	 *
907
	 * @param string $table
908
	 * @return boolean
909
	 */
910 View Code Duplication
	function rollback_lock($table)
911
	{
912
		if (!$this->Link_ID && !$this->connect())
913
		{
914
			return False;
915
		}
916
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
917
		{
918
			$table = self::$tablealiases[$table];
919
		}
920
921
		return $this->Link_ID->RollbackLock($table);
922
	}
923
924
	/**
925
	* Find the primary key of the last insertion on the current db connection
926
	*
927
	* @param string $table name of table the insert was performed on
928
	* @param string $field the autoincrement primary key of the table
929
	* @return int the id, -1 if fails
930
	*/
931
	function get_last_insert_id($table, $field)
932
	{
933
		if (!$this->Link_ID && !$this->connect())
934
		{
935
			return False;
936
		}
937
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
938
		{
939
			$table = self::$tablealiases[$table];
940
		}
941
		$id = $this->Link_ID->PO_Insert_ID($table,$field);	// simulates Insert_ID with "SELECT MAX($field) FROM $table" if not native availible
942
943
		if ($id === False)	// function not supported
944
		{
945
			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";
946
			echo '<p>'.function_backtrace()."</p>\n";
947
			return -1;
948
		}
949
		return $id;
950
	}
951
952
	/**
953
	* Get the number of rows affected by last update or delete
954
	*
955
	* @return int number of rows
956
	*/
957
	function affected_rows()
958
	{
959
		if ($this->log_updates) return 0;
960
961
		if (!$this->Link_ID && !$this->connect())
962
		{
963
			return False;
964
		}
965
		return $this->Link_ID->Affected_Rows();
966
	}
967
968
	/**
969
	* Get description of a table
970
	*
971
	* Beside the column-name all other data depends on the db-type !!!
972
	*
973
	* @param string $table name of table to describe
974
	* @param bool $full optional, default False summary information, True full information
975
	* @return array table meta data
976
	*/
977
	function metadata($table='',$full=false)
978
	{
979
		if (!$this->Link_ID && !$this->connect())
980
		{
981
			return False;
982
		}
983
		$columns = $this->Link_ID->MetaColumns($table);
984
		//$columns = $this->Link_ID->MetaColumnsSQL($table);
985
		//echo "<b>metadata</b>('$table')=<pre>\n".print_r($columns,True)."</pre>\n";
986
987
		$metadata = array();
988
		$i = 0;
989
		foreach($columns as $column)
990
		{
991
			// for backwards compatibilty (depreciated)
992
			$flags = null;
993
			if($column->auto_increment) $flags .= "auto_increment ";
994
			if($column->primary_key) $flags .= "primary_key ";
995
			if($column->binary) $flags .= "binary ";
996
997
			$metadata[$i] = array(
998
				'table' => $table,
999
				'name'  => $column->name,
1000
				'type'  => $column->type,
1001
				'len'   => $column->max_length,
1002
				'flags' => $flags, // for backwards compatibilty (depreciated) used by JiNN atm
1003
				'not_null' => $column->not_null,
1004
				'auto_increment' => $column->auto_increment,
1005
				'primary_key' => $column->primary_key,
1006
				'binary' => $column->binary,
1007
				'has_default' => $column->has_default,
1008
				'default'  => $column->default_value,
1009
			);
1010
			$metadata[$i]['table'] = $table;
1011
			if ($full)
1012
			{
1013
				$metadata['meta'][$column->name] = $i;
1014
			}
1015
			++$i;
1016
		}
1017
		if ($full)
1018
		{
1019
			$metadata['num_fields'] = $i;
1020
		}
1021
		return $metadata;
1022
	}
1023
1024
	/**
1025
	 * Get a list of table names in the current database
1026
	 *
1027
	 * @param boolean $just_name =false true return array of table-names, false return old format
1028
	 * @return array list of the tables
1029
	 */
1030
	function table_names($just_name=false)
1031
	{
1032
		if (!$this->Link_ID) $this->connect();
1033
		if (!$this->Link_ID)
1034
		{
1035
			return False;
1036
		}
1037
		$result = array();
1038
		$tables = $this->Link_ID->MetaTables('TABLES');
1039
		if (is_array($tables))
1040
		{
1041
			foreach($tables as $table)
1042
			{
1043
				if ($this->capabilities[self::CAPABILITY_NAME_CASE] == 'upper')
1044
				{
1045
					$table = strtolower($table);
1046
				}
1047
				$result[] = $just_name ? $table : array(
1048
					'table_name'      => $table,
1049
					'tablespace_name' => $this->Database,
1050
					'database'        => $this->Database
1051
				);
1052
			}
1053
		}
1054
		return $result;
1055
	}
1056
1057
	/**
1058
	* Return a list of indexes in current database
1059
	*
1060
	* @return array list of indexes
1061
	*/
1062
	function index_names()
1063
	{
1064
		$indices = array();
1065
		if ($this->Type != 'pgsql')
1066
		{
1067
			echo "<p>db::index_names() not yet implemented for db-type '$this->Type'</p>\n";
1068
			return $indices;
1069
		}
1070
		foreach($this->query("SELECT relname FROM pg_class WHERE NOT relname ~ 'pg_.*' AND relkind ='i' ORDER BY relname") as $row)
1071
		{
1072
			$indices[] = array(
1073
				'index_name'      => $row[0],
1074
				'tablespace_name' => $this->Database,
1075
				'database'        => $this->Database,
1076
			);
1077
		}
1078
		return $indices;
1079
	}
1080
1081
	/**
1082
	* Returns an array containing column names that are the primary keys of $tablename.
1083
	*
1084
	* @return array of columns
1085
	*/
1086
	function pkey_columns($tablename)
1087
	{
1088
		if (!$this->Link_ID && !$this->connect())
1089
		{
1090
			return False;
1091
		}
1092
		return $this->Link_ID->MetaPrimaryKeys($tablename);
1093
	}
1094
1095
	/**
1096
	* Create a new database
1097
	*
1098
	* @param string $adminname name of database administrator user (optional)
1099
	* @param string $adminpasswd password for the database administrator user (optional)
1100
	* @param string $charset default charset for the database
1101
	* @param string $grant_host ='localhost' host/ip of the webserver
1102
	*/
1103
	function create_database($adminname = '', $adminpasswd = '', $charset='', $grant_host='localhost')
1104
	{
1105
		$currentUser = $this->User;
1106
		$currentPassword = $this->Password;
1107
		$currentDatabase = $this->Database;
1108
1109
		if ($adminname != '')
1110
		{
1111
			$this->User = $adminname;
1112
			$this->Password = $adminpasswd;
1113
			$this->Database = $this->Type == 'pgsql' ? 'template1' : 'mysql';
1114
		}
1115
		$this->disconnect();
1116
1117
		$sqls = array();
1118
		switch ($this->Type)
1119
		{
1120
			case 'pgsql':
1121
				$sqls[] = "CREATE DATABASE $currentDatabase";
1122
				break;
1123
			case 'mysql':
1124
			case 'mysqli':
1125
			case 'mysqlt':
1126
				$create = "CREATE DATABASE `$currentDatabase`";
1127
				if ($charset && isset($this->Link_ID->charset2mysql[$charset]) && (float) $this->ServerInfo['version'] >= 4.1)
1128
				{
1129
					$create .= ' DEFAULT CHARACTER SET '.$this->Link_ID->charset2mysql[$charset].';';
1130
				}
1131
				$sqls[] = $create;
1132
				$sqls[] = "GRANT ALL ON `$currentDatabase`.* TO $currentUser@'$grant_host' IDENTIFIED BY ".$this->quote($currentPassword);
1133
				break;
1134
			default:
1135
				throw new Exception\WrongParameter(__METHOD__."(user=$adminname, \$pw) not yet implemented for DB-type '$this->Type'");
1136
		}
1137
		//error_log(__METHOD__."() this->Type=$this->Type: sqls=".array2string($sqls));
1138
		foreach($sqls as $sql)
1139
		{
1140
			$this->query($sql,__LINE__,__FILE__);
1141
		}
1142
		$this->disconnect();
1143
1144
		$this->User = $currentUser;
1145
		$this->Password = $currentPassword;
1146
		$this->Database = $currentDatabase;
1147
		$this->connect();
1148
	}
1149
1150
	/**
1151
	 * concat a variable number of strings together, to be used in a query
1152
	 *
1153
	 * Example: $db->concat($db->quote('Hallo '),'username') would return
1154
	 *	for mysql "concat('Hallo ',username)" or "'Hallo ' || username" for postgres
1155
	 * @param string $str1 already quoted stringliteral or column-name, variable number of arguments
1156
	 * @return string to be used in a query
1157
	 */
1158
	function concat(/*$str1, ...*/)
1159
	{
1160
		$args = func_get_args();
1161
1162
		if (!$this->Link_ID && !$this->connect())
1163
		{
1164
			return False;
1165
		}
1166
		return call_user_func_array(array(&$this->Link_ID,'concat'),$args);
1167
	}
1168
1169
	/**
1170
	 * Concat grouped values of an expression with optional order and separator
1171
	 *
1172
	 * @param string $expr column-name or expression optional prefixed with "DISTINCT"
1173
	 * @param string $order_by ='' optional order
1174
	 * @param string $separator =',' optional separator, default is comma
1175
	 * @return string|boolean false if not supported by dbms
1176
	 */
1177
	function group_concat($expr, $order_by='', $separator=',')
1178
	{
1179
		switch($this->Type)
1180
		{
1181
			case 'mysql':
1182
				$sql = 'GROUP_CONCAT('.$expr;
1183
				if ($order_by) $sql .= ' ORDER BY '.$order_by;
1184
				if ($separator != ',') $sql .= ' SEPARATOR '.$this->quote($separator);
1185
				$sql .= ')';
1186
				break;
1187
1188
			case 'pgsql':	// requires for Postgresql < 8.4 to have a custom ARRAY_AGG method installed!
1189
				if ($this->Type == 'pgsql' && $this->ServerInfo['version'] < 8.4)
1190
				{
1191
					return false;
1192
				}
1193
				$sql = 'ARRAY_TO_STRING(ARRAY_AGG('.$expr;
1194
				if ($order_by) $sql .= ' ORDER BY '.$order_by;
1195
				$sql .= '), '.$this->quote($separator).')';
1196
				break;
1197
1198
			default:	// probably gives an sql error anyway
1199
				return false;
1200
		}
1201
		return $sql;
1202
	}
1203
1204
	/**
1205
	 * SQL returning character (not byte!) positions for $substr in $str
1206
	 *
1207
	 * @param string $str
1208
	 * @param string $substr
1209
	 * @return string SQL returning character (not byte!) positions for $substr in $str
1210
	 */
1211
	function strpos($str, $substr)
1212
	{
1213 View Code Duplication
		switch($this->Type)
1214
		{
1215
			case 'mysql':
1216
				return "LOCATE($substr,$str)";
1217
			case 'pgsql':
1218
				return "STRPOS($str,$substr)";
1219
			case 'mssql':
1220
				return "CHARINDEX($substr,$str)";
1221
		}
1222
		die(__METHOD__." not implemented for DB type '$this->Type'!");
1223
	}
1224
1225
	/**
1226
	 * Convert a DB specific timestamp in a unix timestamp stored as integer, like MySQL: UNIX_TIMESTAMP(ts)
1227
	 *
1228
	 * @param string $expr name of an integer column or integer expression
1229
	 * @return string SQL expression of type timestamp
1230
	 */
1231
	function unix_timestamp($expr)
1232
	{
1233 View Code Duplication
		switch($this->Type)
1234
		{
1235
			case 'mysql':
1236
				return "UNIX_TIMESTAMP($expr)";
1237
1238
			case 'pgsql':
1239
				return "EXTRACT(EPOCH FROM CAST($expr AS TIMESTAMP))";
1240
1241
			case 'mssql':
1242
				return "DATEDIFF(second,'1970-01-01',($expr))";
1243
		}
1244
	}
1245
1246
	/**
1247
	 * Convert a unix timestamp stored as integer in the db into a db timestamp, like MySQL: FROM_UNIXTIME(ts)
1248
	 *
1249
	 * @param string $expr name of an integer column or integer expression
1250
	 * @return string SQL expression of type timestamp
1251
	 */
1252
	function from_unixtime($expr)
1253
	{
1254
		switch($this->Type)
1255
		{
1256
			case 'mysql':
1257
				return "FROM_UNIXTIME($expr)";
1258
1259
			case 'pgsql':
1260
				return "(TIMESTAMP WITH TIME ZONE 'epoch' + ($expr) * INTERVAL '1 sec')";
1261
1262
			case 'mssql':	// we use date(,0) as we store server-time
1263
				return "DATEADD(second,($expr),'".date('Y-m-d H:i:s',0)."')";
1264
		}
1265
		return false;
1266
	}
1267
1268
	/**
1269
	 * format a timestamp as string, like MySQL: DATE_FORMAT(ts)
1270
	 *
1271
	 * Please note: only a subset of the MySQL formats are implemented
1272
	 *
1273
	 * @param string $expr name of a timestamp column or timestamp expression
1274
	 * @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
1275
	 * @return string SQL expression of type timestamp
1276
	 */
1277
	function date_format($expr,$format)
1278
	{
1279
		switch($this->Type)
1280
		{
1281
			case 'mysql':
1282
				return "DATE_FORMAT($expr,'$format')";
1283
1284
			case 'pgsql':
1285
				$format = str_replace(
1286
					array('%Y',  '%y','%m','%d','%H',  '%h','%i','%s','%V','%v','%X',  '%x'),
1287
					array('YYYY','YY','MM','DD','HH24','HH','MI','SS','IW','IW','YYYY','YYYY'),
1288
					$format);
1289
				return "TO_CHAR($expr,'$format')";
1290
1291
			case 'mssql':
1292
				$from = $to = array();
1293
				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)
1294
				{
1295
					$from[] = $f;
1296
					$to[] = "'+DATEPART($t,($expr))+'";
1297
				}
1298
				$from[] = "''+"; $to[] = '';
1299
				$from[] = "+''"; $to[] = '';
1300
				return str_replace($from,$to,$format);
1301
		}
1302
		return false;
1303
	}
1304
1305
	/**
1306
	 * Cast a column or sql expression to integer, necessary at least for postgreSQL or MySQL for sorting
1307
	 *
1308
	 * @param string $expr
1309
	 * @return string
1310
	 */
1311
	function to_double($expr)
1312
	{
1313
		switch($this->Type)
1314
		{
1315
			case 'pgsql':
1316
				return $expr.'::double';
1317
			case 'mysql':
1318
				return 'CAST('.$expr.' AS DECIMAL(24,3))';
1319
		}
1320
		return $expr;
1321
	}
1322
1323
	/**
1324
	 * Cast a column or sql expression to integer, necessary at least for postgreSQL
1325
	 *
1326
	 * @param string $expr
1327
	 * @return string
1328
	 */
1329
	function to_int($expr)
1330
	{
1331
		switch($this->Type)
1332
		{
1333
			case 'pgsql':
1334
				return $expr.'::integer';
1335
			case 'mysql':
1336
				return 'CAST('.$expr.' AS SIGNED)';
1337
		}
1338
		return $expr;
1339
	}
1340
1341
	/**
1342
	 * Cast a column or sql expression to varchar, necessary at least for postgreSQL
1343
	 *
1344
	 * @param string $expr
1345
	 * @return string
1346
	 */
1347
	function to_varchar($expr)
1348
	{
1349
		switch($this->Type)
1350
		{
1351
			case 'pgsql':
1352
				return 'CAST('.$expr.' AS varchar)';
1353
		}
1354
		return $expr;
1355
	}
1356
1357
	/**
1358
	* Correctly Quote Identifiers like table- or colmnnames for use in SQL-statements
1359
	*
1360
	* This is mostly copy & paste from adodb's datadict class
1361
	* @param string $_name
1362
	* @return string quoted string
1363
	*/
1364
	function name_quote($_name = NULL)
1365
	{
1366
		if (!is_string($_name))
1367
		{
1368
			return false;
1369
		}
1370
1371
		$name = trim($_name);
1372
1373
		if (!$this->Link_ID && !$this->connect())
1374
		{
1375
			return false;
1376
		}
1377
1378
		$quote = $this->Link_ID->nameQuote;
1379
		$type = $this->Type;
1380
1381
		// if name is of the form `name`, remove MySQL quotes and leave it to automatic below
1382 View Code Duplication
		if ($name[0] === '`' && substr($name, -1) === '`')
1383
		{
1384
			$name = substr($name, 1, -1);
1385
		}
1386
1387
		$quoted = array_map(function($name) use ($quote, $type)
1388
		{
1389
			// if name contains special characters, quote it
1390
			// always quote for postgreSQL, as this is the only way to support mixed case names
1391
			if (preg_match('/\W/', $name) || $type == 'pgsql' && preg_match('/[A-Z]+/', $name) || $name == 'index')
1392
			{
1393
				return $quote . $name . $quote;
1394
			}
1395
			return $name;
1396
		}, explode('.', $name));
1397
1398
		return implode('.', $quoted);
1399
	}
1400
1401
	/**
1402
	* Escape values before sending them to the database - prevents SQL injection and SQL errors ;-)
1403
	*
1404
	* Please note that the quote function already returns necessary quotes: quote('Hello') === "'Hello'".
1405
	* Int and Auto types are casted to int: quote('1','int') === 1, quote('','int') === 0, quote('Hello','int') === 0
1406
	* Arrays of id's stored in strings: quote(array(1,2,3),'string') === "'1,2,3'"
1407
	*
1408
	* @param mixed $value the value to be escaped
1409
	* @param string|boolean $type =false string the type of the db-column, default False === varchar
1410
	* @param boolean $not_null =true is column NOT NULL, default true, else php null values are written as SQL NULL
1411
	* @param int $length =null length of the varchar column, to truncate it if the database requires it (eg. Postgres)
1412
	* @param string $glue =',' used to glue array values together for the string type
1413
	* @return string escaped sting
1414
	*/
1415
	function quote($value,$type=False,$not_null=true,$length=null,$glue=',')
1416
	{
1417
		if ($this->Debug) echo "<p>db::quote(".(is_null($value)?'NULL':"'$value'").",'$type','$not_null')</p>\n";
1418
1419
		if (!$not_null && is_null($value))	// writing unset php-variables and those set to NULL now as SQL NULL
1420
		{
1421
			return 'NULL';
1422
		}
1423
		switch($type)
1424
		{
1425
			case 'int':
1426
				// if DateTime object given, convert it to a unix timestamp (NOT converting the timezone!)
1427
				if (is_object($value) && ($value instanceof \DateTime))
1428
				{
1429
					return ($value instanceof DateTime) ? $value->format('ts') : DateTime::to($value,'ts');
1430
				}
1431
			case 'auto':
1432
				// atm. (php5.2) php has only 32bit integers, it converts everything else to float.
1433
				// Casting it to int gives a negative number instead of the big 64bit integer!
1434
				// There for we have to keep it as float by using round instead the int cast.
1435
				return is_float($value) ? round($value) : (int) $value;
1436
			case 'bool':
1437
				if ($this->Type == 'mysql')		// maybe it's not longer necessary with mysql5
1438
				{
1439
					return $value ? 1 : 0;
1440
				}
1441
				return $value ? 'true' : 'false';
1442
			case 'float':
1443
			case 'decimal':
1444
				return (double) $value;
1445
		}
1446
		if (!$this->Link_ID && !$this->connect())
1447
		{
1448
			return False;
1449
		}
1450
		switch($type)
1451
		{
1452
			case 'blob':
1453
				switch ($this->Link_ID->blobEncodeType)
1454
				{
1455
					case 'C':	// eg. postgres
1456
						return "'" . $this->Link_ID->BlobEncode($value) . "'";
1457
					case 'I':
1458
						return $this->Link_ID->BlobEncode($value);
1459
				}
1460
				break;	// handled like strings
1461 View Code Duplication
			case 'date':
1462
				// if DateTime object given, convert it (NOT converting the timezone!)
1463
				if (is_object($value) && ($value instanceof \DateTime))
1464
				{
1465
					return $this->Link_ID->qstr($value->format('Y-m-d'));
1466
				}
1467
				return $this->Link_ID->DBDate($value);
1468 View Code Duplication
			case 'timestamp':
1469
				// if DateTime object given, convert it (NOT converting the timezone!)
1470
				if (is_object($value) && ($value instanceof \DateTime))
1471
				{
1472
					return $this->Link_ID->qstr($value->format('Y-m-d H:i:s'));
1473
				}
1474
				return $this->Link_ID->DBTimeStamp($value);
1475
		}
1476
		if (is_array($value))
1477
		{
1478
			$value = implode($glue,$value);
1479
		}
1480
		// only truncate string if length given and <= 255
1481
		// to not unnecessary truncate varchar(>255) as PostgreSQL uses text anyway and MySQL truncates itself silently (unless strict mode!)
1482
		if (!is_null($length) && $length <= 255 && mb_strlen($value) > $length)
1483
		{
1484
			$value = mb_substr($value, 0, $length);
1485
		}
1486
		// casting boolean explicitly to string, as ADODB_postgres64::qstr() has an unwanted special handling
1487
		// for boolean types, causing it to return "true" or "false" and not a quoted string like "'1'"!
1488
		if (is_bool($value)) $value = (string)$value;
1489
1490
		// 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
1491
		return $this->Link_ID->qstr((string)$value);
1492
	}
1493
1494
	/**
1495
	* Implodes an array of column-value pairs for the use in sql-querys.
1496
	* All data is run through quote (does either addslashes() or (int)) - prevents SQL injunction and SQL errors ;-).
1497
	*
1498
	* @author RalfBecker<at>outdoor-training.de
1499
	*
1500
	* @param string $glue in most cases this will be either ',' or ' AND ', depending you your query
1501
	* @param array $array column-name / value pairs, if the value is an array all its array-values will be quoted
1502
	*	according to the type of the column, and the whole array with be formatted like (val1,val2,...)
1503
	*	If $use_key == True, an ' IN ' instead a '=' is used. Good for category- or user-lists.
1504
	*	If the key is numerical (no key given in the array-definition) the value is used as is, eg.
1505
	*	array('visits=visits+1') gives just "visits=visits+1" (no quoting at all !!!)
1506
	* @param boolean|string $use_key If $use_key===True a "$key=" prefix each value (default), typically set to False
1507
	*	or 'VALUES' for insert querys, on 'VALUES' "(key1,key2,...) VALUES (val1,val2,...)" is returned
1508
	* @param array|boolean $only if set to an array only colums which are set (as data !!!) are written
1509
	*	typicaly used to form a WHERE-clause from the primary keys.
1510
	*	If set to True, only columns from the colum_definitons are written.
1511
	* @param array|boolean $column_definitions this can be set to the column-definitions-array
1512
	*	of your table ($tables_baseline[$table]['fd'] of the setup/tables_current.inc.php file).
1513
	*	If its set, the column-type-data determinates if (int) or addslashes is used.
1514
	* @return string SQL
1515
	*/
1516
	function column_data_implode($glue,$array,$use_key=True,$only=False,$column_definitions=False)
1517
	{
1518
		if (!is_array($array))	// this allows to give an SQL-string for delete or update
1519
		{
1520
			return $array;
1521
		}
1522
		if (!$column_definitions)
1523
		{
1524
			$column_definitions = $this->column_definitions;
1525
		}
1526
		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";
1527
1528
		// do we need to truncate varchars to their max length (INSERT and UPDATE on Postgres)
1529
		$truncate_varchar = $glue == ',' && $this->capabilities[self::CAPABILITY_REQUIRE_TRUNCATE_VARCHAR];
1530
1531
		$keys = $values = array();
1532
		foreach($array as $key => $data)
1533
		{
1534
			if (is_int($key) && $use_key !== 'VALUES' || !$only || $only === True && isset($column_definitions[$key]) ||
1535
				is_array($only) && in_array($key,$only))
1536
			{
1537
				$keys[] = $this->name_quote($key);
1538
1539
				$col = $key;
1540
				// fix "table.column" expressions, to not trigger exception, if column alone would work
1541
				if (!is_int($key) && is_array($column_definitions) && !isset($column_definitions[$key]))
1542
				{
1543
					if (strpos($key, '.') !== false) list(, $col) = explode('.', $key);
1544
					if (!isset($column_definitions[$col]))
1545
					{
1546
						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>");
1547
					}
1548
				}
1549
				$column_type = is_array($column_definitions) ? @$column_definitions[$col]['type'] : False;
1550
				$not_null = is_array($column_definitions) && isset($column_definitions[$col]['nullable']) ? !$column_definitions[$col]['nullable'] : false;
1551
1552
				$maxlength = null;
1553
				if ($truncate_varchar)
1554
				{
1555
					$maxlength = in_array($column_definitions[$col]['type'], array('varchar','ascii')) ? $column_definitions[$col]['precision'] : null;
1556
				}
1557
				// dont use IN ( ), if there's only one value, it's slower for MySQL
1558
				if (is_array($data) && count($data) == 1)
1559
				{
1560
					$data = array_shift($data);
1561
				}
1562
				if (is_array($data))
1563
				{
1564
					$or_null = '';
1565
					foreach($data as $k => $v)
1566
					{
1567 View Code Duplication
						if (!$not_null && $use_key===True && is_null($v))
1568
						{
1569
							$or_null = $this->name_quote($key).' IS NULL)';
1570
							unset($data[$k]);
1571
							continue;
1572
						}
1573
						$data[$k] = $this->quote($v,$column_type,$not_null,$maxlength);
1574
					}
1575
					$values[] = ($or_null?'(':'').(!count($data) ?
1576
						// empty array on insert/update, store as NULL, or if not allowed whatever value NULL is casted to
1577
						$this->quote(null, $column_type, $not_null) :
1578
						($use_key===True ? $this->name_quote($key).' IN ' : '') .
1579
						'('.implode(',',$data).')'.($or_null ? ' OR ' : '')).$or_null;
1580
				}
1581
				elseif (is_int($key) && $use_key===True)
1582
				{
1583
					if (empty($data)) continue;	// would give SQL error
1584
					$values[] = $data;
1585
				}
1586 View Code Duplication
				elseif ($glue != ',' && $use_key === True && !$not_null && is_null($data))
1587
				{
1588
					$values[] = $this->name_quote($key) .' IS NULL';
1589
				}
1590
				else
1591
				{
1592
					$values[] = ($use_key===True ? $this->name_quote($key) . '=' : '') . $this->quote($data,$column_type,$not_null,$maxlength);
1593
				}
1594
			}
1595
		}
1596
		return ($use_key==='VALUES' ? '('.implode(',',$keys).') VALUES (' : '').
1597
			implode($glue,$values) . ($use_key==='VALUES' ? ')' : '');
1598
	}
1599
1600
	/**
1601
	* Sets the default column-definitions for use with column_data_implode()
1602
	*
1603
	* @author RalfBecker<at>outdoor-training.de
1604
	*
1605
	* @param array|boolean $column_definitions this can be set to the column-definitions-array
1606
	*	of your table ($tables_baseline[$table]['fd'] of the setup/tables_current.inc.php file).
1607
	*	If its set, the column-type-data determinates if (int) or addslashes is used.
1608
	*/
1609
	function set_column_definitions($column_definitions=False)
1610
	{
1611
		$this->column_definitions=$column_definitions;
1612
	}
1613
1614
	/**
1615
	 * Application name used by the API
1616
	 *
1617
	 */
1618
	const API_APPNAME = 'api';
1619
	/**
1620
	 * Default app, if no app specified in select, insert, delete, ...
1621
	 *
1622
	 * @var string
1623
	 */
1624
	private $app=self::API_APPNAME;
1625
1626
	/**
1627
	 * Sets the application in which the db-class looks for table-defintions
1628
	 *
1629
	 * Used by table_definitions, insert, update, select, expression and delete. If the app is not set via set_app,
1630
	 * it need to be set for these functions on every call
1631
	 *
1632
	 * @param string $app the app-name
1633
	 */
1634
	function set_app($app)
1635
	{
1636
		// ease the transition to api
1637
		if ($app == 'phpgwapi') $app = 'api';
1638
1639
		if ($this === $GLOBALS['egw']->db && $app != self::API_APPNAME)
1640
		{
1641
			// prevent that anyone switches the global db object to an other app
1642
			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!');
1643
		}
1644
		$this->app = $app;
1645
	}
1646
1647
	/**
1648
	* reads the table-definitions from the app's setup/tables_current.inc.php file
1649
	*
1650
	* The already read table-definitions are shared between all db-instances via a static var.
1651
	*
1652
	* @author RalfBecker<at>outdoor-training.de
1653
	*
1654
	* @param bool|string $app name of the app or default False to use the app set by db::set_app or the current app,
1655
	*	true to search the already loaded table-definitions for $table and then search all existing apps for it
1656
	* @param bool|string $table if set return only defintions of that table, else return all defintions
1657
	* @return mixed array with table-defintions or False if file not found
1658
	*/
1659
	function get_table_definitions($app=False,$table=False)
1660
	{
1661
		// ease the transition to api
1662
		if ($app === 'phpgwapi') $app = 'api';
1663
1664
		static $all_app_data = array();
1665
		if ($app === true && $table)
1666
		{
1667
			foreach($all_app_data as $app => &$app_data)
1668
			{
1669
				if (isset($app_data[$table]))
1670
				{
1671
					return $app_data[$table];
1672
				}
1673
			}
1674
			// $table not found in loaded apps, check not yet loaded ones
1675
			foreach(scandir(EGW_INCLUDE_ROOT) as $app)
1676
			{
1677
				if ($app[0] == '.' || !is_dir(EGW_INCLUDE_ROOT.'/'.$app) || isset($all_app_data[$app]))
1678
				{
1679
					continue;
1680
				}
1681
				$tables_current = EGW_INCLUDE_ROOT . "/$app/setup/tables_current.inc.php";
1682
				if (!@file_exists($tables_current))
1683
				{
1684
					$all_app_data[$app] = False;
1685
				}
1686
				else
1687
				{
1688
					$phpgw_baseline = null;
1689
					include($tables_current);
1690
					$all_app_data[$app] =& $phpgw_baseline;
1691
					unset($phpgw_baseline);
1692
1693
					if (isset($all_app_data[$app][$table]))
1694
					{
1695
						return $all_app_data[$app][$table];
1696
					}
1697
				}
1698
			}
1699
			$app = false;
1700
		}
1701
		if (!$app)
1702
		{
1703
			$app = $this->app ? $this->app : $GLOBALS['egw_info']['flags']['currentapp'];
1704
		}
1705
		$app_data =& $all_app_data[$app];
1706
1707
		if (!isset($app_data))
1708
		{
1709
			$tables_current = EGW_INCLUDE_ROOT . "/$app/setup/tables_current.inc.php";
1710
			if (!@file_exists($tables_current))
1711
			{
1712
				return $app_data = False;
1713
			}
1714
			include($tables_current);
1715
			$app_data =& $phpgw_baseline;
1716
			unset($phpgw_baseline);
1717
		}
1718
		if ($table && (!$app_data || !isset($app_data[$table])))
1719
		{
1720
			if ($this->Debug) echo "<p>!!!get_table_definitions($app,$table) failed!!!</p>\n";
1721
			return False;
1722
		}
1723
		if ($this->Debug) echo "<p>get_table_definitions($app,$table) succeeded</p>\n";
1724
		return $table ? $app_data[$table] : $app_data;
1725
	}
1726
1727
	/**
1728
	 * Get specified attribute (default comment) of a colum or whole definition (if $attribute === null)
1729
	 *
1730
	 * Can be used static, in which case the global db object is used ($GLOBALS['egw']->db) and $app should be specified
1731
	 *
1732
	 * @param string $column name of column
1733
	 * @param string $table name of table
1734
	 * @param string $app=null app name or NULL to use $this->app, set via self::set_app()
1735
	 * @param string $attribute='comment' what field to return, NULL for array with all fields, default 'comment' to return the comment
1736
	 * @return string|array NULL if table or column or attribute not found
1737
	 */
1738
	/* static */ function get_column_attribute($column,$table,$app=null,$attribute='comment')
1739
	{
1740
		static $cached_columns=null,$cached_table=null;	// some caching
1741
1742
		if ($cached_table !== $table || is_null($cached_columns))
1743
		{
1744
			$db = isset($this) ? $this : $GLOBALS['egw']->db;
1745
			$table_def = $db->get_table_definitions($app,$table);
1746
			$cached_columns = is_array($table_def) ? $table_def['fd'] : false;
1747
		}
1748
		if ($cached_columns === false) return null;
1749
1750
		return is_null($attribute) ? $cached_columns[$column] : $cached_columns[$column][$attribute];
1751
	}
1752
1753
	/**
1754
	* Insert a row of data into a table or updates it if $where is given, all data is quoted according to it's type
1755
	*
1756
	* @author RalfBecker<at>outdoor-training.de
1757
	*
1758
	* @param string $table name of the table
1759
	* @param array $data with column-name / value pairs
1760
	* @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
1761
	*	if the row exists db::update is called else a new row with $date merged with $where gets inserted (data has precedence)
1762
	* @param int $line line-number to pass to query
1763
	* @param string $file file-name to pass to query
1764
	* @param string|boolean $app string with name of app or False to use the current-app
1765
	* @param bool $use_prepared_statement use a prepared statement
1766
	* @param array|bool $table_def use this table definition. If False, the table definition will be read from tables_baseline
1767
	* @return ADORecordSet or false, if the query fails
1768
	*/
1769
	function insert($table,$data,$where,$line,$file,$app=False,$use_prepared_statement=false,$table_def=False)
1770
	{
1771 View Code Duplication
		if ($this->Debug) echo "<p>db::insert('$table',".print_r($data,True).",".print_r($where,True).",$line,$file,'$app')</p>\n";
1772
1773
		if (!$table_def) $table_def = $this->get_table_definitions($app,$table);
1774
1775
		$sql_append = '';
1776
		$cmd = 'INSERT';
1777
		if (is_array($where) && count($where))
1778
		{
1779
			switch($this->Type)
1780
			{
1781
				case 'sapdb': case 'maxdb':
1782
					$sql_append = ' UPDATE DUPLICATES';
1783
					break;
1784
				case 'mysql':
1785
					// use replace if primary keys are included
1786
					if (count(array_intersect(array_keys($where),(array)$table_def['pk'])) == count($table_def['pk']))
1787
					{
1788
						$cmd = 'REPLACE';
1789
						break;
1790
					}
1791
					// fall through !!!
1792
				default:
1793
					if ($this->select($table,'count(*)',$where,$line,$file)->fetchColumn())
1794
					{
1795
						return !!$this->update($table,$data,$where,$line,$file,$app,$use_prepared_statement,$table_def);
1796
					}
1797
					break;
1798
			}
1799
			// the checked values need to be inserted too, value in data has precedence, also cant insert sql strings (numerical id)
1800
			foreach($where as $column => $value)
1801
			{
1802
				if (!is_numeric($column) && !isset($data[$column]) &&
1803
					// skip auto-id of 0 or NULL, as PostgreSQL does NOT create an auto-id, if they are given
1804
					!(!$value && count($table_def['pk']) == 1 && $column == $table_def['pk'][0]))
1805
				{
1806
					$data[$column] = $value;
1807
				}
1808
			}
1809
		}
1810
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
1811
		{
1812
			$table = self::$tablealiases[$table];
1813
		}
1814
		$inputarr = false;
1815
		if (isset($data[0]) && is_array($data[0]))	// multiple data rows
1816
		{
1817
			if ($where) throw new Exception\WrongParameter('Can NOT use $where together with multiple data rows in $data!');
1818
1819
			$sql = "$cmd INTO $table ";
1820
			foreach($data as $k => $d)
1821
			{
1822
				if (!$k)
1823
				{
1824
					$sql .= $this->column_data_implode(',',$d,'VALUES',true,$table_def['fd']);
1825
				}
1826
				else
1827
				{
1828
					$sql .= ",\n(".$this->column_data_implode(',',$d,false,true,$table_def['fd']).')';
1829
				}
1830
			}
1831
			$sql .= $sql_append;
1832
		}
1833
		elseif ($use_prepared_statement && $this->Link_ID->_bindInputArray)	// eg. MaxDB
1834
		{
1835
			$this->Link_ID->Param(false);	// reset param-counter
1836
			$cols = array_keys($data);
1837
			foreach($cols as $k => $col)
1838
			{
1839
				if (!isset($table_def['fd'][$col]))	// ignore columns not in this table
1840
				{
1841
					unset($cols[$k]);
1842
					continue;
1843
				}
1844
				$params[] = $this->Link_ID->Param($col);
1845
			}
1846
			$sql = "$cmd INTO $table (".implode(',',$cols).') VALUES ('.implode(',',$params).')'.$sql_append;
1847
			// check if we already prepared that statement
1848 View Code Duplication
			if (!isset($this->prepared_sql[$sql]))
1849
			{
1850
				$this->prepared_sql[$sql] = $this->Link_ID->Prepare($sql);
1851
			}
1852
			$sql = $this->prepared_sql[$sql];
1853
			$inputarr = &$data;
1854
		}
1855
		else
1856
		{
1857
			$sql = "$cmd INTO $table ".$this->column_data_implode(',',$data,'VALUES',true,$table_def['fd']).$sql_append;
1858
		}
1859 View Code Duplication
		if ($this->Debug) echo "<p>db::insert('$table',".print_r($data,True).",".print_r($where,True).",$line,$file,'$app') sql='$sql'</p>\n";
1860
		return $this->query($sql,$line,$file,0,-1,$inputarr);
1861
	}
1862
1863
	/**
1864
	* Updates the data of one or more rows in a table, all data is quoted according to it's type
1865
	*
1866
	* @author RalfBecker<at>outdoor-training.de
1867
	*
1868
	* @param string $table name of the table
1869
	* @param array $data with column-name / value pairs
1870
	* @param array $where column-name / values pairs and'ed together for the where clause
1871
	* @param int $line line-number to pass to query
1872
	* @param string $file file-name to pass to query
1873
	* @param string|boolean $app string with name of app or False to use the current-app
1874
	* @param bool $use_prepared_statement use a prepared statement
1875
	* @param array|bool $table_def use this table definition. If False, the table definition will be read from tables_baseline
1876
	* @return ADORecordSet or false, if the query fails
1877
	*/
1878
	function update($table,$data,$where,$line,$file,$app=False,$use_prepared_statement=false,$table_def=False)
1879
	{
1880 View Code Duplication
		if ($this->Debug) echo "<p>db::update('$table',".print_r($data,true).','.print_r($where,true).",$line,$file,'$app')</p>\n";
1881
		if (!$table_def) $table_def = $this->get_table_definitions($app,$table);
1882
1883
		$blobs2update = array();
1884
		// SapDB/MaxDB cant update LONG columns / blob's: if a blob-column is included in the update we remember it in $blobs2update
1885
		// and remove it from $data
1886
		switch ($this->Type)
1887
		{
1888
			case 'sapdb':
1889
			case 'maxdb':
1890
				if ($use_prepared_statement) break;
1891
				// check if data contains any LONG columns
1892
				foreach($data as $col => $val)
1893
				{
1894
					switch ($table_def['fd'][$col]['type'])
1895
					{
1896
						case 'text':
1897
						case 'longtext':
1898
						case 'blob':
1899
							$blobs2update[$col] = &$data[$col];
1900
							unset($data[$col]);
1901
							break;
1902
					}
1903
				}
1904
				break;
1905
		}
1906
		$where_str = $this->column_data_implode(' AND ',$where,True,true,$table_def['fd']);
1907
1908
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
1909
		{
1910
			$table = self::$tablealiases[$table];
1911
		}
1912
		if (count($data))
1913
		{
1914
			$inputarr = false;
1915
			if ($use_prepared_statement && $this->Link_ID->_bindInputArray)	// eg. MaxDB
1916
			{
1917
				$this->Link_ID->Param(false);	// reset param-counter
1918
				foreach($data as $col => $val)
1919
				{
1920
					if (!isset($table_def['fd'][$col])) continue;	// ignore columns not in this table
1921
					$params[] = $this->name_quote($col).'='.$this->Link_ID->Param($col);
1922
				}
1923
				$sql = "UPDATE $table SET ".implode(',',$params).' WHERE '.$where_str;
1924
				// check if we already prepared that statement
1925 View Code Duplication
				if (!isset($this->prepared_sql[$sql]))
1926
				{
1927
					$this->prepared_sql[$sql] = $this->Link_ID->Prepare($sql);
1928
				}
1929
				$sql = $this->prepared_sql[$sql];
1930
				$inputarr = &$data;
1931
			}
1932
			else
1933
			{
1934
				$sql = "UPDATE $table SET ".
1935
					$this->column_data_implode(',',$data,True,true,$table_def['fd']).' WHERE '.$where_str;
1936
			}
1937
			$ret = $this->query($sql,$line,$file,0,-1,$inputarr);
1938
			if ($this->Debug) echo "<p>db::query('$sql',$line,$file)</p>\n";
1939
		}
1940
		// if we have any blobs to update, we do so now
1941
		if (($ret || !count($data)) && count($blobs2update))
1942
		{
1943
			foreach($blobs2update as $col => $val)
1944
			{
1945
				$ret = $this->Link_ID->UpdateBlob($table,$col,$val,$where_str,$table_def['fd'][$col]['type'] == 'blob' ? 'BLOB' : 'CLOB');
1946
				if ($this->Debug) echo "<p>adodb::UpdateBlob('$table','$col','$val','$where_str') = '$ret'</p>\n";
1947
				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 InvalidSql::__construct() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1948
			}
1949
		}
1950
		return $ret;
1951
	}
1952
1953
	/**
1954
	* Deletes one or more rows in table, all data is quoted according to it's type
1955
	*
1956
	* @author RalfBecker<at>outdoor-training.de
1957
	*
1958
	* @param string $table name of the table
1959
	* @param array $where column-name / values pairs and'ed together for the where clause
1960
	* @param int $line line-number to pass to query
1961
	* @param string $file file-name to pass to query
1962
	* @param string|boolean $app string with name of app or False to use the current-app
1963
	* @param array|bool $table_def use this table definition. If False, the table definition will be read from tables_baseline
1964
	* @return ADORecordSet or false, if the query fails
1965
	*/
1966
	function delete($table,$where,$line,$file,$app=False,$table_def=False)
1967
	{
1968
		if (!$table_def) $table_def = $this->get_table_definitions($app,$table);
1969
1970
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
1971
		{
1972
			$table = self::$tablealiases[$table];
1973
		}
1974
		$sql = "DELETE FROM $table WHERE ".
1975
			$this->column_data_implode(' AND ',$where,True,False,$table_def['fd']);
1976
1977
		return $this->query($sql,$line,$file);
1978
	}
1979
1980
	/**
1981
	 * Formats and quotes a sql expression to be used eg. as where-clause
1982
	 *
1983
	 * The function has a variable number of arguments, from which the expession gets constructed
1984
	 * eg. db::expression('my_table','(',array('name'=>"test'ed",'lang'=>'en'),') OR ',array('owner'=>array('',4,10)))
1985
	 * 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
1986
	 *
1987
	 * @param string|array $table_def table-name or definition array
1988
	 * @param mixed $args variable number of arguments of the following types:
1989
	 *	string: get's as is into the result
1990
	 *	array:	column-name / value pairs: the value gets quoted according to the type of the column and prefixed
1991
	 *		with column-name=, multiple pairs are AND'ed together, see db::column_data_implode
1992
	 *	bool: If False or is_null($arg): the next 2 (!) arguments gets ignored
1993
	 *
1994
	 * Please note: As the function has a variable number of arguments, you CAN NOT add further parameters !!!
1995
	 *
1996
	 * @return string the expression generated from the arguments
1997
	 */
1998
	function expression($table_def/*,$args, ...*/)
1999
	{
2000
		if (!is_array($table_def)) $table_def = $this->get_table_definitions(true,$table_def);
2001
		$sql = '';
2002
		$ignore_next = 0;
2003
		foreach(func_get_args() as $n => $arg)
2004
		{
2005
			if ($n < 1) continue;	// table-name
2006
2007
			if ($ignore_next)
2008
			{
2009
				--$ignore_next;
2010
				continue;
2011
			}
2012
			if (is_null($arg)) $arg = False;
2013
2014
			switch(gettype($arg))
2015
			{
2016
				case 'string':
2017
					$sql .= $arg;
2018
					break;
2019
				case 'boolean':
2020
					$ignore_next += !$arg ? 2 : 0;
2021
					break;
2022
				case 'array':
2023
					$sql .= $this->column_data_implode(' AND ',$arg,True,False,$table_def['fd']);
2024
					break;
2025
			}
2026
		}
2027
		return $sql;
2028
	}
2029
2030
	/**
2031
	* Selects one or more rows in table depending on where, all data is quoted according to it's type
2032
	*
2033
	* @author RalfBecker<at>outdoor-training.de
2034
	*
2035
	* @param string $table name of the table
2036
	* @param array|string $cols string or array of column-names / select-expressions
2037
	* @param array|string $where string or array with column-name / values pairs AND'ed together for the where clause
2038
	* @param int $line line-number to pass to query
2039
	* @param string $file file-name to pass to query
2040
	* @param int|bool $offset offset for a limited query or False (default)
2041
	* @param string $append string to append to the end of the query, eg. ORDER BY ...
2042
	* @param string|boolean $app string with name of app or False to use the current-app
2043
	* @param int $num_rows number of rows to return if offset set, default 0 = use default in user prefs
2044
	* @param string $join =null sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
2045
	*	"LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join!
2046
	* @param array|bool $table_def use this table definition. If False, the table definition will be read from tables_baseline
2047
	* @param int $fetchmode =self::FETCH_ASSOC self::FETCH_ASSOC (default), self::FETCH_BOTH or self::FETCH_NUM
2048
	* @return ADORecordSet or false, if the query fails
2049
	*/
2050
	function select($table,$cols,$where,$line,$file,$offset=False,$append='',$app=False,$num_rows=0,$join='',$table_def=False,$fetchmode=self::FETCH_ASSOC)
2051
	{
2052 View Code Duplication
		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";
2053
2054
		if (!$table_def) $table_def = $this->get_table_definitions($app,$table);
2055
		if (is_array($cols))
2056
		{
2057
			$cols = implode(',',$cols);
2058
		}
2059
		if (is_array($where))
2060
		{
2061
			$where = $this->column_data_implode(' AND ',$where,True,False,$table_def['fd']);
2062
		}
2063
		if (self::$tablealiases && isset(self::$tablealiases[$table]))
2064
		{
2065
			$table = self::$tablealiases[$table];
2066
		}
2067
		$sql = "SELECT $cols FROM $table $join";
2068
2069
		// if we have a where clause, we need to add it together with the WHERE statement, if thats not in the join
2070
		if ($where) $sql .= (strpos($join,"WHERE")!==false) ? ' AND ('.$where.')' : ' WHERE '.$where;
2071
2072
		if ($append) $sql .= ' '.$append;
2073
2074
		if ($this->Debug) echo "<p>sql='$sql'</p>";
2075
2076
		if ($line === false && $file === false)	// call by union, to return the sql rather then run the query
2077
		{
2078
			return $sql;
2079
		}
2080
		return $this->query($sql,$line,$file,$offset,$offset===False ? -1 : (int)$num_rows,false,$fetchmode);
0 ignored issues
show
Bug introduced by
It seems like $offset defined by parameter $offset on line 2050 can also be of type boolean; however, EGroupware\Api\Db::query() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2081
	}
2082
2083
	/**
2084
	* Does a union over multiple selects
2085
	*
2086
	* @author RalfBecker<at>outdoor-training.de
2087
	*
2088
	* @param array $selects array of selects, each select is an array with the possible keys/parameters: table, cols, where, append, app, join, table_def
2089
	*	For further info about parameters see the definition of the select function, beside table, cols and where all other params are optional
2090
	* @param int $line line-number to pass to query
2091
	* @param string $file file-name to pass to query
2092
	* @param string $order_by ORDER BY statement for the union
2093
	* @param int|bool $offset offset for a limited query or False (default)
2094
	* @param int $num_rows number of rows to return if offset set, default 0 = use default in user prefs
2095
	* @param int $fetchmode =self::FETCH_ASSOC self::FETCH_ASSOC (default), self::FETCH_BOTH or self::FETCH_NUM
2096
	* @return ADORecordSet or false, if the query fails
2097
	*/
2098
	function union($selects,$line,$file,$order_by='',$offset=false,$num_rows=0,$fetchmode=self::FETCH_ASSOC)
2099
	{
2100
		if ($this->Debug) echo "<p>db::union(".print_r($selects,True).",$line,$file,$order_by,$offset,$num_rows)</p>\n";
2101
2102
		$union = array();
2103
		foreach($selects as $select)
2104
		{
2105
			$union[] = call_user_func_array(array($this,'select'),array(
2106
				$select['table'],
2107
				$select['cols'],
2108
				$select['where'],
2109
				false,	// line
2110
				false,	// file
2111
				false,	// offset
2112
				$select['append'],
2113
				$select['app'],
2114
				0,		// num_rows,
2115
				$select['join'],
2116
				$select['table_def'],
2117
			));
2118
		}
2119
		$sql = count($union) > 1 ? '(' . implode(")\nUNION\n(",$union).')' : 'SELECT DISTINCT'.substr($union[0],6);
2120
2121
		if ($order_by) $sql .=  (!stristr($order_by,'ORDER BY') ? "\nORDER BY " : '').$order_by;
2122
2123
		if ($this->Debug) echo "<p>sql='$sql'</p>";
2124
2125
		return $this->query($sql,$line,$file,$offset,$offset===False ? -1 : (int)$num_rows,false,$fetchmode);
0 ignored issues
show
Bug introduced by
It seems like $offset defined by parameter $offset on line 2098 can also be of type boolean; however, EGroupware\Api\Db::query() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2126
	}
2127
2128
	/**
2129
	 * Strip eg. a prefix from the keys of an array
2130
	 *
2131
	 * @param array $arr
2132
	 * @param string|array $strip
2133
	 * @return array
2134
	 */
2135
	static function strip_array_keys($arr,$strip)
2136
	{
2137
		$keys = array_keys($arr);
2138
2139
		return array_walk($keys, function(&$v, $k, $strip)
2140
		{
2141
			unset($k);	// not used, but required by function signature
2142
			$v = str_replace($strip, '', $v);
2143
		}, $strip) ?
2144
			array_combine($keys,$arr) : $arr;
2145
	}
2146
}
2147