aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_framework/phpbb_database_test_connection_manager.php
blob: f3adbefc1ba3702a22d3ac9f9c984e7db7916149 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/

require_once dirname(__FILE__) . '/phpbb_database_connection_odbc_pdo_wrapper.php';

class phpbb_database_test_connection_manager
{
	/** @var array */
	private $config;
	/** @var array */
	private $dbms;
	/** @var \PDO */
	private $pdo;

	/**
	* Constructor
	*
	* @param	array	$config		Tests database configuration as returned by
	* 								phpbb_database_test_case::get_database_config()
	*/
	public function __construct($config)
	{
		$this->config = $config;
		$this->dbms = $this->get_dbms_data($this->config['dbms']);
	}

	/**
	* Return the current PDO instance
	*/
	public function get_pdo()
	{
		return $this->pdo;
	}

	/**
	* Creates a PDO connection for the configured database.
	*
	* @param	bool	$use_db		Whether the DSN should be tied to a
	*								particular database making it impossible
	*								to delete that database.
	*/
	public function connect($use_db = true)
	{
		$dsn = $this->dbms['PDO'] . ':';

		switch ($this->dbms['PDO'])
		{
			case 'sqlite':	// SQLite3 driver
				$dsn .= $this->config['dbhost'];
			break;

			case 'sqlsrv':
				// prefix the hostname (or DSN) with Server= so using just (local)\SQLExpress
				// works for example, further parameters can still be appended using ;x=y
				$dsn .= 'Server=';
			// no break -> rest like ODBC
			case 'odbc':
				// for ODBC assume dbhost is a suitable DSN
				// e.g. Driver={SQL Server Native Client 10.0};Server=(local)\SQLExpress;
				$dsn .= $this->config['dbhost'];

				// Primarily for MSSQL Native/Azure as ODBC needs it in $dbhost, attached to the Server param
				if ($this->config['dbport'])
				{
					$port_delimiter = (defined('PHP_OS') && substr(PHP_OS, 0, 3) === 'WIN') ? ',' : ':';
					$dsn .= $port_delimiter . $this->config['dbport'];
				}

				if ($use_db)
				{
					$dsn .= ';Database=' . $this->config['dbname'];
				}
			break;

			default:
				if (!empty($this->config['dbport']) && !is_numeric($this->config['dbport']) && $this->dbms['PDO'] != 'pgsql')
				{
					$dsn .= 'unix_socket=' . $this->config['dbport'];
				}
				else
				{
					$dsn .= 'host=' . $this->config['dbhost'];

					if ($this->config['dbport'])
					{
						$dsn .= ';port=' . $this->config['dbport'];
					}
				}

				if ($use_db)
				{
					$dsn .= ';dbname=' . $this->config['dbname'];
				}
				else if ($this->dbms['PDO'] == 'pgsql')
				{
					// Postgres always connects to a
					// database. If the database is not
					// specified here, but the username
					// is specified, then connection
					// will be to the database named
					// as the username.
					//
					// For greater compatibility, connect
					// instead to postgres database which
					// should always exist:
					// http://www.postgresql.org/docs/9.0/static/manage-ag-templatedbs.html
					$dsn .= ';dbname=postgres';
				}
			break;
		}

		// These require different connection strings on the phpBB side than they do in PDO
		// so you must provide a DSN string for ODBC separately
		if (!empty($this->config['custom_dsn']) && $this->config['dbms'] == 'phpbb\db\driver\mssql')
		{
			$dsn = 'odbc:' . $this->config['custom_dsn'];
		}

		try
		{
			switch ($this->config['dbms'])
			{
				case 'phpbb\db\driver\mssql':
				case 'phpbb\db\driver\mssql_odbc':
					$this->pdo = new phpbb_database_connection_odbc_pdo_wrapper('mssql', 0, $dsn, $this->config['dbuser'], $this->config['dbpasswd']);
				break;

				default:
					$this->pdo = new PDO($dsn, $this->config['dbuser'], $this->config['dbpasswd']);
				break;
			}
		}
		catch (PDOException $e)
		{
			$cleaned_dsn = str_replace($this->config['dbpasswd'], '*password*', $dsn);
			throw new Exception("Unable to connect to $cleaned_dsn using PDO with error: {$e->getMessage()}");
		}

		$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

		switch ($this->config['dbms'])
		{
			case 'phpbb\db\driver\mysql':
			case 'phpbb\db\driver\mysqli':
				$this->pdo->exec('SET NAMES utf8');

				/*
				* The phpBB MySQL drivers set the STRICT_ALL_TABLES and
				* STRICT_TRANS_TABLES flags/modes, so as a minimum requirement
				* we want to make sure those are set for the PDO side of the
				* test suite.
				*
				* The TRADITIONAL flag implies STRICT_ALL_TABLES and
				* STRICT_TRANS_TABLES as well as other useful strictness flags
				* the phpBB MySQL driver does not set.
				*/
				$this->pdo->exec("SET SESSION sql_mode='TRADITIONAL'");
			break;

			default:
		}
	}

