Passed
Push — release_2_0 ( 15c6ac...a1bf1f )
by Stefan
08:45
created

DBConnection::handle()   B

Complexity

Conditions 7
Paths 11

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 16
rs 8.8333
c 0
b 0
f 0
cc 7
nc 11
nop 1
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 the (only) instance of this class
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
            default:
70
                throw new Exception("This type of database (" . strtoupper($database) . ") is not known!");
71
        }
72
    }
73
74
    /**
75
     * Implemented for safety reasons only. Cloning is forbidden and will tell the user so.
76
     *
77
     * @return void
78
     */
79
    public function __clone() {
80
        trigger_error('Clone is not allowed.', E_USER_ERROR);
81
    }
82
83
    /**
84
     * tells the caller if the database is to be accessed read-only
85
     * @return bool
86
     */
87
    public function isReadOnly() {
88
        return $this->readOnly;
89
    }
90
91
    /**
92
     * executes a query and triggers logging to the SQL audit log if it's not a SELECT
93
     * @param string $querystring  the query to be executed
94
     * @param string $types        for prepared statements, the type list of parameters
95
     * @param mixed  ...$arguments for prepared statements, the parameters
96
     * @return mixed the query result as mysqli_result object; or TRUE on non-return-value statements
97
     * @throws Exception
98
     */
99
    public function exec($querystring, $types = NULL, &...$arguments) {
100
        // log exact query to audit log, if it's not a SELECT
101
        $isMoreThanSelect = FALSE;
102
        if (preg_match("/^SELECT/i", $querystring) == 0 && preg_match("/^DESC/i", $querystring) == 0) {
103
            $isMoreThanSelect = TRUE;
104
            if ($this->readOnly) { // let's not do this.
105
                throw new Exception("This is a read-only DB connection, but this is statement is not a SELECT!");
106
            }
107
        }
108
        // log exact query to debug log, if log level is at 5
109
        $this->loggerInstance->debug(5, "DB ATTEMPT: " . $querystring . "\n");
110
        if ($types !== NULL) {
111
            $this->loggerInstance->debug(5, "Argument type sequence: $types, parameters are: " . print_r($arguments, true));
112
        }
113
114
        if ($this->connection->connect_error) {
115
            throw new Exception("ERROR: Cannot send query to $this->databaseInstance database (no connection, error number" . $this->connection->connect_error . ")!");
116
        }
117
        if ($types === NULL) {
118
            $result = $this->connection->query($querystring);
119
            if ($result === FALSE) {
120
                throw new Exception("DB: Unable to execute simple statement! Error was --> " . $this->connection->error . " <--");
121
            }
122
        } else {
123
            // fancy! prepared statement with dedicated argument list
124
            if (strlen($types) != count($arguments)) {
125
                throw new Exception("DB: Prepared Statement: Number of arguments and the type list length differ!");
126
            }
127
            $statementObject = $this->connection->stmt_init();
128
            if ($statementObject === FALSE) {
129
                throw new Exception("DB: Unable to initialise prepared Statement!");
130
            }
131
            $prepResult = $statementObject->prepare($querystring);
132
            if ($prepResult === FALSE) {
133
                throw new Exception("DB: Unable to prepare statement! Statement was --> $querystring <--, error was --> " . $statementObject->error . " <--.");
134
            }
135
136
            // we have a variable number of arguments packed into the ... array
137
            // but the function needs to be called exactly once, with a series of
138
            // individual arguments, not an array. The voodoo solution is to call
139
            // it via call_user_func_array()
140
141
            $localArray = $arguments;
142
            array_unshift($localArray, $types);
143
            $retval = call_user_func_array([$statementObject, "bind_param"], $localArray);
144
            if ($retval === FALSE) {
145
                throw new Exception("DB: Unable to bind parameters to prepared statement! Argument array was --> " . var_export($localArray, TRUE) . " <--. Error was --> " . $statementObject->error . " <--");
146
            }
147
            $result = $statementObject->execute();
148
            if ($result === FALSE) {
149
                throw new Exception("DB: Unable to execute prepared statement! Error was --> " . $statementObject->error . " <--");
150
            }
151
            $selectResult = $statementObject->get_result();
152
            if ($selectResult !== FALSE) {
153
                $result = $selectResult;
154
            }
155
156
            $statementObject->close();
157
        }
158
159
        // all cases where $result could be FALSE have been caught earlier
160
        if ($this->connection->errno) {
161
            throw new Exception("ERROR: Cannot execute query in $this->databaseInstance database - (hopefully escaped) query was '$querystring', errno was " . $this->connection->errno . "!");
162
        }
163
164
165
        if ($isMoreThanSelect) {
166
            $this->loggerInstance->writeSQLAudit("[DB: " . strtoupper($this->databaseInstance) . "] " . $querystring);
167
            if ($types !== NULL) {
168
                $this->loggerInstance->writeSQLAudit("Argument type sequence: $types, parameters are: " . print_r($arguments, true));
169
            }
170
        }
171
        return $result;
172
    }
