Subversion Repositories oidplus

Rev

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