	/**
	* Load the phpBB database schema into the database
	*/
	public function load_schema($db)
	{
		$this->ensure_connected(__METHOD__);

		$directory = dirname(__FILE__) . '/../../phpBB/install/schemas/';
		$this->load_schema_from_file($directory, $db);
	}

	/**
	* Drop the database if it exists and re-create it
	*
	* Note: This does not load the schema, and it is suggested
	* to re-connect after calling to get use_db isolation.
	*/
	public function recreate_db()
	{
		switch ($this->config['dbms'])
		{
			case 'phpbb\db\driver\sqlite3':
				$this->connect();
				// Drop all of the tables
				foreach ($this->get_tables() as $table)
				{
					$this->pdo->exec('DROP TABLE ' . $table);
				}
				$this->purge_extras();
			break;

			case 'phpbb\db\driver\oracle':
				$this->connect();
				// Drop all of the tables
				foreach ($this->get_tables() as $table)
				{
					$this->pdo->exec('DROP TABLE ' . $table . ' CASCADE CONSTRAINTS');
				}
				$this->purge_extras();
			break;

			case 'phpbb\db\driver\postgres':
				$this->connect();
				// Drop all of the tables
				foreach ($this->get_tables() as $table)
				{
					$this->pdo->exec('DROP TABLE ' . $table . ' CASCADE');
				}
				$this->purge_extras();
			break;

			case 'phpbb\db\driver\mssql':
			case 'phpbb\db\driver\mssqlnative':
				$this->connect();
				// Drop all tables
				$this->pdo->exec("EXEC sp_MSforeachtable 'DROP TABLE ?'");
				$this->purge_extras();
			break;

			default:
				$this->connect(false);

				try
				{
					$this->pdo->exec('DROP DATABASE ' . $this->config['dbname']);

					try
					{
						$this->pdo->exec('CREATE DATABASE ' . $this->config['dbname']);
					}
					catch (PDOException $e)
					{
						throw new Exception("Unable to re-create database: {$e->getMessage()}");
					}
				}
				catch (PDOException $e)
				{
					// try to delete all tables if dropping the database was not possible.
					foreach ($this->get_tables() as $table)
					{
						$this->pdo->exec('DROP TABLE ' . $table);
					}
					$this->purge_extras();
				}
			 break;
		}
	}

