Subversion Repositories oidplus

Rev

Rev 1447 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
295 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
1086 daniel-mar 5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
295 daniel-mar 6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 */
19
 
1050 daniel-mar 20
namespace ViaThinkSoft\OIDplus;
511 daniel-mar 21
 
1086 daniel-mar 22
// phpcs:disable PSR1.Files.SideEffects
23
\defined('INSIDE_OIDPLUS') or die;
24
// phpcs:enable PSR1.Files.SideEffects
25
 
730 daniel-mar 26
abstract class OIDplusDatabaseConnection extends OIDplusBaseClass {
1130 daniel-mar 27
        /**
28
         * @var bool
29
         */
295 daniel-mar 30
        protected /*bool*/ $connected = false;
1130 daniel-mar 31
 
32
        /**
33
         * @var bool|null
34
         */
295 daniel-mar 35
        protected /*?bool*/ $html = null;
1130 daniel-mar 36
 
37
        /**
38
         * @var string|null
39
         */
295 daniel-mar 40
        protected /*?string*/ $last_query = null;
1130 daniel-mar 41
 
42
        /**
43
         * @var bool
44
         */
502 daniel-mar 45
        protected /*bool*/ $slangDetectionDone = false;
295 daniel-mar 46
 
1116 daniel-mar 47
        /**
1450 daniel-mar 48
         * @var OIDplusSqlSlangPlugin
49
         */
50
        private $slangCache = null;
51
 
52
        /**
1116 daniel-mar 53
         * @param string $sql
54
         * @param array|null $prepared_args
55
         * @return OIDplusQueryResult
56
         * @throws OIDplusException
57
         */
58
        protected abstract function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult;
59
 
60
        /**
61
         * @return string
62
         */
295 daniel-mar 63
        public abstract function error(): string;
1116 daniel-mar 64
 
65
        /**
66
         * @return void
67
         */
295 daniel-mar 68
        public abstract function transaction_begin()/*: void*/;
1116 daniel-mar 69
 
70
        /**
71
         * @return void
72
         */
295 daniel-mar 73
        public abstract function transaction_commit()/*: void*/;
1116 daniel-mar 74
 
75
        /**
76
         * @return void
77
         */
295 daniel-mar 78
        public abstract function transaction_rollback()/*: void*/;
1116 daniel-mar 79
 
80
        /**
81
         * @return bool
82
         */
295 daniel-mar 83
        public abstract function transaction_supported(): bool;
1116 daniel-mar 84
 
85
        /**
86
         * @return int
87
         */
295 daniel-mar 88
        public abstract function transaction_level(): int;
1116 daniel-mar 89
 
90
        /**
91
         * @return void
92
         */
295 daniel-mar 93
        protected abstract function doConnect()/*: void*/;
1116 daniel-mar 94
 
95
        /**
96
         * @return void
97
         */
295 daniel-mar 98
        protected abstract function doDisconnect()/*: void*/;
99
 
1116 daniel-mar 100
        /**
101
         * @return OIDplusDatabasePlugin|null
102
         */
817 daniel-mar 103
        public function getPlugin()/*: ?OIDplusDatabasePlugin*/ {
104
                $plugins = OIDplus::getDatabasePlugins();
105
                foreach ($plugins as $plugin) {
1116 daniel-mar 106
                        if (get_class($this) == get_class($plugin::newConnection())) {
817 daniel-mar 107
                                return $plugin;
108
                        }
109
                }
1130 daniel-mar 110
                return null;
817 daniel-mar 111
        }
112
 
1116 daniel-mar 113
        /**
114
         * @return int
1160 daniel-mar 115
         * @throws OIDplusConfigInitializationException
1116 daniel-mar 116
         * @throws OIDplusException
117
         */
1160 daniel-mar 118
        protected function doInsertId(): int {
295 daniel-mar 119
                // This is the "fallback" variant. If your database provider (e.g. PDO) supports
120
                // a function to detect the last inserted id, please override this
121
                // function in order to use that specialized function (since it is usually
122
                // more reliable).
316 daniel-mar 123
                return $this->getSlang()->insert_id($this);
295 daniel-mar 124
        }
125
 
1116 daniel-mar 126
        /**
1160 daniel-mar 127
         * @return int
128
         * @throws OIDplusException
129
         */
130
        public final function insert_id(): int {
131
                // DM 04 Apr 2023: Added, because MSSQL's @@IDENTITY, PgSQL, and SQLite3 does not reset after
132
                // a Non-Insert query (this is a test case in dev/test_database_plugins).
133
                // Note that the INSERT could be hidden inside a Stored Procedure; we don't support (or need) that yet.
134
                if (!str_starts_with(trim(strtolower($this->last_query)),'insert')) return 0;
135
 
136
                return $this->doInsertId();
137
        }
138
 
139
        /**
1226 daniel-mar 140
         * Get the rows affected, for either SELECT, INSERT, DELETE, UPDATE
141
         * @return int
142
         */
143
        public function rowsAffected(): int {
144
                return -1; // -1 means not implemented
145
        }
146
 
147
        /**
1116 daniel-mar 148
         * @param string $sql
149
         * @return array[]
150
         * @throws OIDplusException
151
         */
152
        public final function getTable(string $sql): array {
990 daniel-mar 153
                $out = array();
989 daniel-mar 154
                $res = $this->query($sql);
990 daniel-mar 155
                while ($row = $res->fetch_array()) {
1226 daniel-mar 156
                        $out[] = /*yield*/ $row;
990 daniel-mar 157
                }
158
                return $out;
159
        }
160
 
1116 daniel-mar 161
        /**
162
         * @param string $sql
163
         * @return mixed|null
164
         * @throws OIDplusException
165
         */
990 daniel-mar 166
        public final function getScalar(string $sql) {
167
                $res = $this->query($sql);
989 daniel-mar 168
                $row = $res->fetch_array();
990 daniel-mar 169
                return $row ? reset($row) : null;
989 daniel-mar 170
        }
171
 
1116 daniel-mar 172
        /**
173
         * @param string $sql
174
         * @param array|null $prepared_args
175
         * @return OIDplusQueryResult
176
         * @throws OIDplusException
177
         */
1130 daniel-mar 178
        public final function query(string $sql, array $prepared_args=null): OIDplusQueryResult {
295 daniel-mar 179
 
180
                $query_logfile = OIDplus::baseConfig()->getValue('QUERY_LOGFILE', '');
181
                if (!empty($query_logfile)) {
182
                        $ts = explode(" ",microtime());
592 daniel-mar 183
                        $ts = date("Y-m-d H:i:s",intval($ts[1])).substr((string)$ts[0],1,4);
295 daniel-mar 184
                        static $log_session_id = "";
185
                        if (empty($log_session_id)) {
186
                                $log_session_id = rand(10000,99999);
187
                        }
188
                        $file = isset($_SERVER['REQUEST_URI']) ? ' | '.$_SERVER['REQUEST_URI'] : '';
1316 daniel-mar 189
                        // file_put_contents($query_logfile, "$ts <$log_session_id$file> $sql ".print_r($prepared_args,true)."\n", FILE_APPEND);
295 daniel-mar 190
                        file_put_contents($query_logfile, "$ts <$log_session_id$file> $sql\n", FILE_APPEND);
191
                }
192
 
193
                $this->last_query = $sql;
194
                $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
502 daniel-mar 195
 
1238 daniel-mar 196
                if ($this->slangDetectionDone) {
502 daniel-mar 197
                        $slang = $this->getSlang();
198
                        if ($slang) {
199
                                $sql = $slang->filterQuery($sql);
200
                        }
201
                }
202
 
1240 daniel-mar 203
                $res = $this->doQuery($sql, $prepared_args);
204
                if ($this->slangDetectionDone) $this->getSlang()->reviewResult($res, $sql, $prepared_args);
205
                return $res;
295 daniel-mar 206
        }
207
 
1116 daniel-mar 208
        /**
209
         * @return void
210
         * @throws OIDplusException
211
         */
295 daniel-mar 212
        public final function connect()/*: void*/ {
213
                if ($this->connected) return;
1177 daniel-mar 214
 
215
                $bcKeys = OIDplus::baseConfig()->getAllKeys();
216
                foreach ($bcKeys as $bkKey) {
217
                        $val = OIDplus::baseConfig()->getValue($bkKey, '');
218
                        if (is_string($val) && preg_match('@(userdata[/\\\\]database[/\\\\]oidplus_(empty|example)\\.(db|db3|sqlite|sqlite3|mdb|accdb))@i', $val, $m)) {
219
                                throw new OIDplusConfigInitializationException(_L('It looks like you are trying to use the template database file %1 in your base configuration. Since this file gets overridden by software updates, you must copy the template file and use this copy instead.', $m[1]));
220
                        }
221
                }
222
 
295 daniel-mar 223
                $this->beforeConnect();
224
                $this->doConnect();
225
                $this->connected = true;
639 daniel-mar 226
                OIDplus::register_shutdown_function(array($this, 'disconnect'));
318 daniel-mar 227
                $this->afterConnectMandatory();
295 daniel-mar 228
                $this->afterConnect();
229
        }
230
 
1116 daniel-mar 231
        /**
232
         * @return void
233
         */
295 daniel-mar 234
        public final function disconnect()/*: void*/ {
235
                if (!$this->connected) return;
236
                $this->beforeDisconnect();
237
                $this->doDisconnect();
238
                $this->connected = false;
239
                $this->afterDisconnect();
240
        }
241
 
1116 daniel-mar 242
        /**
243
         * @return void
244
         */
295 daniel-mar 245
        protected function beforeDisconnect()/*: void*/ {}
246
 
1116 daniel-mar 247
        /**
248
         * @return void
249
         */
295 daniel-mar 250
        protected function afterDisconnect()/*: void*/ {}
251
 
1116 daniel-mar 252
        /**
253
         * @return void
254
         */
295 daniel-mar 255
        protected function beforeConnect()/*: void*/ {}
256
 
1116 daniel-mar 257
        /**
258
         * @return void
259
         */
318 daniel-mar 260
        protected function afterConnect()/*: void*/ {}
261
 
1116 daniel-mar 262
        /**
263
         * @return void
264
         * @throws OIDplusConfigInitializationException
265
         * @throws OIDplusException
266
         */
318 daniel-mar 267
        private function afterConnectMandatory()/*: void*/ {
1238 daniel-mar 268
                // In case an auto-detection of the slang is required (for generic providers like PDO or ODBC),
269
                // we must not be inside a transaction, because the detection requires intentionally submitting
270
                // invalid queries to detect the correct DBMS. If we would be inside a transaction, providers like
271
                // PDO would automatically roll-back. Therefore, we detect the slang right at the beginning,
272
                // before any transaction is used.
273
                $this->getSlang();
274
 
295 daniel-mar 275
                // Check if the config table exists. This is important because the database version is stored in it
276
                $this->initRequireTables(array('config'));
277
 
278
                // Do the database tables need an update?
279
                // It is important that we do it immediately after connecting,
280
                // because the database structure might change and therefore various things might fail.
830 daniel-mar 281
                require_once __DIR__.'/../db_updates/run.inc.php';
282
                oidplus_dbupdate($this);
295 daniel-mar 283
 
284
                // Now that our database is up-to-date, we check if database tables are existing
285
                // without config table, because it was checked above
286
                $this->initRequireTables(array('objects', 'asn1id', 'iri', 'ra'/*, 'config'*/));
287
        }
288
 
1116 daniel-mar 289
        /**
290
         * @param string[] $tableNames
291
         * @return void
292
         * @throws OIDplusConfigInitializationException
293
         * @throws OIDplusException
294
         */
295
        private function initRequireTables(array $tableNames)/*: void*/ {
295 daniel-mar 296
                $msgs = array();
1241 daniel-mar 297
 
298
                // Check for a general database error like a locked file DBMS
299
                // which would raise a false warning "Table oidplus_config missing"
1447 daniel-mar 300
                // if we wouldn't do this fake query first.
1241 daniel-mar 301
                $this->query("select 0");
302
 
303
                $prefix = OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', '');
295 daniel-mar 304
                foreach ($tableNames as $tableName) {
305
                        if (!$this->tableExists($prefix.$tableName)) {
360 daniel-mar 306
                                $msgs[] = _L('Table %1 is missing!',$prefix.$tableName);
295 daniel-mar 307
                        }
308
                }
309
                if (count($msgs) > 0) {
310
                        throw new OIDplusConfigInitializationException(implode("\n\n",$msgs));
311
                }
312
        }
313
 
1116 daniel-mar 314
        /**
315
         * @param string $tableName
316
         * @return bool
317
         */
318
        public function tableExists(string $tableName): bool {
295 daniel-mar 319
                try {
318 daniel-mar 320
                        // Attention: This query could interrupt transactions if Rollback-On-Error is enabled
295 daniel-mar 321
                        $this->query("select 0 from ".$tableName." where 1=0");
322
                        return true;
1050 daniel-mar 323
                } catch (\Exception $e) {
295 daniel-mar 324
                        return false;
325
                }
326
        }
327
 
1116 daniel-mar 328
        /**
329
         * @return bool
330
         */
295 daniel-mar 331
        public function isConnected(): bool {
332
                return $this->connected;
333
        }
334
 
1116 daniel-mar 335
        /**
336
         * @param bool $html
337
         * @return void
338
         */
339
        public function init(bool $html = true)/*: void*/ {
295 daniel-mar 340
                $this->html = $html;
341
        }
342
 
1116 daniel-mar 343
        /**
344
         * @return string
345
         * @throws OIDplusException
346
         */
295 daniel-mar 347
        public function sqlDate(): string {
325 daniel-mar 348
                $slang = $this->getSlang();
349
                if (!is_null($slang)) {
350
                        return $slang->sqlDate();
295 daniel-mar 351
                } else {
1233 daniel-mar 352
                        // Fallback: Take the server date
489 daniel-mar 353
                        return "'" . date('Y-m-d H:i:s') . "'";
295 daniel-mar 354
                }
355
        }
356
 
1116 daniel-mar 357
        /**
358
         * @param bool $mustExist
359
         * @return OIDplusSqlSlangPlugin|null
360
         * @throws OIDplusConfigInitializationException
361
         * @throws OIDplusException
362
         */
502 daniel-mar 363
        protected function doGetSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ {
364
                $res = null;
326 daniel-mar 365
 
502 daniel-mar 366
                if (OIDplus::baseConfig()->exists('FORCE_DBMS_SLANG')) {
367
                        $name = OIDplus::baseConfig()->getValue('FORCE_DBMS_SLANG', '');
368
                        $res = OIDplus::getSqlSlangPlugin($name);
369
                        if ($mustExist && is_null($res)) {
370
                                throw new OIDplusConfigInitializationException(_L('Enforced SQL slang (via setting FORCE_DBMS_SLANG) "%1" does not exist.',$name));
371
                        }
372
                } else {
373
                        foreach (OIDplus::getSqlSlangPlugins() as $plugin) {
374
                                if ($plugin->detect($this)) {
375
                                        if (OIDplus::baseConfig()->getValue('DEBUG') && !is_null($res)) {
1160 daniel-mar 376
 
377
                                                $detected = array();
378
                                                foreach (OIDplus::getSqlSlangPlugins() as $plugin) {
379
                                                        if ($plugin->detect($this)) {
380
                                                                $detected[] = get_class($plugin);
381
                                                        }
382
                                                }
383
 
384
                                                throw new OIDplusException(_L('DB-Slang detection failed: Multiple slangs were detected (%1). Use base config setting FORCE_DBMS_SLANG to define one.', implode(', ',$detected)));
502 daniel-mar 385
                                        }
386
 
387
                                        $res = $plugin;
388
 
389
                                        if (!OIDplus::baseConfig()->getValue('DEBUG')) {
295 daniel-mar 390
                                                break;
391
                                        }
392
                                }
393
                        }
502 daniel-mar 394
                        if ($mustExist && is_null($res)) {
395
                                throw new OIDplusException(_L('Cannot determine the SQL slang of your DBMS. Your DBMS is probably not supported.'));
396
                        }
295 daniel-mar 397
                }
398
 
502 daniel-mar 399
                return $res;
400
        }
401
 
1116 daniel-mar 402
        /**
403
         * @param bool $mustExist
404
         * @return OIDplusSqlSlangPlugin|null
405
         * @throws OIDplusConfigInitializationException
406
         * @throws OIDplusException
407
         */
502 daniel-mar 408
        public final function getSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ {
409
                if ($this->slangDetectionDone) {
1450 daniel-mar 410
                        return $this->slangCache;
502 daniel-mar 411
                }
412
 
1450 daniel-mar 413
                $this->slangCache = $this->doGetSlang();
502 daniel-mar 414
                $this->slangDetectionDone = true;
1450 daniel-mar 415
                return $this->slangCache;
295 daniel-mar 416
        }
1220 daniel-mar 417
 
418
        /**
419
         * @return array
420
         */
421
        public function getExtendedInfo(): array {
422
                return array();
423
        }
489 daniel-mar 424
}