Subversion Repositories oidplus

Rev

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

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