	/**
	* Retrieves a list of all tables from the database.
	*
	* @return	array(string)
	*/
	public function get_tables()
	{
		$this->ensure_connected(__METHOD__);

		switch ($this->config['dbms'])
		{
			case 'phpbb\db\driver\mysql':
			case 'phpbb\db\driver\mysqli':
				$sql = 'SHOW TABLES';
			break;

			case 'phpbb\db\driver\sqlite3':
				$sql = 'SELECT name
					FROM sqlite_master
					WHERE type = "table"
						AND name <> "sqlite_sequence"';
			break;

			case 'phpbb\db\driver\mssql':
			case 'phpbb\db\driver\mssql_odbc':
			case 'phpbb\db\driver\mssqlnative':
				$sql = "SELECT name
					FROM sysobjects
					WHERE type='U'";
			break;

			case 'phpbb\db\driver\postgres':
				$sql = 'SELECT relname
					FROM pg_stat_user_tables';
			break;

			case 'phpbb\db\driver\oracle':
				$sql = 'SELECT table_name
					FROM USER_TABLES';
			break;
		}

		$result = $this->pdo->query($sql);

		$tables = array();
		while ($row = $result->fetch(PDO::FETCH_NUM))
		{
			$tables[] = current($row);
		}

		return $tables;
	}

	/**
	* Throw an exception if not connected
	*/
	protected function ensure_connected($method_name)
	{
		if (null === $this->pdo)
		{
			throw new Exception(sprintf('You must connect before calling %s', $method_name));
		}
	}

	/**
	* Compile the correct schema filename (as per create_schema_files) and
	* load it into the database.
	*/
	protected function load_schema_from_file($directory, \phpbb\db\driver\driver_interface $db)
	{
		$schema = $this->dbms['SCHEMA'];

		if ($this->config['dbms'] == 'phpbb\db\driver\mysql')
		{
			$sth = $this->pdo->query('SELECT VERSION() AS version');
			$row = $sth->fetch(PDO::FETCH_ASSOC);

			if (version_compare($row['version'], '4.1.3', '>='))
			{
				$schema .= '_41';
			}
			else
			{
				$schema .= '_40';
			}
		}

		$filename = $directory . $schema . '_schema.sql';

		if (file_exists($filename))
		{
			global $phpbb_root_path;

			$queries = file_get_contents($filename);

			$db_helper = new \phpbb\install\helper\database(new \phpbb\filesystem\filesystem(), $phpbb_root_path);
			$sql = $db_helper->remove_comments($queries);
			$sql = $db_helper->split_sql_file($sql, $this->dbms['DELIM']);

			foreach ($sql as $query)
			{
				$this->pdo->exec($query);
			}
		}

		// Ok we have the db info go ahead and work on building the table
		if (file_exists($directory . 'schema.json'))
		{
			$db_table_schema = file_get_contents($directory . 'schema.json');
			$db_table_schema = json_decode($db_table_schema, true);
		}
		else
		{
			global $phpbb_root_path, $phpEx, $table_prefix;

			$finder = new \phpbb\finder(new \phpbb\filesystem\filesystem(), $phpbb_root_path, null, $phpEx);
			$classes = $finder->core_path('phpbb/db/migration/data/')
				->get_classes();

			$db = new \phpbb\db\driver\sqlite3();
			$factory = new \phpbb\db\tools\factory();
			$db_tools = $factory->get($db, true);

			$schema_generator = new \phpbb\db\migration\schema_generator($classes, new \phpbb\config\config(array()), $db, $db_tools, $phpbb_root_path, $phpEx, $table_prefix);
			$db_table_schema = $schema_generator->get_schema();
		}

		$factory = new \phpbb\db\tools\factory();
		$db_tools = $factory->get($db, true);
		foreach ($db_table_schema as $table_name => $table_data)
		{
			$queries = $db_tools->sql_create_table(
				$table_name,
				$table_data
			);

			foreach ($queries as $query)
			{
				if ($query === 'begin')
				{
					$this->pdo->beginTransaction();
				}
				else if ($query === 'commit')
				{
					$this->pdo->commit();
				}
				else
				{
					$this->pdo->exec($query);
				}
			}
		}
	}