173
174
    /**
175
     * Retrieves the last auto-id of an INSERT. Needs to be called immediately after the corresponding exec() call
176
     * @return int the last autoincrement-ID
177
     */
178
    public function lastID() {
179
        return $this->connection->insert_id;
180
    }
181
182
    /**
183
     * Holds the singleton instance reference to USER database
184
     * 
185
     * @var DBConnection 
186
     */
187
    private static $instanceUSER;
188
189
    /**
190
     * Holds the singleton instance reference to INST database
191
     * 
192
     * @var DBConnection 
193
     */
194
    private static $instanceINST;
195
196
    /**
197
     * Holds the singleton instance reference to EXTERNAL database
198
     * 
199
     * @var DBConnection 
200
     */
201
    private static $instanceEXTERNAL;
202
203
    /**
204
     * Holds the singleton instance reference to FRONTEND database
205
     * 
206
     * @var DBConnection 
207
     */
208
    private static $instanceFRONTEND;
209
210
    /**
211
     * Holds the singleton instance reference to DIAGNOSTICS database
212
     * 
213
     * @var DBConnection 
214
     */
215
    private static $instanceDIAGNOSTICS;
216
217
    /**
218
     * after instantiation, keep state of which DB *this one* talks to
219
     * 
220
     * @var string which database does this instance talk to
221
     */
222
    private $databaseInstance;
223
224
    /**
225
     * The connection to the DB server
226
     * 
227
     * @var \mysqli
228
     */
229
    private $connection;
230
231
    /**
232
     * @var \core\common\Logging
233
     */
234
    private $loggerInstance;
235
236
    /**
237
     * Keeps state whether we are a readonly DB instance
238
     * @var boolean
239
     */
240
    private $readOnly;
241
242
    /**
243
     * Class constructor. Cannot be called directly; use handle()
244
     * 
245
     * @param string $database the database to open
246
     */
247
    private function __construct($database) {
248
        $this->loggerInstance = new \core\common\Logging();
249
        $databaseCapitalised = strtoupper($database);
250
        $this->connection = new \mysqli(CONFIG['DB'][$databaseCapitalised]['host'], CONFIG['DB'][$databaseCapitalised]['user'], CONFIG['DB'][$databaseCapitalised]['pass'], CONFIG['DB'][$databaseCapitalised]['db']);
251
        if ($this->connection->connect_error) {
252
            throw new Exception("ERROR: Unable to connect to $database database! This is a fatal error, giving up (error number " . $this->connection->connect_errno . ").");
253
        }
254
255
        if ($databaseCapitalised == "EXTERNAL" && CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") {
256
            $this->connection->query("SET NAMES 'latin1'");
257
        }
258
        $this->readOnly = CONFIG['DB'][$databaseCapitalised]['readonly'];
259
    }
260
261
}
262