Subversion Repositories oidplus

Rev

Rev 1086 | Rev 1130 | 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 {
27
        private $conn;
28
        private $last_error = null; // do the same like MySQL+PDO, just to be equal in the behavior
29
        private $transactions_supported = false;
30
 
1116 daniel-mar 31
        /**
32
         * @return bool|null
33
         * @throws OIDplusException
34
         */
635 daniel-mar 35
        protected function forcePrepareEmulation() {
36
                $mode = OIDplus::baseConfig()->getValue('PREPARED_STATEMENTS_EMULATION', 'auto');
37
                if ($mode === 'on') return true;
38
                if ($mode === 'off') return false;
39
 
40
                static $res = null;
41
                if (is_null($res)) {
42
                        $sql = 'select name from ###config where name = ?';
43
                        $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
44
                        $res = @odbc_prepare($this->conn, $sql) === false;
45
                }
46
 
47
                return $res;
48
        }
49
 
1116 daniel-mar 50
        /**
51
         * @param string $sql
52
         * @param array|null $prepared_args
53
         * @return OIDplusQueryResultODBC
54
         * @throws OIDplusConfigInitializationException
55
         * @throws OIDplusException
56
         * @throws OIDplusSQLException
57
         */
58
        protected function doQueryInternalPrepare(string $sql, array $prepared_args=null): OIDplusQueryResultODBC {
949 daniel-mar 59
                                foreach ($prepared_args as &$value) {
60
                                        // ODBC/SQLServer has problems converting "true" to the data type "bit"
61
                                        // Error "Invalid character value for cast specification"
62
                                        if (is_bool($value)) {
63
                                                if ($this->slangDetectionDone) {
64
                                                        $value = $this->getSlang()->getSQLBool($value);
65
                                                } else {
66
                                                        $value = $value ? '1' : '0';
67
                                                }
68
                                        }
69
                                }
635 daniel-mar 70
 
949 daniel-mar 71
                                $ps = @odbc_prepare($this->conn, $sql);
72
                                if (!$ps) {
73
                                        // If preparation fails, try the emulation
74
                                        // For example, SQL Server ODBC Driver cannot have "?" in a subquery,
75
                                        // otherwise you receive the error message
76
                                        // "Syntax error or access violation" on odbc_prepare()
77
                                        return $this->doQueryPrepareEmulation($sql, $prepared_args);
78
                                        /*
79
                                        $this->last_error = odbc_errormsg($this->conn);
80
                                        throw new OIDplusSQLException($sql, _L('Cannot prepare statement').': '.$this->error());
81
                                        */
82
                                }
635 daniel-mar 83
 
949 daniel-mar 84
                                if (!@odbc_execute($ps, $prepared_args)) {
85
                                        $this->last_error = odbc_errormsg($this->conn);
86
                                        throw new OIDplusSQLException($sql, $this->error());
87
                                }
88
                                return new OIDplusQueryResultODBC($ps);
89
 
90
        }
91
 
1116 daniel-mar 92
        /**
93
         * @param string $sql
94
         * @param array|null $prepared_args
95
         * @return OIDplusQueryResultODBC
96
         * @throws OIDplusConfigInitializationException
97
         * @throws OIDplusException
98
         * @throws OIDplusSQLException
99
         */
100
        protected function doQueryPrepareEmulation(string $sql, array $prepared_args=null): OIDplusQueryResultODBC {
635 daniel-mar 101
                                // For some drivers (e.g. Microsoft Access), we need to do this kind of emulation, because odbc_prepare() does not work
102
                                $sql = str_replace('?', chr(1), $sql);
103
                                foreach ($prepared_args as $arg) {
104
                                        $needle = chr(1);
105
                                        if (is_bool($arg)) {
106
                                                if ($this->slangDetectionDone) {
107
                                                        $replace = $this->getSlang()->getSQLBool($arg);
108
                                                } else {
109
                                                        $replace = $arg ? '1' : '0';
110
                                                }
111
                                        } else {
112
                                                if ($this->slangDetectionDone) {
113
                                                        $replace = "'".$this->getSlang()->escapeString($arg)."'"; // TODO: types
114
                                                } else {
115
                                                        $replace = "'".str_replace("'", "''", $arg)."'"; // TODO: types
116
                                                }
117
                                        }
118
                                        $pos = strpos($sql, $needle);
119
                                        if ($pos !== false) {
120
                                                $sql = substr_replace($sql, $replace, $pos, strlen($needle));
121
                                        }
122
                                }
123
                                $sql = str_replace(chr(1), '?', $sql);
124
                                $ps = @odbc_exec($this->conn, $sql);
125
                                if (!$ps) {
126
                                        $this->last_error = odbc_errormsg($this->conn);
127
                                        throw new OIDplusSQLException($sql, _L('Cannot prepare statement').': '.$this->error());
128
                                }
129
                                return new OIDplusQueryResultODBC($ps);
949 daniel-mar 130
        }
635 daniel-mar 131
 
1116 daniel-mar 132
        /**
133
         * @param string $sql
134
         * @param array|null $prepared_args
135
         * @return OIDplusQueryResultODBC
136
         * @throws OIDplusException
137
         */
138
        public function doQuery(string $sql, array $prepared_args=null): OIDplusQueryResult {
949 daniel-mar 139
                $this->last_error = null;
140
                if (is_null($prepared_args)) {
141
                        $res = @odbc_exec($this->conn, $sql);
635 daniel-mar 142
 
949 daniel-mar 143
                        if ($res === false) {
144
                                $this->last_error = odbc_errormsg($this->conn);
145
                                throw new OIDplusSQLException($sql, $this->error());
146
                        } else {
147
                                return new OIDplusQueryResultODBC($res);
635 daniel-mar 148
                        }
949 daniel-mar 149
                } else {
150
                        if (!is_array($prepared_args)) {
151
                                throw new OIDplusException(_L('"prepared_args" must be either NULL or an ARRAY.'));
152
                        }
153
 
154
                        if ($this->forcePrepareEmulation()) {
155
                                return $this->doQueryPrepareEmulation($sql, $prepared_args);
156
                        } else {
157
                                return $this->doQueryInternalPrepare($sql, $prepared_args);
158
                        }
635 daniel-mar 159
                }
160
        }
161
 
1116 daniel-mar 162
        /**
163
         * @return string
164
         */
635 daniel-mar 165
        public function error(): string {
166
                $err = $this->last_error;
167
                if ($err == null) $err = '';
1116 daniel-mar 168
                return vts_utf8_encode($err); // UTF-8 encode, because ODBC might output weird stuff ...
635 daniel-mar 169
        }
170
 
1116 daniel-mar 171
        /**
172
         * @return void
173
         * @throws OIDplusConfigInitializationException
174
         * @throws OIDplusException
175
         */
635 daniel-mar 176
        protected function doConnect()/*: void*/ {
177
                if (!function_exists('odbc_connect')) throw new OIDplusConfigInitializationException(_L('PHP extension "%1" not installed','ODBC'));
178
 
179
                // Try connecting to the database
180
                $dsn      = OIDplus::baseConfig()->getValue('ODBC_DSN',      'DRIVER={SQL Server};SERVER=localhost;DATABASE=oidplus;CHARSET=UTF8');
181
                $username = OIDplus::baseConfig()->getValue('ODBC_USERNAME', '');
182
                $password = OIDplus::baseConfig()->getValue('ODBC_PASSWORD', '');
183
                $this->conn = @odbc_connect($dsn, $username, $password);
184
 
185
                if (!$this->conn) {
186
                        $message = odbc_errormsg();
863 daniel-mar 187
                        throw new OIDplusConfigInitializationException(trim(_L('Connection to the database failed!').' '.$message));
635 daniel-mar 188
                }
189
 
190
                $this->last_error = null;
191
 
192
                try {
193
                        @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 194
                } catch (\Exception $e) {
635 daniel-mar 195
                }
196
 
197
                // We check if the DBMS supports autocommit.
198
                // Attention: Check it after you have sent a query already, because Microsoft Access doesn't seem to allow
199
                // changing auto commit once a query was executed ("Attribute cannot be set now SQLState: S1011")
200
                // Note: For some weird reason we *DO* need to redirect the output to "$dummy", otherwise it won't work!
201
                $sql = "select name from ###config where 1=0";
202
                $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql);
203
                $dummy = @odbc_exec($this->conn, $sql);
204
                $this->transactions_supported = @odbc_autocommit($this->conn, false);
205
                @odbc_autocommit($this->conn, true);
206
        }
