|
1
|
|
|
<?php |
|
|
|
|
|
|
2
|
|
|
/* |
|
3
|
|
|
* ***************************************************************************** |
|
4
|
|
|
* Contributions to this work were made on behalf of the GÉANT project, a |
|
5
|
|
|
* project that has received funding from the European Union’s Framework |
|
6
|
|
|
* Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus), |
|
7
|
|
|
* Horizon 2020 research and innovation programme under Grant Agreements No. |
|
8
|
|
|
* 691567 (GN4-1) and No. 731122 (GN4-2). |
|
9
|
|
|
* On behalf of the aforementioned projects, GEANT Association is the sole owner |
|
10
|
|
|
* of the copyright in all material which was developed by a member of the GÉANT |
|
11
|
|
|
* project. GÉANT Vereniging (Association) is registered with the Chamber of |
|
12
|
|
|
* Commerce in Amsterdam with registration number 40535155 and operates in the |
|
13
|
|
|
* UK as a branch of GÉANT Vereniging. |
|
14
|
|
|
* |
|
15
|
|
|
* Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. |
|
16
|
|
|
* UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK |
|
17
|
|
|
* |
|
18
|
|
|
* License: see the web/copyright.inc.php file in the file structure or |
|
19
|
|
|
* <base_url>/copyright.php after deploying the software |
|
20
|
|
|
*/ |
|
21
|
|
|
|
|
22
|
|
|
/** |
|
23
|
|
|
* This file contains the DBConnection singleton. |
|
24
|
|
|
* |
|
25
|
|
|
* @author Stefan Winter <[email protected]> |
|
26
|
|
|
* @author Tomasz Wolniewicz <[email protected]> |
|
27
|
|
|
* |
|
28
|
|
|
* @package Developer |
|
29
|
|
|
*/ |
|
30
|
|
|
|
|
31
|
|
|
namespace core; |
|
32
|
|
|
|
|
33
|
|
|
use \Exception; |
|
34
|
|
|
|
|
35
|
|
|
require_once dirname(__DIR__) . "/config/_config.php"; |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* This class is a singleton for establishing a connection to the database |
|
39
|
|
|
* |
|
40
|
|
|
* @author Stefan Winter <[email protected]> |
|
41
|
|
|
* @author Tomasz Wolniewicz <[email protected]> |
|
42
|
|
|
* |
|
43
|
|
|
* @license see LICENSE file in root directory |
|
44
|
|
|
* |
|
45
|
|
|
* @package Developer |
|
46
|
|
|
*/ |
|
47
|
|
|
class DBConnection { |
|
48
|
|
|
|
|
49
|
|
|
/** |
|
50
|
|
|
* This is the actual constructor for the singleton. It creates a database connection if it is not up yet, and returns a handle to the database connection on every call. |
|
51
|
|
|
* |
|
52
|
|
|
* @param string $database the database type to open |
|
53
|
|
|
* @return DBConnection|array the (only) instance of this class; or all instances of a DB cluster (only for RADIUS auth servers right now) |
|
54
|
|
|
*/ |
|
|
|
|
|
|
55
|
|
|
public static function handle($database) { |
|
56
|
|
|
$theDb = strtoupper($database); |
|
57
|
|
|
switch ($theDb) { |
|
58
|
|
|
case "INST": |
|
59
|
|
|
case "USER": |
|
60
|
|
|
case "EXTERNAL": |
|
61
|
|
|
case "FRONTEND": |
|
62
|
|
|
case "DIAGNOSTICS": |
|
63
|
|
|
if (!isset(self::${"instance" . $theDb})) { |
|
64
|
|
|
$class = __CLASS__; |
|
65
|
|
|
self::${"instance" . $theDb} = new $class($database); |
|
66
|
|
|
DBConnection::${"instance" . $theDb}->databaseInstance = $theDb; |
|
67
|
|
|
} |
|
68
|
|
|
return self::${"instance" . $theDb}; |
|
69
|
|
|
case "RADIUS": |
|
70
|
|
|
if (!isset(self::${"instance" . $theDb})) { |
|
71
|
|
|
$class = __CLASS__; |
|
72
|
|
|
foreach (CONFIG_CONFASSISTANT['DB'] as $name => $oneRadiusAuthDb) { |
|
73
|
|
|
$theInstance = new $class($name); |
|
74
|
|
|
self::${"instance" . $theDb}[] = $theInstance; |
|
75
|
|
|
$theInstance->databaseInstance = $theDb; |
|
76
|
|
|
} |
|
77
|
|
|
} |
|
78
|
|
|
return self::${"instance" . $theDb}; |
|
79
|
|
|
default: |
|
80
|
|
|
throw new Exception("This type of database (" . strtoupper($database) . ") is not known!"); |
|
81
|
|
|
} |
|
82
|
|
|
} |
|
83
|
|
|
|
|
84
|
|
|
/** |
|
85
|
|
|
* Implemented for safety reasons only. Cloning is forbidden and will tell the user so. |
|
86
|
|
|
* |
|
87
|
|
|
* @return void |
|
88
|
|
|
*/ |
|
89
|
|
|
public function __clone() { |
|
90
|
|
|
trigger_error('Clone is not allowed.', E_USER_ERROR); |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
/** |
|
94
|
|
|
* tells the caller if the database is to be accessed read-only |
|
95
|
|
|
* @return bool |
|
96
|
|
|
*/ |
|
97
|
|
|
public function isReadOnly() { |
|
98
|
|
|
return $this->readOnly; |
|
99
|
|
|
} |
|
100
|
|
|
|
|
101
|
|
|
/** |
|
102
|
|
|
* executes a query and triggers logging to the SQL audit log if it's not a SELECT |
|
103
|
|
|
* @param string $querystring the query to be executed |
|
104
|
|
|
* @param string $types for prepared statements, the type list of parameters |
|
105
|
|
|
* @param mixed ...$arguments for prepared statements, the parameters |
|
106
|
|
|
* @return mixed the query result as mysqli_result object; or TRUE on non-return-value statements |
|
107
|
|
|
* @throws Exception |
|
108
|
|
|
*/ |
|
109
|
|
|
public function exec($querystring, $types = NULL, &...$arguments) { |
|
110
|
|
|
// log exact query to audit log, if it's not a SELECT |
|
111
|
|
|
$isMoreThanSelect = FALSE; |
|
112
|
|
|
if (preg_match("/^SELECT/i", $querystring) == 0 && preg_match("/^DESC/i", $querystring) == 0) { |
|
113
|
|
|
$isMoreThanSelect = TRUE; |
|
114
|
|
|
if ($this->readOnly) { // let's not do this. |
|
115
|
|
|
throw new Exception("This is a read-only DB connection, but this is statement is not a SELECT!"); |
|
116
|
|
|
} |
|
117
|
|
|
} |
|
118
|
|
|
// log exact query to debug log, if log level is at 5 |
|
119
|
|
|
$this->loggerInstance->debug(5, "DB ATTEMPT: " . $querystring . "\n"); |
|
120
|
|
|
if ($types !== NULL) { |
|
121
|
|
|
$this->loggerInstance->debug(5, "Argument type sequence: $types, parameters are: " . print_r($arguments, true)); |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
if ($this->connection->connect_error) { |
|
125
|
|
|
throw new Exception("ERROR: Cannot send query to $this->databaseInstance database (no connection, error number" . $this->connection->connect_error . ")!"); |
|
126
|
|
|
} |
|
127
|
|
|
if ($types === NULL) { |
|
128
|
|
|
$result = $this->connection->query($querystring); |
|
129
|
|
|
if ($result === FALSE) { |
|
130
|
|
|
throw new Exception("DB: Unable to execute simple statement! Error was --> " . $this->connection->error . " <--"); |
|
131
|
|
|
} |
|
132
|
|
|
} else { |
|
133
|
|
|
// fancy! prepared statement with dedicated argument list |
|
134
|
|
|
if (strlen($types) != count($arguments)) { |
|
135
|
|
|
throw new Exception("DB: Prepared Statement: Number of arguments and the type list length differ!"); |
|
136
|
|
|
} |
|
137
|
|
|
$statementObject = $this->connection->stmt_init(); |
|
138
|
|
|
if ($statementObject === FALSE) { |
|
139
|
|
|
throw new Exception("DB: Unable to initialise prepared Statement!"); |
|
140
|
|
|
} |
|
141
|
|
|
$prepResult = $statementObject->prepare($querystring); |
|
142
|
|
|
if ($prepResult === FALSE) { |
|
143
|
|
|
throw new Exception("DB: Unable to prepare statement! Statement was --> $querystring <--, error was --> " . $statementObject->error . " <--."); |
|
144
|
|
|
} |
|
145
|
|
|
|
|
146
|
|
|
// we have a variable number of arguments packed into the ... array |
|
147
|
|
|
// but the function needs to be called exactly once, with a series of |
|
148
|
|
|
// individual arguments, not an array. The voodoo solution is to call |
|
149
|
|
|
// it via call_user_func_array() |
|
150
|
|
|
|
|
151
|
|
|
$localArray = $arguments; |
|
152
|
|
|
array_unshift($localArray, $types); |
|
153
|
|
|
$retval = call_user_func_array([$statementObject, "bind_param"], $localArray); |
|
154
|
|
|
if ($retval === FALSE) { |
|
155
|
|
|
throw new Exception("DB: Unable to bind parameters to prepared statement! Argument array was --> " . var_export($localArray, TRUE) . " <--. Error was --> " . $statementObject->error . " <--"); |
|
156
|
|
|
} |
|
157
|
|
|
$result = $statementObject->execute(); |
|
158
|
|
|
if ($result === FALSE) { |
|
159
|
|
|
throw new Exception("DB: Unable to execute prepared statement! Error was --> " . $statementObject->error . " <--"); |
|
160
|
|
|
} |
|
161
|
|
|
$selectResult = $statementObject->get_result(); |
|
162
|
|
|
if ($selectResult !== FALSE) { |
|
163
|
|
|
$result = $selectResult; |
|
164
|
|
|
} |
|
165
|
|
|
|
|
166
|
|
|
$statementObject->close(); |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
// all cases where $result could be FALSE have been caught earlier |
|
170
|
|
|
if ($this->connection->errno) { |
|
171
|
|
|
throw new Exception("ERROR: Cannot execute query in $this->databaseInstance database - (hopefully escaped) query was '$querystring', errno was " . $this->connection->errno . "!"); |
|
172
|
|
|
} |
|
173
|
|
|
|
|
174
|
|
|
|
|
175
|
|
|
if ($isMoreThanSelect) { |
|
176
|
|
|
$this->loggerInstance->writeSQLAudit("[DB: " . strtoupper($this->databaseInstance) . "] " . $querystring); |
|
177
|
|
|
if ($types !== NULL) { |
|
178
|
|
|
$this->loggerInstance->writeSQLAudit("Argument type sequence: $types, parameters are: " . print_r($arguments, true)); |
|
179
|
|
|
} |
|
180
|
|
|
} |
|
181
|
|
|
return $result; |
|
182
|
|
|
} |
|
183
|
|
|
|
|
184
|
|
|
/** |
|
185
|
|
|
* Retrieves the last auto-id of an INSERT. Needs to be called immediately after the corresponding exec() call |
|
186
|
|
|
* @return int the last autoincrement-ID |
|
187
|
|
|
*/ |
|
188
|
|
|
public function lastID() { |
|
189
|
|
|
return $this->connection->insert_id; |
|
190
|
|
|
} |
|
191
|
|
|
|
|
192
|
|
|
/** |
|
193
|
|
|
* Holds the singleton instance reference to USER database |
|
194
|
|
|
* |
|
195
|
|
|
* @var DBConnection |
|
196
|
|
|
*/ |
|
197
|
|
|
private static $instanceUSER; |
|
198
|
|
|
|
|
199
|
|
|
/** |
|
200
|
|
|
* Holds the singleton instance reference to INST database |
|
201
|
|
|
* |
|
202
|
|
|
* @var DBConnection |
|
203
|
|
|
*/ |
|
204
|
|
|
private static $instanceINST; |
|
205
|
|
|
|
|
206
|
|
|
/** |
|
207
|
|
|
* Holds the singleton instance reference to EXTERNAL database |
|
208
|
|
|
* |
|
209
|
|
|
* @var DBConnection |
|
210
|
|
|
*/ |
|
211
|
|
|
private static $instanceEXTERNAL; |
|
212
|
|
|
|
|
213
|
|
|
/** |
|
214
|
|
|
* Holds the singleton instance reference to FRONTEND database |
|
215
|
|
|
* |
|
216
|
|
|
* @var DBConnection |
|
217
|
|
|
*/ |
|
218
|
|
|
private static $instanceFRONTEND; |
|
219
|
|
|
|
|
220
|
|
|
/** |
|
221
|
|
|
* Holds the singleton instance reference to DIAGNOSTICS database |
|
222
|
|
|
* |
|
223
|
|
|
* @var DBConnection |
|
224
|
|
|
*/ |
|
225
|
|
|
private static $instanceDIAGNOSTICS; |
|
226
|
|
|
|
|
227
|
|
|
/** |
|
228
|
|
|
* Holds an ARRAY of all RADIUS server instances for Silverbullet |
|
229
|
|
|
*/ |
|
|
|
|
|
|
230
|
|
|
private static $instanceRADIUS; |
|
231
|
|
|
|
|
232
|
|
|
/** |
|
233
|
|
|
* after instantiation, keep state of which DB *this one* talks to |
|
234
|
|
|
* |
|
235
|
|
|
* @var string which database does this instance talk to |
|
236
|
|
|
*/ |
|
237
|
|
|
private $databaseInstance; |
|
238
|
|
|
|
|
239
|
|
|
/** |
|
240
|
|
|
* The connection to the DB server |
|
241
|
|
|
* |
|
242
|
|
|
* @var \mysqli |
|
243
|
|
|
*/ |
|
244
|
|
|
private $connection; |
|
245
|
|
|
|
|
246
|
|
|
/** |
|
247
|
|
|
* @var \core\common\Logging |
|
248
|
|
|
*/ |
|
249
|
|
|
private $loggerInstance; |
|
250
|
|
|
|
|
251
|
|
|
/** |
|
252
|
|
|
* Keeps state whether we are a readonly DB instance |
|
253
|
|
|
* @var boolean |
|
254
|
|
|
*/ |
|
255
|
|
|
private $readOnly; |
|
256
|
|
|
|
|
257
|
|
|
/** |
|
258
|
|
|
* Class constructor. Cannot be called directly; use handle() |
|
259
|
|
|
* |
|
260
|
|
|
* @param string $database the database to open |
|
261
|
|
|
*/ |
|
|
|
|
|
|
262
|
|
|
private function __construct($database) { |
|
263
|
|
|
$this->loggerInstance = new \core\common\Logging(); |
|
264
|
|
|
$databaseCapitalised = strtoupper($database); |
|
265
|
|
|
if (isset(CONFIG['DB'][$databaseCapitalised])) { |
|
266
|
|
|
$this->connection = new \mysqli(CONFIG['DB'][$databaseCapitalised]['host'], CONFIG['DB'][$databaseCapitalised]['user'], CONFIG['DB'][$databaseCapitalised]['pass'], CONFIG['DB'][$databaseCapitalised]['db']); |
|
267
|
|
|
$this->readOnly = CONFIG['DB'][$databaseCapitalised]['readonly']; |
|
268
|
|
|
} else { // one of the RADIUS DBs |
|
269
|
|
|
$this->connection = new \mysqli(CONFIG_CONFASSISTANT['DB'][$databaseCapitalised]['host'], CONFIG_CONFASSISTANT['DB'][$databaseCapitalised]['user'], CONFIG_CONFASSISTANT['DB'][$databaseCapitalised]['pass'], CONFIG_CONFASSISTANT['DB'][$databaseCapitalised]['db']); |
|
270
|
|
|
$this->readOnly = CONFIG_CONFASSISTANT['DB'][$databaseCapitalised]['readonly']; |
|
271
|
|
|
} |
|
272
|
|
|
if ($this->connection->connect_error) { |
|
273
|
|
|
throw new Exception("ERROR: Unable to connect to $database database! This is a fatal error, giving up (error number " . $this->connection->connect_errno . ")."); |
|
274
|
|
|
} |
|
275
|
|
|
|
|
276
|
|
|
if ($databaseCapitalised == "EXTERNAL" && CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { |
|
277
|
|
|
$this->connection->query("SET NAMES 'latin1'"); |
|
278
|
|
|
} |
|
279
|
|
|
|
|
280
|
|
|
} |
|
281
|
|
|
|
|
282
|
|
|
} |
|
283
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.