Subversion Repositories oidplus

Rev

Rev 1116 | Rev 1171 | 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 {
949 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';
78
                                                }
79
                                        }
80
                                }
635 daniel-mar 81
 
949 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
 
949 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);
100
 
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 {
635 daniel-mar 112
                                // For some drivers (e.g. Microsoft Access), we need to do this kind of emulation, because odbc_prepare() does not work
113
                                $sql = str_replace('?', chr(1), $sql);
114
                                foreach ($prepared_args as $arg) {
115
                                        $needle = chr(1);
116
                                        if (is_bool($arg)) {
117
                                                if ($this->slangDetectionDone) {
118
                                                        $replace = $this->getSlang()->getSQLBool($arg);
119
                                                } else {
120
                                                        $replace = $arg ? '1' : '0';
121
                                                }
122
                                        } else {
123
                                                if ($this->slangDetectionDone) {
124
                                                        $replace = "'".$this->getSlang()->escapeString($arg)."'"; // TODO: types
125
                                                } else {
126
                                                        $replace = "'".str_replace("'", "''", $arg)."'"; // TODO: types
127
                                                }
128
                                        }
129
                                        $pos = strpos($sql, $needle);
130
                                        if ($pos !== false) {
131
                                                $sql = substr_replace($sql, $replace, $pos, strlen($needle));
132
                                        }
133
                                }
134
                                $sql = str_replace(chr(1), '?', $sql);
135
                                $ps = @odbc_exec($this->conn, $sql);
136
                                if (!$ps) {
137
                                        $this->last_error = odbc_errormsg($this->conn);
138
                                        throw new OIDplusSQLException($sql, _L('Cannot prepare statement').': '.$this->error());
139
                                }
140
                                return new OIDplusQueryResultODBC($ps);
949 daniel-mar 141
        }
635 daniel-mar 142
 
1116 daniel-mar 143
        /**
144
         * @param string $sql
145
         * @param array|null $prepared_args
146
         * @return OIDplusQueryResultODBC
147
         * @throws OIDplusException
148
         */
149
        public function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult {
949 daniel-mar 150
                $this->last_error = null;
151
                if (is_null($prepared_args)) {
152
                        $res = @odbc_exec($this->conn, $sql);
635 daniel-mar 153
 
949 daniel-mar 154
                        if ($res === false) {
155
                                $this->last_error = odbc_errormsg($this->conn);
156
                                throw new OIDplusSQLException($sql, $this->error());
157
                        } else {
158
                                return new OIDplusQueryResultODBC($res);
635 daniel-mar 159
                        }
949 daniel-mar 160
                } else {
161
                        if (!is_array($prepared_args)) {
162
                                throw new OIDplusException(_L('"prepared_args" must be either NULL or an ARRAY.'));
163
                        }
164
 
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', '');
194
                $this->conn = @odbc_connect($dsn, $username, $password);
195
 
196
                if (!$this->conn) {
197
                        $message = odbc_errormsg();
863 daniel-mar 198
                        throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
635 daniel-mar 199
                }
200
 
201
                $this->last_error = null;
202
 
203
                try {
204
                        @odbc_exec($this->conn, "SET NAMES 'utf8'"); // Does most likely NOT work with ODBC. Try adding ";CHARSET=UTF8" (or similar) to the DSN
1050 daniel-mar 205
                } catch (\Exception $e) {
635 daniel-mar 206
                }
207
 
208
                // We check if the DBMS supports autocommit.
209
                // Attention: Check it after you have sent a query already, because Microsoft Access doesn't seem to allow
210
                // changing auto commit once a query was executed ("Attribute cannot be set now SQLState: S1011")
211
                // Note: For some weird reason we *DO* need to redirect the output to "$dummy", otherwise it won't work!
212
                $sql = "select name from ###config where 1=0";
213
                $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
214
                $dummy = @odbc_exec($this->conn, $sql);
215
                $this->transactions_supported = @odbc_autocommit($this->conn, false);
216
                @odbc_autocommit($this->conn, true);
217
        }
218
 
1116 daniel-mar 219
        /**
220
         * @return void
221
         */
635 daniel-mar 222
        protected function doDisconnect()/*: void*/ {
223
                if (!is_null($this->conn)) {
224
                        @odbc_close($this->conn);
225
                        $this->conn = null;
226
                }
227
        }
228
 
1116 daniel-mar 229
        /**
230
         * @var bool
231
         */
635 daniel-mar 232
        private $intransaction = false;
233
 
1116 daniel-mar 234
        /**
235
         * @return bool
236
         */
635 daniel-mar 237
        public function transaction_supported(): bool {
238
                return $this->transactions_supported;
239
        }
240
 
1116 daniel-mar 241
        /**
242
         * @return int
243
         */
635 daniel-mar 244
        public function transaction_level(): int {
245
                if (!$this->transaction_supported()) {
246
                        // TODO?
247
                        return 0;
248
                }
249
                return $this->intransaction ? 1 : 0;
250
        }
251
 
1116 daniel-mar 252
        /**
253
         * @return void
254
         * @throws OIDplusException
255
         */
635 daniel-mar 256
        public function transaction_begin()/*: void*/ {
257
                if (!$this->transaction_supported()) {
258
                        // TODO?
259
                        return;
260
                }
261
                if ($this->intransaction) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
262
                odbc_autocommit($this->conn, false); // begin transaction
263
                $this->intransaction = true;
264
        }
265
 
1116 daniel-mar 266
        /**
267
         * @return void
268
         */
635 daniel-mar 269
        public function transaction_commit()/*: void*/ {
270
                if (!$this->transaction_supported()) {
271
                        // TODO?
272
                        return;
273
                }
274
                odbc_commit($this->conn);
275
                odbc_autocommit($this->conn, true);
276
                $this->intransaction = false;
277
        }
278
 
1116 daniel-mar 279
        /**
280
         * @return void
281
         */
635 daniel-mar 282
        public function transaction_rollback()/*: void*/ {
283
                if (!$this->transaction_supported()) {
284
                        // TODO?
285
                        return;
286
                }
287
                odbc_rollback($this->conn);
288
                odbc_autocommit($this->conn, true);
289
                $this->intransaction = false;
290
        }
291
}