207
 
1116 daniel-mar 208
        /**
209
         * @return void
210
         */
635 daniel-mar 211
        protected function doDisconnect()/*: void*/ {
212
                if (!is_null($this->conn)) {
213
                        @odbc_close($this->conn);
214
                        $this->conn = null;
215
                }
216
        }
217
 
1116 daniel-mar 218
        /**
219
         * @var bool
220
         */
635 daniel-mar 221
        private $intransaction = false;
222
 
1116 daniel-mar 223
        /**
224
         * @return bool
225
         */
635 daniel-mar 226
        public function transaction_supported(): bool {
227
                return $this->transactions_supported;
228
        }
229
 
1116 daniel-mar 230
        /**
231
         * @return int
232
         */
635 daniel-mar 233
        public function transaction_level(): int {
234
                if (!$this->transaction_supported()) {
235
                        // TODO?
236
                        return 0;
237
                }
238
                return $this->intransaction ? 1 : 0;
239
        }
240
 
1116 daniel-mar 241
        /**
242
         * @return void
243
         * @throws OIDplusException
244
         */
635 daniel-mar 245
        public function transaction_begin()/*: void*/ {
246
                if (!$this->transaction_supported()) {
247
                        // TODO?
248
                        return;
249
                }
250
                if ($this->intransaction) throw new OIDplusException(_L('Nested transactions are not supported by this database plugin.'));
251
                odbc_autocommit($this->conn, false); // begin transaction
252
                $this->intransaction = true;
253
        }
254
 
1116 daniel-mar 255
        /**
256
         * @return void
257
         */
635 daniel-mar 258
        public function transaction_commit()/*: void*/ {
259
                if (!$this->transaction_supported()) {
260
                        // TODO?
261
                        return;
262
                }
263
                odbc_commit($this->conn);
264
                odbc_autocommit($this->conn, true);
265
                $this->intransaction = false;
266
        }
267
 
1116 daniel-mar 268
        /**
269
         * @return void
270
         */
635 daniel-mar 271
        public function transaction_rollback()/*: void*/ {
272
                if (!$this->transaction_supported()) {
273
                        // TODO?
274
                        return;
275
                }
276
                odbc_rollback($this->conn);
277
                odbc_autocommit($this->conn, true);
278
                $this->intransaction = false;
279
        }
280
}