Schema::RefreshTable()   F
last analyzed

Complexity

Conditions 46
Paths > 20000

Size

Total Lines 138
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 46
eloc 73
nc 44640
nop 3
dl 0
loc 138
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * EGroupware - Setup - db-schema-processor
4
 *
5
 * Originaly written by
6
 *  - Michael Dean <[email protected]>
7
 *  - Miles Lott<[email protected]>
8
 * Rewritten and adapted to ADOdb's schema processor by Ralf Becker.
9
 *
10
 * @link http://www.egroupware.org
11
 * @author Ralf Becker <[email protected]>
12
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
13
 * @package api
14
 * @subpackage db
15
 */
16
17
namespace EGroupware\Api\Db;
18
19
use EGroupware\Api;
20
21
/**
22
 * EGroupware's ADOdb based schema-processor
23
 */
24
class Schema
25
{
26
	/**
27
	 * @deprecated formerly used translator class, now a reference to ourself
28
	 */
29
	var $m_oTranslator;
30
	/**
31
	 * db-object
32
	 *
33
	 * @var EGroupware\Api\Db\Deprecated
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Db\EGroupware\Api\Db\Deprecated was not found. Did you mean EGroupware\Api\Db\Deprecated? If so, make sure to prefix the type with \.
Loading history...
34
	 */
35
	var $m_odb;
36
	/**
37
	 * reference to the global ADOdb object
38
	 *
39
	 * @var ADOConnection
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Db\ADOConnection was not found. Did you mean ADOConnection? If so, make sure to prefix the type with \.
Loading history...
40
	 */
41
	var $adodb;
42
	/**
43
	 * adodb's datadictionary object for the used db-type
44
	 *
45
	 * @var ADODB_DataDict
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Db\ADODB_DataDict was not found. Did you mean ADODB_DataDict? If so, make sure to prefix the type with \.
Loading history...
46
	 */
47
	var $dict;
48
	/**
49
	 * Debuglevel: 0=Off, 1=some, eg. primary function calls, 2=lots incl. the SQL used
50
	 *
51
	 * @var int
52
	 */
53
	var $debug = 0;
54
	/**
55
	 * Array with db => max. length of indexes pairs (if there is a considerable low limit for a db)
56
	 *
57
	 * @var array
58
	 */
59
	var $max_index_length=array(
60
		'maxdb'  => 32,
61
		'oracle' => 30,
62
	);
63
	/**
64
	 * type of the database, set by the the constructor: 'mysql','pgsql','mssql','maxdb'
65
	 *
66
	 * @var string
67
	 */
68
	var $sType;
69
	/**
70
	 *	maximum length of a varchar column, everything above get converted to text
71
	 *
72
	 *	@var int
73
	 */
74
	var $max_varchar_length = 255;
75
	/**
76
	 * system-charset if set
77
	 *
78
	 * @var string
79
	 */
80
	var $system_charset = 'utf8';
81
	/**
82
	 * reference to the capabilities array of the db-class
83
	 *
84
	 * @var array
85
	 */
86
	var $capabilities;
87
	/**
88
	 * preserve value of old sequences in PostgreSQL
89
	 *
90
	 * @var int
91
	 */
92
	var $pgsql_old_seq;
93
94
	/**
95
	 * Constructor of schema-processor
96
	 *
97
	 * @param string $dbms type of the database: 'mysql','pgsql','mssql','maxdb'
98
	 * @param Db $db =null database class, if null we use $GLOBALS['egw']->db
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Db\Db was not found. Did you mean Db? If so, make sure to prefix the type with \.
Loading history...
99
	 * @return schema_proc
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Db\schema_proc 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...
100
	 */
101
	function __construct($dbms=False, Api\Db $db=null)
102
	{
103
	    if(is_object($db))
104
		{
105
			$this->m_odb = $db;
0 ignored issues
show
Documentation Bug introduced by
It seems like $db of type EGroupware\Api\Db is incompatible with the declared type EGroupware\Api\Db\EGroupware\Api\Db\Deprecated of property $m_odb.

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...
106
	    }
107
	    else
108
	    {
109
			$this->m_odb = isset($GLOBALS['egw']->db) && is_object($GLOBALS['egw']->db) ? $GLOBALS['egw']->db : $GLOBALS['egw_setup']->db;
110
	    }
111
	    if (!($this->m_odb instanceof Api\Db))
112
	    {
113
	    	throw new Api\Exception\AssertionFailed('no EGroupware\Api\Db object!');
114
	    }
115
	    $this->m_odb->connect();
116
		$this->capabilities =& $this->m_odb->capabilities;
117
118
		$this->sType = $dbms ? $dbms : $this->m_odb->Type;
119
		$this->adodb = &$this->m_odb->Link_ID;
120
		$this->dict = NewDataDictionary($this->adodb);
0 ignored issues
show
Documentation Bug introduced by
It seems like NewDataDictionary($this->adodb) can also be of type false. However, the property $dict is declared as type EGroupware\Api\Db\ADODB_DataDict. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
121
122
		// enable the debuging in ADOdb's datadictionary if the debug-level is greater then 1
123
		if ($this->debug > 1) $this->dict->debug = True;
124
125
		// to allow some of the former translator-functions to be called, we assign ourself as the translator
126
		$this->m_oTranslator = &$this;
0 ignored issues
show
Deprecated Code introduced by
The property EGroupware\Api\Db\Schema::$m_oTranslator has been deprecated: formerly used translator class, now a reference to ourself ( Ignorable by Annotation )

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

126
		/** @scrutinizer ignore-deprecated */ $this->m_oTranslator = &$this;

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

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

Loading history...
127
128
		switch($this->sType)
129
		{
130
			case 'maxdb':
131
				$this->max_varchar_length = 8000;
132
				break;
133
			case 'mysql':
134
				// since MySQL 5.0 65535, but with utf8 and row-size-limit of 64k:
135
				// it's effective 65535/3 - size of other columns, so we use 20000 (mysql silently convert to text anyway)
136
				if ((float)$this->m_odb->ServerInfo['version'] >= 5.0)
137
				{
138
					$this->max_varchar_length = 20000;
139
				}
140
				break;
141
		}
142
		if (is_object($GLOBALS['egw_setup']))
143
		{
144
			$this->system_charset =& $GLOBALS['egw_setup']->system_charset;
145
		}
146
		elseif (isset($GLOBALS['egw_info']['server']['system_charset']))
147
		{
148
			$this->system_charset = $GLOBALS['egw_info']['server']['system_charset'];
149
		}
150
	}
151
152
	/**
153
	 * Check if the given $columns exist as index in the index array $indexes
154
	 *
155
	 * @param string|array $columns column-name as string or array of column-names plus optional options key
156
	 * @param array $indexs array of indexes (column-name as string or array of column-names plus optional options key)
157
	 * @param boolean $ignore_length_limit =false should we take lenght-limits of indexes into account or not
158
	 * @return boolean true if index over $columns exist in the $indexes array
159
	 */
160
	function _in_index($columns, $indexs, $ignore_length_limit=false)
161
	{
162
		if (is_array($columns))
163
		{
164
			unset($columns['options']);
165
			$columns = implode('-',$columns);
166
			if ($ignore_length_limit) $columns = preg_replace('/\(\d+\)/', '', $columns);
167
		}
168
169
		if(is_array($indexs)){
0 ignored issues
show
introduced by
The condition is_array($indexs) is always true.
Loading history...
170
171
			foreach($indexs as $index)
172
			{
173
				if (is_array($index))
174
				{
175
					unset($index['options']);
176
					$index = implode('-',$index);
177
				}
178
				if ($ignore_length_limit) $index = preg_replace('/\(\d+\)/', '', $index);
179
				if ($columns == $index) return true;
180
			}
181
182
		}
183
184
		return false;
185
	}
186
187
	/**
188
	 * Created a table named $sTableName as defined in $aTableDef
189
	 *
190
	 * @param string $sTableName
191
	 * @param array $aTableDef
192
	 * @param bool $preserveSequence
193
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
194
	 */
195
	function CreateTable($sTableName, $aTableDef, $preserveSequence=False)
196
	{
197
		if ($this->debug)
198
		{
199
			$this->debug_message('schema_proc::CreateTable(%1,%2)',False,$sTableName, $aTableDef);
200
		}
201
		// for mysql 4.0+ we set the charset for the table
202
		if ($this->system_charset && substr($this->sType,0,5) == 'mysql' &&
203
			(float) $this->m_odb->ServerInfo['version'] >= 4.0)
204
		{
205
			$set_table_charset = array($this->sType => 'CHARACTER SET utf8');
206
		}
207
		// creating the table
208
		$aSql = $this->dict->CreateTableSQL($sTableName,$ado_cols = $this->_egw2adodb_columndef($aTableDef),$set_table_charset);
209
		if (!($retVal = $this->ExecuteSQLArray($aSql,2,'CreateTableSQL(%1,%2) sql=%3',False,$sTableName,$ado_cols,$aSql)))
210
		{
211
			return $retVal;
212
		}
213
		// creating unique indices/constrains
214
		foreach ($aTableDef['uc'] as $name => $mFields)
215
		{
216
			if (empty($mFields))
217
			{
218
				continue;	// cant create an index without fields (was observed in broken backups)
219
			}
220
			if ($this->_in_index($mFields,array($aTableDef['pk'])))
221
			{
222
				continue;	// is already created as primary key
223
			}
224
			if (!($retVal = $this->CreateIndex($sTableName,$mFields,true,'',$name)))
225
			{
226
				return $retVal;
227
			}
228
		}
229
		// creation indices
230
		foreach ($aTableDef['ix'] as $name => $mFields)
231
		{
232
			if (empty($mFields))
233
			{
234
				continue;	// cant create an index without fields (was observed in broken backups)
235
			}
236
			if ($this->_in_index($mFields,array($aTableDef['pk'])) ||
237
				$this->_in_index($mFields,$aTableDef['uc']))
238
			{
239
				continue;	// is already created as primary key or unique index
240
			}
241
			$options = False;
242
			if (is_array($mFields))
243
			{
244
				if (isset($mFields['options']))		// array sets additional options
245
				{
246
					if (isset($mFields['options'][$this->sType]))
247
					{
248
						$options = $mFields['options'][$this->sType];	// db-specific options, eg. index-type
249
						if (!$options) continue;	// no index for our db-type
250
					}
251
					unset($mFields['options']);
252
				}
253
			}
254
			foreach((array)$mFields as $k => $col)
255
			{
256
				// only create indexes on text-columns, if (db-)specifiy options are given or FULLTEXT for mysql
257
				// most DB's cant do them and give errors
258
				if (in_array($aTableDef['fd'][$col]['type'],array('text','longtext')))
259
				{
260
					if (is_array($mFields))	// index over multiple columns including a text column
261
					{
262
						$mFields[$k] .= '(32)';	// 32=limit of egw_addressbook_extra.extra_value to fix old backups
263
					}
264
					elseif (!$options)	// index over a single text column and no options given
265
					{
266
						if ($this->sType == 'mysql')
267
						{
268
							$options = 'FULLTEXT';
269
						}
270
						else
271
						{
272
							continue 2;	// ignore that index, 2=not just column but whole index!
273
						}
274
					}
275
				}
276
			}
277
			if (!($retVal = $this->CreateIndex($sTableName,$mFields,false,$options,$name)))
278
			{
279
				return $retVal;
280
			}
281
		}
282
		// preserve last value of an old sequence
283
		if ($this->sType == 'pgsql' && $preserveSequence && $this->pgsql_old_seq)
284
		{
285
			if (($seq = $this->_PostgresHasOldSequence($sTableName)))
286
			{
287
				$this->pgsql_old_seq = $this->pgsql_old_seq + 1;
288
				$this->m_odb->query("ALTER SEQUENCE $seq RESTART WITH " . $this->pgsql_old_seq,__LINE__,__FILE__);
289
			}
290
			$this->pgsql_old_seq = 0;
291
		}
292
		return $retVal;
293
	}
294
295
	/**
296
	 * Drops all tables in $aTables
297
	 *
298
	 * @param array $aTables array of eGW table-definitions
299
	 * @param boolean $bOutputHTML should we give diagnostics, default False
300
	 * @return boolean True if no error, else False
301
	 */
302
	function DropAllTables($aTables, $bOutputHTML=False)
303
	{
304
		if(!is_array($aTables) || !isset($this->m_odb))
0 ignored issues
show
introduced by
The condition is_array($aTables) is always true.
Loading history...
305
		{
306
			return False;
307
		}
308
		// set our debug-mode or $bOutputHTML is the other one is set
309
		if ($this->debug) $bOutputHTML = True;
310
		if ($bOutputHTML && !$this->debug) $this->debug = 2;
311
312
		foreach(array_keys($aTables) as $sTableName)
313
		{
314
			if($this->DropTable($sTableName))
315
			{
316
				if($bOutputHTML)
317
				{
318
					echo '<br>Drop Table <b>' . $sTableName . '</b>';
319
				}
320
			}
321
			else
322
			{
323
				return False;
324
			}
325
		}
326
		return True;
327
	}
328
329
	/**
330
	 * Drops the table $sTableName
331
	 *
332
	 * @param string $sTableName
333
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
334
	 */
335
	function DropTable($sTableName)
336
	{
337
		if ($this->sType == 'pgsql') $this->_PostgresTestDropOldSequence($sTableName);
338
339
		$aSql = $this->dict->DropTableSql($sTableName);
340
341
		return $this->ExecuteSQLArray($aSql,2,'DropTable(%1) sql=%2',False,$sTableName,$aSql);
342
	}
343
344
	/**
345
	 * Drops column $sColumnName from table $sTableName
346
	 *
347
	 * @param string $sTableName table-name
348
	 * @param array $aTableDef eGW table-defintion
349
	 * @param string $sColumnName column-name
350
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
351
	 */
352
	function DropColumn($sTableName, $aTableDef, $sColumnName)
353
	{
354
		unset($aTableDef);	// not used, but required by function signature
355
356
		if (!($table_def = $this->GetTableDefinition($sTableName))) return 0;
357
		unset($table_def['fd'][$sColumnName]);
358
359
		$aSql = $this->dict->DropColumnSql($sTableName,$sColumnName,$ado_table=$this->_egw2adodb_columndef($table_def));
360
361
		return $this->ExecuteSQLArray($aSql,2,'DropColumnSQL(%1,%2,%3) sql=%4',False,$sTableName,$sColumnName,$ado_table,$aSql);
362
	}
363
364
	/**
365
	 * Renames table $sOldTableName to $sNewTableName
366
	 *
367
	 * @param string $sOldTableName old (existing) table-name
368
	 * @param string $sNewTableName new table-name
369
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
370
	 */
371
	function RenameTable($sOldTableName, $sNewTableName)
372
	{
373
		// if we have an old postgres sequence or index (the ones not linked to the table),
374
		// we create a new table, copy the content and drop the old one
375
		if ($this->sType == 'pgsql')
376
		{
377
			if (!($table_def = $this->GetTableDefinition($sOldTableName))) return 0;
378
379
			// only use old PostgreSQL stuff, if we have an old sequence, otherwise rely on it being new enough
380
			if ($this->_PostgresHasOldSequence($sOldTableName,True) &&
381
				(count($table_def['pk']) || count($table_def['ix']) || count($table_def['uc'])))
382
			{
383
				if ($this->adodb->BeginTrans() &&
384
					$this->CreateTable($sNewTableName,$table_def,True) &&
385
					$this->m_odb->query("INSERT INTO $sNewTableName SELECT * FROM $sOldTableName",__LINE__,__FILE__) &&
386
					// sequence must be updated, after inserts containing pkey, otherwise new inserst will fail!
387
					(count($table_def['pk']) !== 1 || $this->UpdateSequence($sNewTableName, $table_def['pk'][0])) &&
388
					$this->DropTable($sOldTableName))
389
				{
390
					$this->adodb->CommitTrans();
391
					return 2;
392
				}
393
				$this->adodb->RollbackTrans();
394
				return 0;
395
			}
396
		}
397
		$aSql = $this->dict->RenameTableSQL($sOldTableName, $sNewTableName);
398
399
		return $this->ExecuteSQLArray($aSql,2,'RenameTableSQL(%1,%2) sql=%3',False,$sOldTableName,$sNewTableName,$aSql);
400
	}
401
402
	/**
403
	 * Check if we have an old, not automaticaly droped sequence
404
	 *
405
	 * @param string $sTableName
406
	 * @param bool $preserveValue
407
	 * @return boolean|string sequence-name or false
408
	 */
409
	function _PostgresHasOldSequence($sTableName,$preserveValue=False)
410
	{
411
		if ($this->sType != 'pgsql') return false;
412
413
		$seq = $this->adodb->GetOne("SELECT d.adsrc FROM pg_attribute a, pg_class c, pg_attrdef d WHERE c.relname='$sTableName' AND c.oid=d.adrelid AND d.adsrc LIKE '%seq_$sTableName''::text)' AND a.attrelid=c.oid AND d.adnum=a.attnum");
414
		$seq2 = $this->adodb->GetOne("SELECT d.adsrc FROM pg_attribute a, pg_class c, pg_attrdef d WHERE c.relname='$sTableName' AND c.oid=d.adrelid AND d.adsrc LIKE '%$sTableName%_seq''::text)' AND a.attrelid=c.oid AND d.adnum=a.attnum");
415
416
		$matches = null;
417
		if ($seq && preg_match('/^nextval\(\'(.*)\'/',$seq,$matches))
418
		{
419
			if ($preserveValue) $this->pgsql_old_seq = $this->adodb->GetOne("SELECT last_value FROM " . $matches[1]);
420
			return $matches[1];
421
		}
422
		if ($seq2 && preg_match('/^nextval\(\'public\.(.*)\'/',$seq2,$matches))
423
		{
424
			if ($preserveValue) $this->pgsql_old_seq = $this->adodb->GetOne("SELECT last_value FROM " . $matches[1]);
425
			return $matches[1];
426
		}
427
		return false;
428
	}
429
430
	/**
431
	 * Check if we have an old, not automaticaly droped sequence and drop it
432
	 *
433
	 * @param $sTableName
434
	 */
435
	function _PostgresTestDropOldSequence($sTableName)
436
	{
437
		$this->pgsql_old_seq = 0;
438
		if ($this->sType == 'pgsql' && ($seq = $this->_PostgresHasOldSequence($sTableName)))
439
		{
440
			// only drop sequence, if there is no dependency on it
441
			if (!$this->adodb->GetOne("SELECT relname FROM pg_class JOIN pg_depend ON pg_class.relfilenode=pg_depend.objid WHERE relname='$seq' AND relkind='S' AND deptype='i'"))
442
			{
443
				$this->query('DROP SEQUENCE '.$seq,__LINE__,__FILE__);
0 ignored issues
show
Bug introduced by
Are you sure $seq of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

443
				$this->query('DROP SEQUENCE './** @scrutinizer ignore-type */ $seq,__LINE__,__FILE__);
Loading history...
444
			}
445
		}
446
	}
447
448
	/**
449
	 * Changes one (exiting) column in a table
450
	 *
451
	 * @param string $sTableName table-name
452
	 * @param string $sColumnName column-name
453
	 * @param array $aColumnDef new column-definition
454
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
455
	 */
456
	function AlterColumn($sTableName, $sColumnName, $aColumnDef)
457
	{
458
		if (!($table_def = $this->GetTableDefinition($sTableName))) return 0;
459
460
		// PostgreSQL: varchar or ascii column shortened, use substring to avoid error if current content is to long
461
		if($this->sType == 'pgsql' && in_array($table_def['fd'][$sColumnName]['type'], array('varchar', 'ascii')) &&
462
			in_array($aColumnDef['type'], array('varchar', 'ascii')) &&
463
			$table_def['fd'][$sColumnName]['precision'] > $aColumnDef['precision'])
464
		{
465
			$this->m_odb->update($sTableName, array(
466
				"$sColumnName=SUBSTRING($sColumnName FROM 1 FOR ".(int)$aColumnDef['precision'].')',
467
			), "LENGTH($sColumnName) > ".(int)$aColumnDef['precision'], __LINE__, __FILE__);
468
469
			if (($shortend = $this->m_odb->affected_rows()))
470
			{
471
				error_log(__METHOD__."('$sTableName', '$sColumnName', ".array2string($aColumnDef).") $shortend values shortened");
472
			}
473
		}
474
		$table_def['fd'][$sColumnName] = $aColumnDef;
475
476
		$aSql = $this->dict->AlterColumnSQL($sTableName,$ado_col = $this->_egw2adodb_columndef(array(
477
				'fd' => array($sColumnName => $aColumnDef),
478
				'pk' => array(),
479
			)),$ado_table=$this->_egw2adodb_columndef($table_def));
480
481
		return $this->ExecuteSQLArray($aSql,2,'AlterColumnSQL(%1,%2,%3) sql=%4',False,$sTableName,$ado_col,$ado_table,$aSql);
482
	}
483
484
	/**
485
	 * Renames column $sOldColumnName to $sNewColumnName in table $sTableName
486
	 *
487
	 * @param string $sTableName table-name
488
	 * @param string $sOldColumnName old (existing) column-name
489
	 * @param string $sNewColumnName new column-name
490
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
491
	 */
492
	function RenameColumn($sTableName, $sOldColumnName, $sNewColumnName)
493
	{
494
		$table_def = $this->GetTableDefinition($sTableName);
495
		$old_def = array();
496
497
		if (isset($table_def['fd'][$sOldColumnName]))
498
		{
499
			$old_def = $table_def['fd'][$sOldColumnName];
500
		}
501
		else
502
		{
503
			foreach($table_def['fd'] as $col => $def)
504
			{
505
				if (strtolower($col) == strtolower($sOldColumnName))
506
				{
507
					$old_def = $def;
508
					break;
509
				}
510
			}
511
		}
512
		$col_def = $this->_egw2adodb_columndef(array(
513
				'fd' => array($sNewColumnName => $old_def),
514
				'pk' => array(),
515
			));
516
517
		$aSql = $this->dict->RenameColumnSQL($sTableName,$sOldColumnName,$sNewColumnName,$col_def);
518
519
		return $this->ExecuteSQLArray($aSql,2,'RenameColumnSQL(%1,%2,%3) sql=%4',False,$sTableName,$sOldColumnName, $sNewColumnName,$aSql);
520
	}
521
522
	/**
523
	 * Add one (new) column to a table
524
	 *
525
	 * @param string $sTableName table-name
526
	 * @param string $sColumnName column-name
527
	 * @param array $aColumnDef column-definition
528
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
529
	 */
530
	function AddColumn($sTableName, $sColumnName, $aColumnDef)
531
	{
532
		$aSql = $this->dict->AddColumnSQL($sTableName,$ado_cols = $this->_egw2adodb_columndef(array(
533
				'fd' => array($sColumnName => $aColumnDef),
534
				'pk' => array(),
535
			)));
536
537
		return $this->ExecuteSQLArray($aSql,2,'AlterColumnSQL(%1,%2,%3) sql=%4',False,$sTableName,$sColumnName, $aColumnDef,$aSql);
538
	}
539
540
	/**
541
	 * Create an (unique) Index over one or more columns
542
	 *
543
	 * @param string $sTableName table-name
544
	 * @param string|array $aColumnNames column(s) for the index
545
	 * @param boolean $bUnique =false true for a unique index, default false
546
	 * @param array|string $options ='' db-sepecific options, default '' = none
547
	 * @param string $sIdxName ='' name of the index, if not given (default) its created automaticaly
548
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
549
	 */
550
	function CreateIndex($sTableName,$aColumnNames,$bUnique=false,$options='',$sIdxName='')
551
	{
552
		// remove length limits from column names, if DB type is NOT MySQL
553
		if ($this->sType != 'mysql')
554
		{
555
			$aColumnNames = preg_replace('/ *\(\d+\)$/','',$aColumnNames);
556
		}
557
		if (!$sIdxName || is_numeric($sIdxName))
558
		{
559
			$sIdxName = $this->_index_name($sTableName,$aColumnNames);
560
		}
561
		if (!is_array($options)) $options = $options ? array($options) : array();
562
		if ($bUnique) $options[] = 'UNIQUE';
563
564
		// if index already exists drop it first
565
		$definition = array();
566
		$this->GetIndexes($sTableName, $definition);
567
		$type = $bUnique ? 'uc' : 'ix';
568
		if ($this->_in_index($aColumnNames, $definition[$type], true) ||
569
			// sometimes index is listed as unique index too --> ignore that
570
			($type == 'ix' && $this->_in_index($aColumnNames, $definition['uc'], true)))
571
		{
572
			//error_log(__METHOD__."('$sTableName', ['".implode("','", (array)$aColumnNames)."'], $bUnique, ...) already exists --> droping it first");
573
			$this->DropIndex($sTableName, (array)$aColumnNames);
574
		}
575
576
		$aSql = $this->dict->CreateIndexSQL($sIdxName,$sTableName,$aColumnNames,$options);
577
578
		return $this->ExecuteSQLArray($aSql,2,'CreateIndexSQL(%1,%2,%3,%4) sql=%5',False,$sTableName,$aColumnNames,$options,$sIdxName,$aSql);
579
	}
580
581
	/**
582
	 * Drop an Index
583
	 *
584
	 * @param string $sTableName table-name
585
	 * @param array|string $aColumnNames columns of the index or the name of the index
586
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
587
	 */
588
	function DropIndex($sTableName,$aColumnNames)
589
	{
590
		if (is_array($aColumnNames))
591
		{
592
			$indexes = $this->dict->MetaIndexes($sTableName);
593
594
			if ($indexes === False)
595
			{
596
				// if MetaIndexes is not availible for the DB, we try the name the index was created with
597
				// this fails if one of the columns have been renamed
598
				$sIdxName = $this->_index_name($sTableName,$aColumnNames);
599
			}
600
			else
601
			{
602
				foreach($indexes as $idx => $idx_data)
603
				{
604
					if (strtolower(implode(':',$idx_data['columns'])) == implode(':',$aColumnNames))
605
					{
606
						$sIdxName = $idx;
607
						break;
608
					}
609
				}
610
			}
611
		}
612
		else
613
		{
614
			$sIdxName = $aColumnNames;
615
		}
616
		if(!$sIdxName)
617
		{
618
			return True;
619
		}
620
		$aSql = $this->dict->DropIndexSQL($sIdxName,$sTableName);
621
622
		return $this->ExecuteSQLArray($aSql,2,'DropIndexSQL(%1(%2),%3) sql=%4',False,$sIdxName,$aColumnNames,$sTableName,$aSql);
623
	}
624
625
	/**
626
	 * Updating the sequence-value, after eg. copying data via RefreshTable
627
	 * @param string $sTableName table-name
628
	 * @param string $sColumnName column-name, which default is set to nextval()
629
	 */
630
	function UpdateSequence($sTableName,$sColumnName)
631
	{
632
		switch($this->sType)
633
		{
634
			case 'pgsql':
635
				// identify the sequence name, ADOdb uses a different name or it might be renamed
636
				$columns = $this->dict->MetaColumns($sTableName);
637
				$seq_name = 'seq_'.$sTableName;
638
				$matches = null;
639
				if (preg_match("/nextval\('([^']+)'::(text|regclass)\)/",$columns[strtoupper($sColumnName)]->default_value,$matches))
640
				{
641
					$seq_name = $matches[1];
642
				}
643
				$sql = "SELECT setval('$seq_name',MAX($sColumnName)) FROM $sTableName";
644
				if($this->debug) { echo "<br>Updating sequence '$seq_name using: $sql"; }
645
				return $this->query($sql,__LINE__,__FILE__);
646
		}
647
		return True;
648
	}
649
650
	/**
651
	 * This function manually re-created the table incl. primary key and all other indices
652
	 *
653
	 * It is meant to use if the primary key, existing indices or column-order changes or
654
	 * columns are not longer used or new columns need to be created (with there default value or NULL)
655
	 * Beside the default-value in the schema, one can give extra defaults via $aDefaults to eg. use an
656
	 * other colum or function to set the value of a new or changed column
657
	 *
658
	 * @param string $sTableName table-name
659
	 * @param array $aTableDef eGW table-defintion
660
	 * @param array|boolean $aDefaults array with default for the colums during copying, values are either (old) column-names or quoted string-literals
661
	 */
662
	function RefreshTable($sTableName, $aTableDef, $aDefaults=False)
663
	{
664
		if($this->debug) { echo "<p>schema_proc::RefreshTable('$sTableName',"._debug_array($aTableDef,False).")\n"; }
665
666
		$old_table_def = $this->GetTableDefinition($sTableName);
667
668
		$tmp_name = 'tmp_'.$sTableName;
669
		$this->m_odb->transaction_begin();
670
671
		$select = array();
672
		$blob_column_included = $auto_column_included = False;
673
		foreach($aTableDef['fd'] as $name => $data)
674
		{
675
			// new auto column with no default or explicit NULL as default (can be an existing column too!)
676
			if ($data['type'] == 'auto' &&
677
				(!isset($old_table_def['fd'][$name]) && (!$aDefaults || !isset($aDefaults[$name])) ||
678
				$aDefaults && strtoupper($aDefaults[$name]) == 'NULL'))
679
			{
680
				$sequence_name = $sTableName.'_'.$name.'_seq';
681
				switch($GLOBALS['egw_setup']->db->Type)
682
				{
683
					case 'mysql':
684
						$value = 'NULL'; break;
685
					case 'pgsql':
686
						$value = "nextval('$sequence_name'::regclass)"; break;
687
					default:
688
						$value = "nextval('$sequence_name')"; break;
689
				}
690
			}
691
			elseif ($aDefaults && isset($aDefaults[$name]))	// use given default
692
			{
693
				$value = $aDefaults[$name];
694
			}
695
			elseif (isset($old_table_def['fd'][$name]))	// existing column, use its value => column-name in query
696
			{
697
				$value = $name;
698
699
				// varchar or ascii column shortened, use substring to avoid error if current content is to long
700
				if(in_array($old_table_def['fd'][$name]['type'], array('varchar', 'ascii')) &&
701
					in_array($data['type'], array('varchar', 'ascii')) &&
702
					$old_table_def['fd'][$name]['precision'] > $data['precision'])
703
				{
704
					$value = "SUBSTRING($value FROM 1 FOR ".(int)$data['precision'].')';
705
				}
706
				if ($this->sType == 'pgsql')			// some postgres specific code
707
				{
708
					// this is eg. necessary to change a varchar into an int column under postgres
709
					if (in_array($old_table_def['fd'][$name]['type'],array('char','varchar','text','blob')) &&
710
						in_array($data['type'],array('int','decimal')))
711
					{
712
						$value = "to_number($name,'S9999999999999D99')";
713
					}
714
					// blobs cant be casted to text
715
					elseif($old_table_def['fd'][$name]['type'] == 'blob' && $data['type'] == 'text')
716
					{
717
						$value = "ENCODE($value,'escape')";
718
					}
719
					// cast everything which is a different type
720
					elseif($old_table_def['fd'][$name]['type'] != $data['type'] && ($type_translated = $this->TranslateType($data['type'])))
721
					{
722
						$value = "CAST($value AS $type_translated)";
723
					}
724
				}
725
			}
726
			else	// new column => use default value or NULL
727
			{
728
				if (!isset($data['default']) && (!isset($data['nullable']) || $data['nullable']))
729
				{
730
					$value = 'NULL';
731
				}
732
				// some stuff is NOT to be quoted
733
				elseif (in_array(strtoupper($data['default']),array('CURRENT_TIMESTAMP','CURRENT_DATE','NULL','NOW()')))
734
				{
735
					$value = $data['default'];
736
				}
737
				else
738
				{
739
					$value = $this->m_odb->quote(isset($data['default']) ? $data['default'] : '',$data['type']);
740
				}
741
				if ($this->sType == 'pgsql')
742
				{
743
					// fix for postgres error "no '<' operator for type 'unknown'"
744
					if(($type_translated = $this->TranslateType($data['type'])))
745
					{
746
						$value = "CAST($value AS $type_translated)";
747
					}
748
				}
749
			}
750
			$blob_column_included = $blob_column_included || in_array($data['type'],array('blob','text','longtext'));
751
			$auto_column_included = $auto_column_included || $data['type'] == 'auto';
752
			$select[] = $value;
753
		}
754
755
		$extra = '';
756
		$distinct = 'DISTINCT';
757
		switch($this->sType)
758
		{
759
			case 'mssql':
760
				if ($auto_column_included) $extra = "SET IDENTITY_INSERT $sTableName ON\n";
761
				if ($blob_column_included) $distinct = '';	// no distinct on blob-columns
762
				break;
763
		}
764
		// because of all the trouble with sequences and indexes in the global namespace,
765
		// we use an additional temp. table for postgres and not rename the existing one, but drop it.
766
		if ($this->sType == 'pgsql')
767
		{
768
			$Ok = $this->m_odb->query("SELEcT * INTO TEMPORARY TABLE $tmp_name FROM $sTableName",__LINE__,__FILE__) &&
769
				$this->DropTable($sTableName);
770
		}
771
		else
772
		{
773
			// drop evtl. existing temp. table eg. from a previous failed upgrade
774
			if (($tables = $this->m_odb->table_names(true)) && in_array($tmp_name, $tables))
775
			{
776
				$this->DropTable($tmp_name);
777
			}
778
			$Ok = $this->RenameTable($sTableName,$tmp_name);
779
		}
780
		$Ok = $Ok && $this->CreateTable($sTableName,$aTableDef) &&
781
			$this->m_odb->query($sql_copy_data="$extra INSERT INTO $sTableName (".
782
				implode(',',array_keys($aTableDef['fd'])).
783
				") SELEcT $distinct ".implode(',',$select)." FROM $tmp_name",__LINE__,__FILE__) &&
784
			$this->DropTable($tmp_name);
785
		//error_log($sql_copy_data);
786
787
		if (!$Ok)
788
		{
789
			$this->m_odb->transaction_abort();
790
			return False;
791
		}
792
		// do we need to update the new sequences value ?
793
		if (count($aTableDef['pk']) == 1 && $aTableDef['fd'][$aTableDef['pk'][0]]['type'] == 'auto')
794
		{
795
			$this->UpdateSequence($sTableName,$aTableDef['pk'][0]);
796
		}
797
		$this->m_odb->transaction_commit();
798
799
		return True;
800
	}
801
802
	/**
803
	 * depricated Function does nothing any more
804
	 * @depricated
805
	 */
806
	function GenerateScripts()
807
	{
808
		return True;
809
	}
810
811
	/**
812
	 * Creates all tables for one application
813
	 *
814
	 * @param array $aTables array of eGW table-definitions
815
	 * @param boolean $bOutputHTML =false should we give diagnostics, default False
816
	 * @return boolean True on success, False if an (fatal) error occured
817
	 */
818
	function ExecuteScripts($aTables, $bOutputHTML=False)
819
	{
820
		if(!is_array($aTables) || !IsSet($this->m_odb))
0 ignored issues
show
introduced by
The condition is_array($aTables) is always true.
Loading history...
821
		{
822
			return False;
823
		}
824
		// set our debug-mode or $bOutputHTML is the other one is set
825
		if ($this->debug) $bOutputHTML = True;
826
		if ($bOutputHTML && !$this->debug) $this->debug = 2;
827
828
		foreach($aTables as $sTableName => $aTableDef)
829
		{
830
			if($this->CreateTable($sTableName, $aTableDef))
831
			{
832
				if($bOutputHTML)
833
				{
834
					echo '<br>Create Table <b>' . $sTableName . '</b>';
835
				}
836
			}
837
			else
838
			{
839
				if($bOutputHTML)
840
				{
841
					echo '<br>Create Table Failed For <b>' . $sTableName . '</b>';
842
				}
843
844
				return False;
845
			}
846
		}
847
		return True;
848
	}
849
850
	/**
851
	* Return the value of a column
852
	*
853
	* @param string|integer $value name of field or positional index starting from 0
854
	* @param bool $strip_slashes string escape chars from field(optional), default false
855
	* @deprecated use result-set returned by query/select
856
	* @return string the field value
857
	*/
858
	function f($value,$strip_slashes=False)
859
	{
860
		if (!($this->m_odb instanceof Deprecated))
861
		{
862
	    	throw new Api\Exception\AssertionFailed(__METHOD__.' requires an EGroupware\Api\Db\Deprecated object!');
863
		}
864
		return $this->m_odb->f($value,$strip_slashes);
0 ignored issues
show
Deprecated Code introduced by
The function EGroupware\Api\Db\Deprecated::f() has been deprecated: use the result-object returned by query() or select() direct, so you can use the global db-object and not a clone ( Ignorable by Annotation )

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

864
		return /** @scrutinizer ignore-deprecated */ $this->m_odb->f($value,$strip_slashes);

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

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

Loading history...
865
	}
866
867
	/**
868
	* Number of rows in current result set
869
	*
870
	* @deprecated use result-set returned by query/select
871
	* @return int number of rows
872
	*/
873
	function num_rows()
874
	{
875
		if (!($this->m_odb instanceof Deprecated))
876
		{
877
	    	throw new Api\Exception\AssertionFailed(__METHOD__.' requires an EGroupware\Api\Db\Deprecated object!');
878
		}
879
		return $this->m_odb->num_rows();
0 ignored issues
show
Deprecated Code introduced by
The function EGroupware\Api\Db\Deprecated::num_rows() has been deprecated: use the result-object returned by query/select()->NumRows(), so you can use the global db-object and not a clone ( Ignorable by Annotation )

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

879
		return /** @scrutinizer ignore-deprecated */ $this->m_odb->num_rows();

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

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

Loading history...
880
	}
881
882
	/**
883
	* Move to the next row in the results set
884
	*
885
	* @deprecated use result-set returned by query/select
886
	* @return bool was another row found?
887
	*/
888
	function next_record()
889
	{
890
		if (!($this->m_odb instanceof Deprecated))
891
		{
892
	    	throw new Api\Exception\AssertionFailed(__METHOD__.' requires an EGroupware\Api\Db\Deprecated object!');
893
		}
894
		return $this->m_odb->next_record();
0 ignored issues
show
Deprecated Code introduced by
The function EGroupware\Api\Db\Deprecated::next_record() has been deprecated: use foreach(query() or foreach(select() to loop over the query using the global db object ( Ignorable by Annotation )

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

894
		return /** @scrutinizer ignore-deprecated */ $this->m_odb->next_record();

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

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

Loading history...
895
	}
896
897
	/**
898
	* Execute a query
899
	*
900
	* @param string $Query_String the query to be executed
901
	* @param mixed $line the line method was called from - use __LINE__
902
	* @param string $file the file method was called from - use __FILE__
903
	* @param int $offset row to start from
904
	* @param int $num_rows number of rows to return (optional), if unset will use $GLOBALS['phpgw_info']['user']['preferences']['common']['maxmatchs']
905
	* @return ADORecordSet or false, if the query fails
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Db\ADORecordSet was not found. Did you mean ADORecordSet? If so, make sure to prefix the type with \.
Loading history...
906
	*/
907
	function query($sQuery, $line='', $file='')
908
	{
909
		return $this->m_odb->query($sQuery, $line, $file);
910
	}
911
912
	/**
913
	* Insert a row of data into a table or updates it if $where is given, all data is quoted according to it's type
914
	*
915
	* @param string $table name of the table
916
	* @param array $data with column-name / value pairs
917
	* @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
918
	*	if the row exists db::update is called else a new row with $date merged with $where gets inserted (data has precedence)
919
	* @param int $line line-number to pass to query
920
	* @param string $file file-name to pass to query
921
	* @param string $app=false string with name of app, this need to be set in setup anyway!!!
922
	* @return ADORecordSet or false, if the query fails
923
	*/
924
	function insert($table,$data,$where,$line,$file,$app=False,$use_prepared_statement=false)
925
	{
926
		return $this->m_odb->insert($table,$data,$where,$line,$file,$app,$use_prepared_statement);
927
	}
928
929
	/**
930
	 * Execute the Sql statements in an array and give diagnostics, if any error occures
931
	 *
932
	 * @param array $aSql array of SQL strings to execute
933
	 * @param int $debug_level for which debug_level (and higher) should the diagnostics always been printed
934
	 * @param string $debug variable number of arguments for the debug_message functions in case of an error
935
	 * @return int 2: no error, 1: errors, but continued, 0: errors aborted
936
	 */
937
	function ExecuteSqlArray($aSql,$debug_level)
938
	{
939
		if ($this->m_odb->query_log)	// we use Db::query to log the queries
940
		{
941
			$retval = 2;
942
			foreach($aSql as $sql)
943
			{
944
				if (!$this->m_odb->query($sql,__LINE__,__FILE__))
945
				{
946
					$retval = 1;
947
				}
948
			}
949
		}
950
		else
951
		{
952
			$retval = $this->dict->ExecuteSQLArray($aSql);
953
		}
954
		if ($retval < 2 || $this->debug >= $debug_level || $this->debug > 3)
955
		{
956
			$debug_params = func_get_args();
957
			array_shift($debug_params);
958
			array_shift($debug_params);
959
			call_user_func_array(array($this,'debug_message'),$debug_params);
960
			if ($retval < 2 && !$this->dict->debug)
961
			{
962
				echo '<p><b>'.$this->adodb->ErrorMsg()."</b></p>\n";
963
			}
964
		}
965
		return $retval;
966
	}
967
968
	/**
969
	 * Created a (unique) name for an index
970
	 *
971
	 * As the length of the index name is limited on some databases, we use two algorithms:
972
	 * a) we use just the first column-name with and added _2, _3, ... if more indexes uses that column
973
	 * b) we use the table-names plus all column-names and remove dublicate parts
974
	 *
975
	 * @internal
976
	 * @param $sTableName string name of the table
977
	 * @param $aColumnNames array of column-names or string with a single column-name
978
	 * @return string the index-name
979
	 */
980
	function _index_name($sTableName,$aColumnNames)
981
	{
982
		// this code creates extrem short index-names, eg. for MaxDB
983
//			if (isset($this->max_index_length[$this->sType]) && $this->max_index_length[$this->sType] <= 32)
984
//			{
985
//				static $existing_indexes=array();
986
//
987
//				if (!isset($existing_indexes[$sTableName]) && method_exists($this->adodb,'MetaIndexes'))
988
//				{
989
//					$existing_indexes[$sTableName] = $this->adodb->MetaIndexes($sTableName);
990
//				}
991
//				$i = 0;
992
//				$firstCol = is_array($aColumnNames) ? $aColumnNames[0] : $aColumnNames;
993
//				do
994
//				{
995
//					++$i;
996
//					$name = $firstCol . ($i > 1  ? '_'.$i : '');
997
//				}
998
//				while (isset($existing_indexes[$sTableName][$name]) || isset($existing_indexes[strtoupper($sTableName)][strtoupper($name)]));
999
//
1000
//				$existing_indexes[$sTableName][$name] = True;	// mark it as existing now
1001
//
1002
//				return $name;
1003
//			}
1004
		// This code creates longer index-names incl. the table-names and the used columns
1005
		$table = str_replace(array('phpgw_','egw_'),'',$sTableName);
1006
		// if the table-name or a part of it is repeated in the column-name, remove it there
1007
		$remove[] = $table.'_';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$remove was never initialized. Although not strictly required by PHP, it is generally a good practice to add $remove = array(); before regardless.
Loading history...
1008
		// also remove 3 or 4 letter shortcuts of the table- or app-name
1009
		$remove[] = substr($table,0,3).'_';
1010
		$remove[] = substr($table,0,4).'_';
1011
		// if the table-name consists of '_' limtied parts, remove occurences of these parts too
1012
		foreach (explode('_',$table) as $part)
1013
		{
1014
			$remove[] = $part.'_';
1015
		}
1016
		$cols = str_replace($remove,'',$aColumnNames);
1017
1018
		$name = $sTableName.'_'.(is_array($cols) ? implode('_',$cols) : $cols);
1019
		// remove length limits from column names
1020
		$name = preg_replace('/ *\(\d+\)/','',$name);
1021
1022
		// this code creates a fixed short index-names (30 chars) from the long and unique name, eg. for MaxDB or Oracle
1023
		if (isset($this->max_index_length[$this->sType]) && $this->max_index_length[$this->sType] <= 32 && strlen($name) > 30 ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $this->max... || strlen($name) >= 64, Probably Intended Meaning: IssetNode && $this->max_...|| strlen($name) >= 64)
Loading history...
1024
			strlen($name) >= 64)	// even mysql has a limit here ;-)
1025
		{
1026
			$name = "i".substr(md5($name),0,29);
1027
		}
1028
		return $name;
1029
	}
1030
1031
	/**
1032
	 * Giving a non-fatal error-message
1033
	 */
1034
	function error($str)
1035
	{
1036
		echo "<p><b>Error:</b> $str</p>";
1037
	}
1038
1039
	/**
1040
	 * Giving a fatal error-message and exiting
1041
	 */
1042
	function fatal($str)
1043
	{
1044
		echo "<p><b>Fatal Error:</b> $str</p>";
1045
		exit;
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...
1046
	}
1047
1048
	/**
1049
	 * Gives out a debug-message with certain parameters
1050
	 *
1051
	 * All permanent debug-messages in the calendar should be done by this function !!!
1052
	 *	(In future they may be logged or sent as xmlrpc-faults back.)
1053
	 *
1054
	 * Permanent debug-message need to make sure NOT to give secret information like passwords !!!
1055
	 *
1056
	 * This function do NOT honor the setting of the debug variable, you may use it like
1057
	 * if ($this->debug > N) $this->debug_message('Error ;-)');
1058
	 *
1059
	 * The parameters get formated depending on their type.
1060
	 *
1061
	 * @param $msg string message with parameters/variables like lang(), eg. '%1'
1062
	 * @param $backtrace include a function-backtrace, default True=On
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Db\include 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...
1063
	 *	should only be set to False=Off, if your code ensures a call with backtrace=On was made before !!!
1064
	 * @param $param mixed a variable number of parameters, to be inserted in $msg
1065
	 *	arrays get serialized with print_r() !
1066
	 */
1067
	function debug_message($msg,$backtrace=True)
1068
	{
1069
		for($i = 2; $i < func_num_args(); ++$i)
1070
		{
1071
			$param = func_get_arg($i);
1072
1073
			if (is_null($param))
1074
			{
1075
				$param='NULL';
1076
			}
1077
			else
1078
			{
1079
				switch(gettype($param))
1080
				{
1081
					case 'string':
1082
						$param = "'$param'";
1083
						break;
1084
					case 'array':
1085
					case 'object':
1086
						$content = @current($param);
1087
						$do_pre = is_array($param) ? count($param) > 6 || is_array($content)&&count($content) : True;
1088
						$param = ($do_pre ? '<pre>' : '').print_r($param,True).($do_pre ? '</pre>' : '');
1089
						break;
1090
					case 'boolean':
1091
						$param = $param ? 'True' : 'False';
1092
						break;
1093
				}
1094
			}
1095
			$msg = str_replace('%'.($i-1),$param,$msg);
1096
		}
1097
		echo '<p>'.$msg."<br>\n".($backtrace ? 'Backtrace: '.function_backtrace(1)."</p>\n" : '');
1098
	}
1099
1100
	/**
1101
	 * Converts an eGW table-definition array into an ADOdb column-definition string
1102
	 *
1103
	 * @internal
1104
	 * @param array $aTableDef eGW table-defintion
1105
	 * @return string ADOdb column-definition string (comma separated)
1106
	 */
1107
	function _egw2adodb_columndef($aTableDef)
1108
	{
1109
		$ado_defs = array();
1110
		foreach($aTableDef['fd'] as $col => $col_data)
1111
		{
1112
			$ado_col = False;
1113
1114
			switch($col_data['type'])
1115
			{
1116
				case 'auto':
1117
					$ado_col = 'I AUTOINCREMENT NOTNULL';
1118
					unset($col_data['nullable']);	// else we set it twice
1119
					break;
1120
				case 'binary':	// varbinary column for MySQL/MariaDB
1121
					if ($this->sType == 'mysql' && $col_data['precision'] <= $this->max_varchar_length)
1122
					{
1123
						$ado_col = 'C('.$col_data['precision'].') CONSTRAINT "CHARACTER SET binary"';
1124
						break;
1125
					}
1126
					// fall through to blob
1127
				case 'blob':
1128
					$ado_col = 'B';
1129
					break;
1130
				case 'bool':
1131
					$ado_col = 'L';
1132
					break;
1133
				case 'char':
1134
					// ADOdb does not differ between char and varchar
1135
				case 'ascii':
1136
				case 'varchar':
1137
					$ado_col = "C";
1138
					if(0 < $col_data['precision'] && $col_data['precision'] <= $this->max_varchar_length)
1139
					{
1140
						$ado_col .= "($col_data[precision])";
1141
					}
1142
					if($col_data['precision'] > $this->max_varchar_length)
1143
					{
1144
						$ado_col = 'X';
1145
					}
1146
					if ($col_data['type'] == 'ascii' && $this->sType == 'mysql')
1147
					{
1148
						$ado_col .= ' CONSTRAINT "CHARACTER SET ascii"';
1149
					}
1150
					break;
1151
				case 'date':
1152
					$ado_col = 'D';
1153
					// allow to use now() beside current_date, as Postgres backups contain it and it's easier to remember anyway
1154
					if (in_array($col_data['default'],array('current_date','now()')))
1155
					{
1156
						$ado_col .= ' DEFDATE';
1157
						unset($col_data['default']);
1158
					}
1159
					break;
1160
				case 'decimal':
1161
					$ado_col = "N($col_data[precision].$col_data[scale])";
1162
					break;
1163
				case 'double':
1164
				case 'float':
1165
					// ADOdb does not differ between float and double
1166
					$ado_col = 'F';
1167
					break;
1168
				case 'int':
1169
					$ado_col = 'I';
1170
					switch($col_data['precision'])
1171
					{
1172
						case 1:
1173
						case 2:
1174
						case 4:
1175
						case 8:
1176
							$ado_col .= $col_data['precision'];
1177
							break;
1178
					}
1179
					break;
1180
				case 'longtext':
1181
					$ado_col = 'XL';
1182
					break;
1183
				case 'text':
1184
					$ado_col = 'X';
1185
					break;
1186
				case 'timestamp':
1187
					$ado_col = 'T';
1188
					// allow to use now() beside current_timestamp, as Postgres backups contain it and it's easier to remember anyway
1189
					if (in_array($col_data['default'],array('current_timestamp','now()')))
1190
					{
1191
						$ado_col .= ' DEFTIMESTAMP';
1192
						unset($col_data['default']);
1193
					}
1194
					break;
1195
			}
1196
			if (!$ado_col)
1197
			{
1198
				$this->error("Ignoring unknown column-type '$col_data[type]($col_data[precision])' !!!<br>".function_backtrace());
1199
				continue;
1200
			}
1201
			if (isset($col_data['nullable']) && !$col_data['nullable'])
1202
			{
1203
				$ado_col .= ' NOTNULL';
1204
			}
1205
			if (isset($col_data['default']))
1206
			{
1207
				$ado_col .= (in_array($col_data['type'],array('bool','int','decimal','float','double')) && $col_data['default'] != 'NULL' ? ' NOQUOTE' : '').
1208
					' DEFAULT '.$this->m_odb->quote($col_data['default'],$col_data['type']);
1209
			}
1210
			if (in_array($col,$aTableDef['pk']))
1211
			{
1212
				$ado_col .= ' PRIMARY';
1213
			}
1214
			$ado_defs[] = $col . ' ' . $ado_col;
1215
		}
1216
		//print_r($aTableDef); echo implode(",\n",$ado_defs)."\n";
1217
		return implode(",\n",$ado_defs);
1218
	}
1219
1220
	/**
1221
	 * Translates an eGW type into the DB's native type
1222
	 *
1223
	 * @param string $egw_type eGW name of type
1224
	 * @param string|boolean DB's name of the type or false if the type could not be identified (should not happen)
0 ignored issues
show
Documentation Bug introduced by
The doc comment DB's at position 0 could not be parsed: Unknown type name 'DB's' at position 0 in DB's.
Loading history...
1225
	 */
1226
	function TranslateType($egw_type)
1227
	{
1228
		$ado_col = $this->_egw2adodb_columndef(array(
1229
			'fd' => array('test' => array('type' => $egw_type)),
1230
			'pk' => array(),
1231
		));
1232
		$matches = null;
1233
		return preg_match('/test ([A-Z0-9]+)/i',$ado_col,$matches) ? $this->dict->ActualType($matches[1]) : false;
1234
	}
1235
1236
	/**
1237
	 * Read the table-definition direct from the database
1238
	 *
1239
	 * The definition might not be as accurate, depending on the DB!
1240
	 *
1241
	 * @param string $sTableName table-name
1242
	 * @return array|boolean table-defition, like $phpgw_baseline[$sTableName] after including tables_current, or false on error
1243
	 */
1244
	function GetTableDefinition($sTableName)
1245
	{
1246
		// MetaType returns all varchar >= blobSize as blob, it's by default 100, which is wrong
1247
		$this->dict->blobSize = $this->max_varchar_length;
1248
1249
		if (!method_exists($this->dict,'MetaColumns') ||
1250
			!($columns = $this->dict->MetaColumns($sTableName)))
1251
		{
1252
			return False;
1253
		}
1254
		$definition = array(
1255
			'fd' => array(),
1256
			'pk' => array(),
1257
			'fk' => array(),
1258
			'ix' => array(),
1259
			'uc' => array(),
1260
		);
1261
		//echo "$sTableName: <pre>".print_r($columns,true)."</pre>";
1262
		foreach($columns as $column)
1263
		{
1264
			$name = $this->capabilities['name_case'] == 'upper' ? strtolower($column->name) : $column->name;
1265
1266
			$type = method_exists($this->dict,'MetaType') ? $this->dict->MetaType($column) : strtoupper($column->type);
1267
1268
			// fix longtext not correctly handled by ADOdb
1269
			if ($type == 'X' && $column->type == 'longtext') $type = 'XL';
1270
1271
			static $ado_type2egw = array(
1272
				'C'		=> 'varchar',
1273
				'C2'	=> 'varchar',
1274
				'X'		=> 'text',
1275
				'X2'	=> 'text',
1276
				'XL'	=> 'longtext',
1277
				'B'		=> 'blob',
1278
				'I'		=> 'int',
1279
				'T'		=> 'timestamp',
1280
				'D'		=> 'date',
1281
				'F'		=> 'float',
1282
				'N'		=> 'decimal',
1283
				'R'		=> 'auto',
1284
				'L'		=> 'bool',
1285
			);
1286
			$definition['fd'][$name]['type'] = $ado_type2egw[$type];
1287
1288
			switch($type)
1289
			{
1290
				case 'D': case 'T':
1291
					// detecting the automatic timestamps again
1292
					if ($column->has_default && preg_match('/(0000-00-00|timestamp)/i',$column->default_value))
1293
					{
1294
						$column->default_value = $type == 'D' ? 'current_date' : 'current_timestamp';
1295
					}
1296
					break;
1297
				case 'C': case 'C2':
1298
					// ascii columns are reported as varchar
1299
					$definition['fd'][$name]['type'] = $this->m_odb->get_column_attribute($name, $sTableName, true, 'type') === 'ascii' ?
1300
						'ascii' : 'varchar';
1301
					$definition['fd'][$name]['precision'] = $column->max_length;
1302
					break;
1303
				case 'B':
1304
				case 'X': case 'XL': case 'X2':
1305
					// text or blob's need to be nullable for most databases
1306
					$column->not_null = false;
1307
					break;
1308
				case 'F':
1309
					$definition['fd'][$name]['precision'] = $column->max_length;
1310
					break;
1311
				case 'N':
1312
					$definition['fd'][$name]['precision'] = $column->max_length;
1313
					$definition['fd'][$name]['scale'] = $column->scale;
1314
					break;
1315
				case 'R':
1316
					$column->auto_increment = true;
1317
					// fall-through
1318
				case 'I': case 'I1': case 'I2': case 'I4': case 'I8':
1319
					switch($type)
1320
					{
1321
						case 'I1': case 'I2': case 'I4': case 'I8':
1322
							$definition['fd'][$name]['precision'] = (int) $type[1];
1323
							break;
1324
						default:
1325
							if ($column->max_length > 11)
1326
							{
1327
								$definition['fd'][$name]['precision'] = 8;
1328
							}
1329
							elseif ($column->max_length > 6 || !$column->max_length)
1330
							{
1331
								$definition['fd'][$name]['precision'] = 4;
1332
							}
1333
							elseif ($column->max_length > 2)
1334
							{
1335
								$definition['fd'][$name]['precision'] = 2;
1336
							}
1337
							else
1338
							{
1339
								$definition['fd'][$name]['precision'] = 1;
1340
							}
1341
							break;
1342
					}
1343
					if ($column->auto_increment)
1344
					{
1345
						// no precision for auto!
1346
						$definition['fd'][$name] = array(
1347
							'type' => 'auto',
1348
							'nullable' => False,
1349
						);
1350
						$column->has_default = False;
1351
						$definition['pk'][] = $name;
1352
					}
1353
					else
1354
					{
1355
						$definition['fd'][$name]['type'] = 'int';
1356
						// detect postgres type-spec and remove it
1357
						$matches = null;
1358
						if ($this->sType == 'pgsql' && $column->has_default && preg_match('/\(([^)])\)::/',$column->default_value,$matches))
1359
						{
1360
							$definition['fd'][$name]['default'] = $matches[1];
1361
							$column->has_default = False;
1362
						}
1363
					}
1364
					// fix MySQL stores bool columns as smallint
1365
					if ($this->sType == 'mysql' && $definition['fd'][$name]['precision'] == 1 &&
1366
						$this->m_odb->get_column_attribute($name, $sTableName, true, 'type') === 'bool')
1367
					{
1368
						$definition['fd'][$name]['type'] = 'bool';
1369
						unset($definition['fd'][$name]['precision']);
1370
						$column->default_value = (bool)$column->default_value;
1371
					}
1372
					break;
1373
			}
1374
			if ($column->has_default)
1375
			{
1376
				if (preg_match("/^'(.*)'::.*$/",$column->default_value,$matches))	// postgres
1377
				{
1378
					$column->default_value = $matches[1];
1379
				}
1380
				$definition['fd'][$name]['default'] = $column->default_value;
1381
			}
1382
			if ($column->not_null)
1383
			{
1384
				$definition['fd'][$name]['nullable'] = False;
1385
			}
1386
			if ($column->primary_key && !in_array($name,$definition['pk']))
1387
			{
1388
				$definition['pk'][] = $name;
1389
			}
1390
		}
1391
		if ($this->debug > 2) $this->debug_message("schema_proc::GetTableDefintion: MetaColumns(%1) = %2",False,$sTableName,$columns);
1392
1393
		// not all DB's (odbc) return the primary keys via MetaColumns
1394
		if (!count($definition['pk']) && method_exists($this->dict,'MetaPrimaryKeys') &&
1395
			is_array($primary = $this->dict->MetaPrimaryKeys($sTableName)) && count($primary))
1396
		{
1397
			if($this->capabilities['name_case'] == 'upper')
1398
			{
1399
				array_walk($primary, function(&$s)
1400
				{
1401
					$s = strtolower($s);
1402
				});
1403
			}
1404
			$definition['pk'] = $primary;
1405
		}
1406
		if ($this->debug > 1) $this->debug_message("schema_proc::GetTableDefintion: MetaPrimaryKeys(%1) = %2",False,$sTableName,$primary);
1407
1408
		$this->GetIndexes($sTableName, $definition);
1409
		if ($this->debug > 1) $this->debug_message("schema_proc::GetTableDefintion(%1) = %2",False,$sTableName,$definition);
1410
1411
		return $definition;
1412
	}
1413
1414
	/**
1415
	 * Query indexes (not primary index) from database
1416
	 *
1417
	 * @param string $sTableName
1418
	 * @param array& $definition=array()
1419
	 * @return array of arrays with keys 'ix' and 'uc'
1420
	 */
1421
	public function GetIndexes($sTableName, array &$definition=array())
1422
	{
1423
		if (method_exists($this->dict,'MetaIndexes') &&
1424
			is_array($indexes = $this->dict->MetaIndexes($sTableName)) && count($indexes))
1425
		{
1426
			foreach($indexes as $index)
1427
			{
1428
				// append (optional) length of index in brackets to column
1429
				foreach((array)$index['length'] as $col => $length)
1430
				{
1431
					if (($key = array_search($col, $index['columns']))) $index['columns'][$key] .= '('.$length.')';
1432
				}
1433
				if($this->capabilities['name_case'] == 'upper')
1434
				{
1435
					array_walk($index['columns'], function(&$s)
1436
					{
1437
						$s = strtolower($s);
1438
					});
1439
				}
1440
				if (!empty($definition['pk']) && (implode(':',$definition['pk']) == implode(':',$index['columns']) ||
1441
					$index['unique'] && count(array_intersect($definition['pk'],$index['columns'])) == count($definition['pk'])))
1442
				{
1443
					continue;	// is already the primary key => ignore it
1444
				}
1445
				$kind = $index['unique'] ? 'uc' : 'ix';
1446
1447
				$definition[$kind][] = count($index['columns']) > 1 ? $index['columns'] : $index['columns'][0];
1448
			}
1449
			if ($this->debug > 2) $this->debug_message("schema_proc::GetTableDefintion: MetaIndexes(%1) = %2",False,$sTableName,$indexes);
1450
		}
1451
		return $definition;
1452
	}
1453
1454
	/**
1455
	 * Check if all indexes exist and create them if not
1456
	 *
1457
	 * Used eg. after restoring a backup to make sure all indexes are in place.
1458
	 *
1459
	 * Does not check index-type or length!
1460
	 */
1461
	function CheckCreateIndexes()
1462
	{
1463
		foreach($this->adodb->MetaTables('TABLES') as $table)
1464
		{
1465
			if (!($table_def = $this->m_odb->get_table_definitions(true, $table))) continue;
1466
1467
			$definition = array();
1468
			$this->GetIndexes($table, $definition);
1469
			$current = $this->m_odb->metadata($table, true);
1470
1471
			// iterate though indexes we should have according to tables_current
1472
			foreach(array('uc', 'ix') as $type)
1473
			{
1474
				foreach($table_def[$type] as $columns)
1475
				{
1476
					// sometimes primary key is listed as (unique) index too --> ignore it
1477
					if ($this->_in_index($columns, array($table_def['pk']), true)) continue;
1478
1479
					// current table does NOT contain all columns, eg. not yet updated --> ignore index
1480
					if (array_diff((array)$columns, array_keys($current['meta'])))
1481
					{
1482
						//error_log(__METHOD__."() Can't create index over ", implode(',')." on table $table, as not all columns exist (yet)!");
1483
						continue;
1484
					}
1485
					// check if they exist in real table and create them if not
1486
					if (!$this->_in_index($columns, $definition[$type]) &&
1487
						// sometimes index is listed as unique index too --> ignore that
1488
						($type == 'uc' || !$this->_in_index($columns, $definition['uc'], true)))
1489
					{
1490
						// check if index may exists, but without limit in column-name
1491
						if ($this->_in_index($columns, $definition[$type], true))
1492
						{
1493
							// for PostgreSQL we dont use length-limited indexes --> nothing to do
1494
							if ($this->m_odb->Type == 'pgsql') continue;
1495
							// for MySQL we drop current index and create it with correct length
1496
							$this->DropIndex($table, $columns);
1497
						}
1498
						$this->CreateIndex($table, $columns, $type == 'uc');
1499
					}
1500
				}
1501
			}
1502
		}
1503
	}
1504
1505
	/**
1506
	 * Get actual columnnames as a comma-separated string in $sColumns and set indices as class-vars pk,fk,ix,uc
1507
	 *
1508
	 * old translator function, use GetTableDefition() instead
1509
	 * @depricated
1510
	 */
1511
	function _GetColumns($oProc,$sTableName,&$sColumns)
1512
	{
1513
		unset($oProc);	// unused, but required by function signature
1514
		$this->sCol = $this->pk = $this->fk = $this->ix = $this->uc = array();
0 ignored issues
show
Bug Best Practice introduced by
The property fk does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property pk does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property sCol does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property ix does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug Best Practice introduced by
The property uc does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1515
1516
		$tabledef = $this->GetTableDefinition($sTableName);
1517
1518
		$sColumns = implode(',',array_keys($tabledef['fd']));
1519
1520
		foreach($tabledef['fd'] as $column => $data)
1521
		{
1522
			$col_def = "'type' => '$data[type]'";
1523
			unset($data['type']);
1524
			foreach($data as $key => $val)
1525
			{
1526
				$col_def .= ", '$key' => ".(is_bool($val) ? ($val ? 'true' : 'false') :
1527
					(is_int($val) ? $val : "'$val'"));
1528
			}
1529
			$this->sCol[] = "\t\t\t\t'$column' => array($col_def),\n";
1530
		}
1531
		foreach(array('pk','fk','ix','uc') as $kind)
1532
		{
1533
			$this->$kind = $tabledef[$kind];
1534
		}
1535
	}
1536
}
1537