Subversion Repositories oidplus

Rev

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