	/**
	* Map a phpBB dbms driver name to dbms data array
	*/
	protected function get_dbms_data($dbms)
	{
		$available_dbms = array(
			'phpbb\db\driver\mysqli'	=> array(
				'SCHEMA'		=> 'mysql_41',
				'DELIM'			=> ';',
				'PDO'			=> 'mysql',
			),
			'phpbb\db\driver\mysql'		=> array(
				'SCHEMA'		=> 'mysql',
				'DELIM'			=> ';',
				'PDO'			=> 'mysql',
			),
			'phpbb\db\driver\mssql'		=> array(
				'SCHEMA'		=> 'mssql',
				'DELIM'			=> 'GO',
				'PDO'			=> 'odbc',
			),
			'phpbb\db\driver\mssql_odbc'=>	array(
				'SCHEMA'		=> 'mssql',
				'DELIM'			=> 'GO',
				'PDO'			=> 'odbc',
			),
			'phpbb\db\driver\mssqlnative'		=> array(
				'SCHEMA'		=> 'mssql',
				'DELIM'			=> 'GO',
				'PDO'			=> 'sqlsrv',
			),
			'phpbb\db\driver\oracle'	=>	array(
				'SCHEMA'		=> 'oracle',
				'DELIM'			=> '/',
				'PDO'			=> 'oci',
			),
			'phpbb\db\driver\postgres' => array(
				'SCHEMA'		=> 'postgres',
				'DELIM'			=> ';',
				'PDO'			=> 'pgsql',
			),
			'phpbb\db\driver\sqlite3'		=> array(
				'SCHEMA'		=> 'sqlite',
				'DELIM'			=> ';',
				'PDO'			=> 'sqlite',
			),
		);

		if (isset($available_dbms[$dbms]))
		{
			return $available_dbms[$dbms];
		}
		else
		{
			$message = "Supplied dbms \"$dbms\" is not a valid phpBB dbms, must be one of: ";
			$message .= implode(', ', array_keys($available_dbms));
			throw new Exception($message);
		}
	}

	/**
	* Removes extra objects from a database. This is for cases where dropping the database fails.
	*/
	public function purge_extras()
	{
		$this->ensure_connected(__METHOD__);
		$queries = array();

		switch ($this->config['dbms'])
		{
			case 'phpbb\db\driver\oracle':
				$sql = 'SELECT sequence_name
					FROM USER_SEQUENCES';
				$result = $this->pdo->query($sql);

				while ($row = $result->fetch(PDO::FETCH_NUM))
				{
					$queries[] = 'DROP SEQUENCE ' . current($row);
				}
			break;

			case 'phpbb\db\driver\postgres':
				$sql = 'SELECT sequence_name
					FROM information_schema.sequences';
				$result = $this->pdo->query($sql);

				while ($row = $result->fetch(PDO::FETCH_NUM))
				{
					$queries[] = 'DROP SEQUENCE ' . current($row);
				}

				$queries[] = 'DROP TYPE IF EXISTS varchar_ci CASCADE';
			break;
		}

		foreach ($queries as $query)
		{
			$this->pdo->exec($query);
		}
	}

	/**
	* Performs synchronisations on the database after a fixture has been loaded
	*
	* @param	PHPUnit_Extensions_Database_DataSet_XmlDataSet	$xml_data_set		Information about the tables contained within the loaded fixture
	*
	* @return null
	*/
	public function post_setup_synchronisation($xml_data_set)
	{
		$table_names = $xml_data_set->getTableNames();

		$tables = array();
		foreach ($table_names as $table)
		{
			$tables[$table] = $xml_data_set->getTableMetaData($table)->getColumns();
		}

		$this->database_synchronisation($tables);
	}

