Subversion Repositories oidplus

Rev

Rev 1370 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
635 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
1086 daniel-mar 5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
635 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;
635 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
 
635 daniel-mar 26
class OIDplusDatabaseConnectionODBC extends OIDplusDatabaseConnection {
1130 daniel-mar 27
        /**
28
         * @var mixed|null
29
         */
30
        private $conn = null;
31
 
32
        /**
33
         * @var string|null
34
         */
635 daniel-mar 35
        private $last_error = null; // do the same like MySQL+PDO, just to be equal in the behavior
1130 daniel-mar 36
 
37
        /**
38
         * @var bool
39
         */
635 daniel-mar 40
        private $transactions_supported = false;
41
 
1116 daniel-mar 42
        /**
43
         * @return bool|null
44
         * @throws OIDplusException
45
         */
635 daniel-mar 46
        protected function forcePrepareEmulation() {
47
                $mode = OIDplus::baseConfig()->getValue('PREPARED_STATEMENTS_EMULATION', 'auto');
48
                if ($mode === 'on') return true;
49
                if ($mode === 'off') return false;
50
 
51
                static $res = null;
52
                if (is_null($res)) {
53
                        $sql = 'select name from ###config where name = ?';
54
                        $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
55
                        $res = @odbc_prepare($this->conn, $sql) === false;
56
                }
57
 
58
                return $res;
59
        }
60
 
1116 daniel-mar 61
        /**
62
         * @param string $sql
63
         * @param array|null $prepared_args
64
         * @return OIDplusQueryResultODBC
65
         * @throws OIDplusConfigInitializationException
66
         * @throws OIDplusException
67
         * @throws OIDplusSQLException
68
         */
69
        protected function doQueryInternalPrepare(string $sql, array $prepared_args=null): OIDplusQueryResultODBC {
1219 daniel-mar 70
                foreach ($prepared_args as &$value) {
71
                        // ODBC/SQLServer has problems converting "true" to the data type "bit"
72
                        // Error "Invalid character value for cast specification"
73
                        if (is_bool($value)) {
74
                                if ($this->slangDetectionDone) {
75
                                        $value = $this->getSlang()->getSQLBool($value);
76
                                } else {
77
                                        $value = $value ? '1' : '0';
949 daniel-mar 78
                                }
1219 daniel-mar 79
                        }
80
                }
1316 daniel-mar 81
                unset($value);
635 daniel-mar 82
 
1219 daniel-mar 83
                $ps = @odbc_prepare($this->conn, $sql);
84
                if (!$ps) {
85
                        // If preparation fails, try the emulation
86
                        // For example, SQL Server ODBC Driver cannot have "?" in a subquery,
87
                        // otherwise you receive the error message
88
                        // "Syntax error or access violation" on odbc_prepare()
89
                        return $this->doQueryPrepareEmulation($sql, $prepared_args);
90
                        /*
91
                        $this->last_error = odbc_errormsg($this->conn);
92
                        throw new OIDplusSQLException($sql, _L('Cannot prepare statement').': '.$this->error());
93
                        */
94
                }
635 daniel-mar 95
 
1219 daniel-mar 96
                if (!@odbc_execute($ps, $prepared_args)) {
97
                        $this->last_error = odbc_errormsg($this->conn);
98
                        throw new OIDplusSQLException($sql, $this->error());
99
                }
100
                return new OIDplusQueryResultODBC($ps);
949 daniel-mar 101
        }
102
 
1116 daniel-mar 103
        /**
104
         * @param string $sql
105
         * @param array|null $prepared_args
106
         * @return OIDplusQueryResultODBC
107
         * @throws OIDplusConfigInitializationException
108
         * @throws OIDplusException
109
         * @throws OIDplusSQLException
110
         */
111
        protected function doQueryPrepareEmulation(string $sql, array $prepared_args=null): OIDplusQueryResultODBC {
1219 daniel-mar 112
                // For some drivers (e.g. Microsoft Access), we need to do this kind of emulation, because odbc_prepare() does not work
1389 daniel-mar 113
                $dummy = find_nonexisting_substr($sql);
114
                $sql = str_replace('?', $dummy, $sql);
1219 daniel-mar 115
                foreach ($prepared_args as $arg) {
1389 daniel-mar 116
                        $needle = $dummy;
1219 daniel-mar 117
                        if (is_bool($arg)) {
118
                                if ($this->slangDetectionDone) {
119
                                        $replace = $this->getSlang()->getSQLBool($arg);
120
                                } else {
121
                                        $replace = $arg ? '1' : '0';
635 daniel-mar 122
                                }
1219 daniel-mar 123
                        } else if (is_int($arg)) {
124
                                $replace = $arg;
125
                        } else if (is_float($arg)) {
126
                                $replace = number_format($arg, 10, '.', '');
1350 daniel-mar 127
                        } else if (is_null($arg)) {
128
                                $replace = 'NULL';
1219 daniel-mar 129
                        } else {
130
                                // TODO: More types?
1370 daniel-mar 131
                                // Note: Actually, the strings should be N'...' instead of '...',
132
                                //       but since Unicode does not work with ODBC, it does not make a difference
1219 daniel-mar 133
                                if ($this->slangDetectionDone) {
1350 daniel-mar 134
                                        $replace = "'".$this->getSlang()->escapeString($arg)."'";
1219 daniel-mar 135
                                } else {
136
                                        $replace = "'".str_replace("'", "''", $arg)."'";
635 daniel-mar 137
                                }
1219 daniel-mar 138
                        }
139
                        $pos = strpos($sql, $needle);
140
                        if ($pos !== false) {
141
                                $sql = substr_replace($sql, $replace, $pos, strlen($needle));
142
                        }
143
                }
1389 daniel-mar 144
                $sql = str_replace($dummy, '?', $sql);
1219 daniel-mar 145
                $ps = @odbc_exec($this->conn, $sql);
146
                if (!$ps) {
147
                        $this->last_error = odbc_errormsg($this->conn);
148
                        throw new OIDplusSQLException($sql, _L('Cannot prepare statement').': '.$this->error());
149
                }
150
                return new OIDplusQueryResultODBC($ps);
949 daniel-mar 151
        }
635 daniel-mar 152
 
1116 daniel-mar 153
        /**
154
         * @param string $sql
155
         * @param array|null $prepared_args
156
         * @return OIDplusQueryResultODBC
157
         * @throws OIDplusException
158
         */
159
        public function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult {
949 daniel-mar 160
                $this->last_error = null;
161
                if (is_null($prepared_args)) {
162
                        $res = @odbc_exec($this->conn, $sql);
635 daniel-mar 163
 
949 daniel-mar 164
                        if ($res === false) {
165
                                $this->last_error = odbc_errormsg($this->conn);
166
                                throw new OIDplusSQLException($sql, $this->error());
167
                        } else {
168
                                return new OIDplusQueryResultODBC($res);
635 daniel-mar 169
                        }
949 daniel-mar 170
                } else {
171
                        if ($this->forcePrepareEmulation()) {
172
                                return $this->doQueryPrepareEmulation($sql, $prepared_args);
173
                        } else {
174
                                return $this->doQueryInternalPrepare($sql, $prepared_args);
175
                        }
635 daniel-mar 176
                }
177
        }
178
 
1116 daniel-mar 179
        /**
180
         * @return string
181
         */
635 daniel-mar 182
        public function error(): string {
183
                $err = $this->last_error;
184
                if ($err == null) $err = '';
1116 daniel-mar 185
                return vts_utf8_encode($err); // UTF-8 encode, because ODBC might output weird stuff ...
635 daniel-mar 186
        }
187
 
1116 daniel-mar 188
        /**
189
         * @return void
190
         * @throws OIDplusConfigInitializationException
191
         * @throws OIDplusException
192
         */
635 daniel-mar 193
        protected function doConnect()/*: void*/ {
194
                if (!function_exists('odbc_connect')) throw new OIDplusConfigInitializationException(_L('PHP extension "%1" not installed','ODBC'));
195
 
196
                // Try connecting to the database
1370 daniel-mar 197
                $dsn      = OIDplus::baseConfig()->getValue('ODBC_DSN',      'DRIVER={SQL Server};SERVER=localhost;DATABASE=oidplus');
635 daniel-mar 198
                $username = OIDplus::baseConfig()->getValue('ODBC_USERNAME', '');
199
                $password = OIDplus::baseConfig()->getValue('ODBC_PASSWORD', '');
1216 daniel-mar 200
 
1217 daniel-mar 201
                // Try to extend DSN with charset
1370 daniel-mar 202
                // Note: For MySQL, must be utf8mb4 or utf8, and not UTF-8
1217 daniel-mar 203
                if (stripos($dsn,"charset=") === false) {
1218 daniel-mar 204
                        $this->conn = odbc_connect("$dsn;charset=utf8mb4", $username, $password);
205
                        if (!$this->conn) $this->conn = odbc_connect("$dsn;charset=utf8", $username, $password);
206
                        if (!$this->conn) $this->conn = odbc_connect("$dsn;charset=UTF-8", $username, $password);
207
                        if (!$this->conn) $this->conn = odbc_connect($dsn, $username, $password);
208
                } else {
209
                        $this->conn = odbc_connect($dsn, $username, $password);
1217 daniel-mar 210
                }
1216 daniel-mar 211
 
635 daniel-mar 212
                if (!$this->conn) {
213
                        $message = odbc_errormsg();
1171 daniel-mar 214
                        $message = vts_utf8_encode($message); // Make UTF-8 if it is NOT already UTF-8. Important for German Microsoft Access.
863 daniel-mar 215
                        throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
635 daniel-mar 216
                }
217
 
218
                $this->last_error = null;
219
 
220
                try {
1216 daniel-mar 221
                        @odbc_exec($this->conn, "SET NAMES 'UTF-8'"); // Does most likely NOT work with ODBC. Try adding ";CHARSET=UTF8" (or similar) to the DSN
1050 daniel-mar 222
                } catch (\Exception $e) {
635 daniel-mar 223
                }
224
 
1216 daniel-mar 225
                try {
226
                        @odbc_exec($this->conn, "SET CHARACTER SET 'UTF-8'"); // Does most likely NOT work with ODBC. Try adding ";CHARSET=UTF8" (or similar) to the DSN
227
                } catch (\Exception $e) {
228
                }
229
 
230
                try {
231
                        @odbc_exec($this->conn, "SET NAMES 'utf8mb4'"); // Does most likely NOT work with ODBC. Try adding ";CHARSET=UTF8" (or similar) to the DSN
232
                } catch (\Exception $e) {
233
                }
234
 
635 daniel-mar 235
                // We check if the DBMS supports autocommit.
236
                // Attention: Check it after you have sent a query already, because Microsoft Access doesn't seem to allow
237
                // changing auto commit once a query was executed ("Attribute cannot be set now SQLState: S1011")
238
                // Note: For some weird reason we *DO* need to redirect the output to "$dummy", otherwise it won't work!
239
                $sql = "select name from ###config where 1=0";
240
                $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
241
                $dummy = @odbc_exec($this->conn, $sql);
242
                $this->transactions_supported = @odbc_autocommit($this->conn, false);
243
                @odbc_autocommit($this->conn, true);
244
        }
245
 
1116 daniel-mar 246
        /**
247
         * @return void
248
         */
635 daniel-mar 249
        protected function doDisconnect()/*: void*/ {
250
                if (!is_null($this->conn)) {
251
                        @odbc_close($this->conn);
252
                        $this->conn = null;
253
                }
254
        }
255
 
1116 daniel-mar 256
        /**
257
         * @var bool
258
         */
635 daniel-mar 259
        private $intransaction = false;
260
 
1116 daniel-mar 261
        /**
262
         * @return bool
263
         */
635 daniel-mar 264
        public function transaction_supported(): bool {
265
                return $this->transactions_supported;
266
        }
267
 
1116 daniel-mar 268
        /**
269
         * @return int
270
         */
635 daniel-mar 271
        public function transaction_level(): int {
272
                if (!$this->transaction_supported()) {
273
                        // TODO?
274
                        return 0;
275
                }
276
                return $this->intransaction ? 1 : 0;
277
        }
278
 
1116 daniel-mar 279
        /**
280
         * @return void
281
         * @throws OIDplusException
282
         */
635 daniel-mar 283
        public function transaction_begin()/*: void*/ {
284
                if (!$this->transaction_supported()) {
285
                        // TODO?
286
                        return;
287
                }
288
                if ($this->intransaction) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
289
                odbc_autocommit($this->conn, false); // begin transaction
290
                $this->intransaction = true;
291
        }
292
 
1116 daniel-mar 293
        /**
294
         * @return void
295
         */
635 daniel-mar 296
        public function transaction_commit()/*: void*/ {
297
                if (!$this->transaction_supported()) {
298
                        // TODO?
299
                        return;
300
                }
301
                odbc_commit($this->conn);
302
                odbc_autocommit($this->conn, true);
303
                $this->intransaction = false;
304
        }
305
 
1116 daniel-mar 306
        /**
307
         * @return void
308
         */
635 daniel-mar 309
        public function transaction_rollback()/*: void*/ {
310
                if (!$this->transaction_supported()) {
311
                        // TODO?
312
                        return;
313
                }
314
                odbc_rollback($this->conn);
315
                odbc_autocommit($this->conn, true);
316
                $this->intransaction = false;
317
        }
1220 daniel-mar 318
 
319
        /**
320
         * @return array
321
         */
322
        public function getExtendedInfo(): array {
323
                $dsn = OIDplus::baseConfig()->getValue('ODBC_DSN', 'DRIVER={SQL Server};SERVER=localhost;DATABASE=oidplus;CHARSET=UTF8');
1231 daniel-mar 324
                $dsn = preg_replace('@(Password|PWD)=(.+);@ismU', '('._L('redacted').');', $dsn);
1220 daniel-mar 325
                return array(
326
                        _L('DSN') => $dsn
327
                );
328
        }
329
 
635 daniel-mar 330
}