	/**
	* Performs synchronisations on the database after a fixture has been loaded
	*
	* @param	array	$table_column_map		Array of tables/columns to synchronise
	*											array(table1 => array(column1, column2))
	*
	* @return null
	*/
	public function database_synchronisation($table_column_map)
	{
		$this->ensure_connected(__METHOD__);
		$queries = array();

		// Get escaped versions of the table names to synchronise
		$table_names = array_map(array($this->pdo, 'PDO::quote'), array_keys($table_column_map));

		switch ($this->config['dbms'])
		{
			case 'phpbb\db\driver\oracle':
				// Get all of the information about the sequences
				$sql = "SELECT t.table_name, tc.column_name, d.referenced_name as sequence_name, s.increment_by, s.min_value
					FROM USER_TRIGGERS t
					JOIN USER_DEPENDENCIES d ON (d.name = t.trigger_name)
					JOIN USER_TRIGGER_COLS tc ON (tc.trigger_name = t.trigger_name)
					JOIN USER_SEQUENCES s ON (s.sequence_name = d.referenced_name)
					WHERE d.referenced_type = 'SEQUENCE'
						AND d.type = 'TRIGGER'
						AND t.table_name IN (" . implode(', ', array_map('strtoupper', $table_names)) . ')';

				$result = $this->pdo->query($sql);

				while ($row = $result->fetch(PDO::FETCH_ASSOC))
				{
					// Get the current max value of the table
					$sql = "SELECT MAX({$row['COLUMN_NAME']}) AS max FROM {$row['TABLE_NAME']}";
					$max_result = $this->pdo->query($sql);
					$max_row = $max_result->fetch(PDO::FETCH_ASSOC);

					if (!$max_row)
					{
						continue;
					}

					$max_val = (int) $max_row['MAX'];
					$max_val++;

					/**
					* This is not the "proper" way, but the proper way does not allow you to completely reset
					* tables with no rows since you have to select the next value to make the change go into effect.
					* You would have to go past the minimum value to set it correctly, but that's illegal.
					* Since we have no objects attached to our sequencers (triggers aren't attached), this works fine.
					*/
					$queries[] = 'DROP SEQUENCE ' . $row['SEQUENCE_NAME'];
					$queries[] = "CREATE SEQUENCE {$row['SEQUENCE_NAME']}
									MINVALUE {$row['MIN_VALUE']}
									INCREMENT BY {$row['INCREMENT_BY']}
									START WITH $max_val";
				}
			break;

			case 'phpbb\db\driver\postgres':
				// Get the sequences attached to the tables
				$sql = 'SELECT column_name, table_name FROM information_schema.columns
					WHERE table_name IN (' . implode(', ', $table_names) . ")
						AND strpos(column_default, '_seq''::regclass') > 0";
				$result = $this->pdo->query($sql);

				$setval_queries = array();
				while ($row = $result->fetch(PDO::FETCH_ASSOC))
				{
					// Get the columns used in the fixture for this table
					$column_names = $table_column_map[$row['table_name']];

					// Skip sequences that weren't specified in the fixture
					if (!in_array($row['column_name'], $column_names))
					{
						continue;
					}

					// Get the old value if it exists, or use 1 if it doesn't
					$sql = "SELECT COALESCE((SELECT MAX({$row['column_name']}) + 1 FROM {$row['table_name']}), 1) AS val";
					$result_max = $this->pdo->query($sql);
					$row_max = $result_max->fetch(PDO::FETCH_ASSOC);

					if ($row_max)
					{
						$seq_name = $this->pdo->quote($row['table_name'] . '_seq');
						$max_val = (int) $row_max['val'];

						// The last parameter is false so that the system doesn't increment it again
						$setval_queries[] = "SETVAL($seq_name, $max_val, false)";
					}
				}

				// Combine all of the SETVALs into one query
				if (count($setval_queries))
				{
					$queries[] = 'SELECT ' . implode(', ', $setval_queries);
				}
			break;

			case 'phpbb\db\driver\sqlite3':
				/**
				* Just delete all of the sequences. When an insertion occurs, the sequence will be automatically
				* re-created from the key with the AUTOINCREMENT attribute
				*/
				$queries[] = 'DELETE FROM sqlite_sequence';
			break;
		}

		foreach ($queries as $query)
		{
			$this->pdo->exec($query);
		}
	}
}