Rev 990 | Rev 1086 | Go to most recent revision | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed
Rev 990 | Rev 1050 | ||
---|---|---|---|
1 | <?php |
1 | <?php |
2 | 2 | ||
3 | /* |
3 | /* |
4 | * OIDplus 2.0 |
4 | * OIDplus 2.0 |
5 | * Copyright 2019 - 2021 Daniel Marschall, ViaThinkSoft |
5 | * Copyright 2019 - 2021 Daniel Marschall, ViaThinkSoft |
6 | * |
6 | * |
7 | * Licensed under the Apache License, Version 2.0 (the "License"); |
7 | * Licensed under the Apache License, Version 2.0 (the "License"); |
8 | * you may not use this file except in compliance with 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 |
9 | * You may obtain a copy of the License at |
10 | * |
10 | * |
11 | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | * http://www.apache.org/licenses/LICENSE-2.0 |
12 | * |
12 | * |
13 | * Unless required by applicable law or agreed to in writing, software |
13 | * Unless required by applicable law or agreed to in writing, software |
14 | * distributed under the License is distributed on an "AS IS" BASIS, |
14 | * distributed under the License is distributed on an "AS IS" BASIS, |
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
16 | * See the License for the specific language governing permissions and |
16 | * See the License for the specific language governing permissions and |
17 | * limitations under the License. |
17 | * limitations under the License. |
18 | */ |
18 | */ |
19 | 19 | ||
20 | if (!defined('INSIDE_OIDPLUS')) die(); |
20 | namespace ViaThinkSoft\OIDplus; |
21 | 21 | ||
22 | abstract class OIDplusDatabaseConnection extends OIDplusBaseClass { |
22 | abstract class OIDplusDatabaseConnection extends OIDplusBaseClass { |
23 | protected /*bool*/ $connected = false; |
23 | protected /*bool*/ $connected = false; |
24 | protected /*?bool*/ $html = null; |
24 | protected /*?bool*/ $html = null; |
25 | protected /*?string*/ $last_query = null; |
25 | protected /*?string*/ $last_query = null; |
26 | protected /*bool*/ $slangDetectionDone = false; |
26 | protected /*bool*/ $slangDetectionDone = false; |
27 | 27 | ||
28 | protected abstract function doQuery(string $sql, /*?array*/ $prepared_args=null): OIDplusQueryResult; |
28 | protected abstract function doQuery(string $sql, /*?array*/ $prepared_args=null): OIDplusQueryResult; |
29 | public abstract function error(): string; |
29 | public abstract function error(): string; |
30 | public abstract function transaction_begin()/*: void*/; |
30 | public abstract function transaction_begin()/*: void*/; |
31 | public abstract function transaction_commit()/*: void*/; |
31 | public abstract function transaction_commit()/*: void*/; |
32 | public abstract function transaction_rollback()/*: void*/; |
32 | public abstract function transaction_rollback()/*: void*/; |
33 | public abstract function transaction_supported(): bool; |
33 | public abstract function transaction_supported(): bool; |
34 | public abstract function transaction_level(): int; |
34 | public abstract function transaction_level(): int; |
35 | protected abstract function doConnect()/*: void*/; |
35 | protected abstract function doConnect()/*: void*/; |
36 | protected abstract function doDisconnect()/*: void*/; |
36 | protected abstract function doDisconnect()/*: void*/; |
37 | 37 | ||
38 | public function getPlugin()/*: ?OIDplusDatabasePlugin*/ { |
38 | public function getPlugin()/*: ?OIDplusDatabasePlugin*/ { |
39 | $res = null; |
39 | $res = null; |
40 | $plugins = OIDplus::getDatabasePlugins(); |
40 | $plugins = OIDplus::getDatabasePlugins(); |
41 | foreach ($plugins as $plugin) { |
41 | foreach ($plugins as $plugin) { |
42 | if (get_class($this) == get_class($plugin::newConnection($this))) { |
42 | if (get_class($this) == get_class($plugin::newConnection($this))) { |
43 | return $plugin; |
43 | return $plugin; |
44 | } |
44 | } |
45 | } |
45 | } |
46 | return $res; |
46 | return $res; |
47 | } |
47 | } |
48 | 48 | ||
49 | public function insert_id(): int { |
49 | public function insert_id(): int { |
50 | // This is the "fallback" variant. If your database provider (e.g. PDO) supports |
50 | // This is the "fallback" variant. If your database provider (e.g. PDO) supports |
51 | // a function to detect the last inserted id, please override this |
51 | // a function to detect the last inserted id, please override this |
52 | // function in order to use that specialized function (since it is usually |
52 | // function in order to use that specialized function (since it is usually |
53 | // more reliable). |
53 | // more reliable). |
54 | return $this->getSlang()->insert_id($this); |
54 | return $this->getSlang()->insert_id($this); |
55 | } |
55 | } |
56 | 56 | ||
57 | public final function getTable(string $sql) { |
57 | public final function getTable(string $sql) { |
58 | $out = array(); |
58 | $out = array(); |
59 | $res = $this->query($sql); |
59 | $res = $this->query($sql); |
60 | while ($row = $res->fetch_array()) { |
60 | while ($row = $res->fetch_array()) { |
61 | $out[] = $row; |
61 | $out[] = $row; |
62 | } |
62 | } |
63 | return $out; |
63 | return $out; |
64 | } |
64 | } |
65 | 65 | ||
66 | public final function getScalar(string $sql) { |
66 | public final function getScalar(string $sql) { |
67 | $res = $this->query($sql); |
67 | $res = $this->query($sql); |
68 | $row = $res->fetch_array(); |
68 | $row = $res->fetch_array(); |
69 | return $row ? reset($row) : null; |
69 | return $row ? reset($row) : null; |
70 | } |
70 | } |
71 | 71 | ||
72 | public final function query(string $sql, /*?array*/ $prepared_args=null): OIDplusQueryResult { |
72 | public final function query(string $sql, /*?array*/ $prepared_args=null): OIDplusQueryResult { |
73 | 73 | ||
74 | $query_logfile = OIDplus::baseConfig()->getValue('QUERY_LOGFILE', ''); |
74 | $query_logfile = OIDplus::baseConfig()->getValue('QUERY_LOGFILE', ''); |
75 | if (!empty($query_logfile)) { |
75 | if (!empty($query_logfile)) { |
76 | $ts = explode(" ",microtime()); |
76 | $ts = explode(" ",microtime()); |
77 | $ts = date("Y-m-d H:i:s",intval($ts[1])).substr((string)$ts[0],1,4); |
77 | $ts = date("Y-m-d H:i:s",intval($ts[1])).substr((string)$ts[0],1,4); |
78 | static $log_session_id = ""; |
78 | static $log_session_id = ""; |
79 | if (empty($log_session_id)) { |
79 | if (empty($log_session_id)) { |
80 | $log_session_id = rand(10000,99999); |
80 | $log_session_id = rand(10000,99999); |
81 | } |
81 | } |
82 | $file = isset($_SERVER['REQUEST_URI']) ? ' | '.$_SERVER['REQUEST_URI'] : ''; |
82 | $file = isset($_SERVER['REQUEST_URI']) ? ' | '.$_SERVER['REQUEST_URI'] : ''; |
83 | file_put_contents($query_logfile, "$ts <$log_session_id$file> $sql\n", FILE_APPEND); |
83 | file_put_contents($query_logfile, "$ts <$log_session_id$file> $sql\n", FILE_APPEND); |
84 | } |
84 | } |
85 | 85 | ||
86 | $this->last_query = $sql; |
86 | $this->last_query = $sql; |
87 | $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql); |
87 | $sql = str_replace('###', OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''), $sql); |
88 | 88 | ||
89 | if ($this->slangDetectionDone) { |
89 | if ($this->slangDetectionDone) { |
90 | $slang = $this->getSlang(); |
90 | $slang = $this->getSlang(); |
91 | if ($slang) { |
91 | if ($slang) { |
92 | $sql = $slang->filterQuery($sql); |
92 | $sql = $slang->filterQuery($sql); |
93 | } |
93 | } |
94 | } |
94 | } |
95 | 95 | ||
96 | return $this->doQuery($sql, $prepared_args); |
96 | return $this->doQuery($sql, $prepared_args); |
97 | } |
97 | } |
98 | 98 | ||
99 | public final function connect()/*: void*/ { |
99 | public final function connect()/*: void*/ { |
100 | if ($this->connected) return; |
100 | if ($this->connected) return; |
101 | $this->beforeConnect(); |
101 | $this->beforeConnect(); |
102 | $this->doConnect(); |
102 | $this->doConnect(); |
103 | $this->connected = true; |
103 | $this->connected = true; |
104 | OIDplus::register_shutdown_function(array($this, 'disconnect')); |
104 | OIDplus::register_shutdown_function(array($this, 'disconnect')); |
105 | $this->afterConnectMandatory(); |
105 | $this->afterConnectMandatory(); |
106 | $this->afterConnect(); |
106 | $this->afterConnect(); |
107 | } |
107 | } |
108 | 108 | ||
109 | public final function disconnect()/*: void*/ { |
109 | public final function disconnect()/*: void*/ { |
110 | if (!$this->connected) return; |
110 | if (!$this->connected) return; |
111 | $this->beforeDisconnect(); |
111 | $this->beforeDisconnect(); |
112 | $this->doDisconnect(); |
112 | $this->doDisconnect(); |
113 | $this->connected = false; |
113 | $this->connected = false; |
114 | $this->afterDisconnect(); |
114 | $this->afterDisconnect(); |
115 | } |
115 | } |
116 | 116 | ||
117 | public function natOrder($fieldname, $order='asc'): string { |
117 | public function natOrder($fieldname, $order='asc'): string { |
118 | $slang = $this->getSlang(); |
118 | $slang = $this->getSlang(); |
119 | if (!is_null($slang)) { |
119 | if (!is_null($slang)) { |
120 | return $slang->natOrder($fieldname, $order); |
120 | return $slang->natOrder($fieldname, $order); |
121 | } else { |
121 | } else { |
122 | $order = strtolower($order); |
122 | $order = strtolower($order); |
123 | if (($order != 'asc') && ($order != 'desc')) { |
123 | if (($order != 'asc') && ($order != 'desc')) { |
124 | throw new OIDplusException(_L('Invalid order "%1" (needs to be "asc" or "desc")',$order)); |
124 | throw new OIDplusException(_L('Invalid order "%1" (needs to be "asc" or "desc")',$order)); |
125 | } |
125 | } |
126 | 126 | ||
127 | // For (yet) unsupported DBMS, we do not offer natural sort |
127 | // For (yet) unsupported DBMS, we do not offer natural sort |
128 | return "$fieldname $order"; |
128 | return "$fieldname $order"; |
129 | } |
129 | } |
130 | } |
130 | } |
131 | 131 | ||
132 | protected function beforeDisconnect()/*: void*/ {} |
132 | protected function beforeDisconnect()/*: void*/ {} |
133 | 133 | ||
134 | protected function afterDisconnect()/*: void*/ {} |
134 | protected function afterDisconnect()/*: void*/ {} |
135 | 135 | ||
136 | protected function beforeConnect()/*: void*/ {} |
136 | protected function beforeConnect()/*: void*/ {} |
137 | 137 | ||
138 | protected function afterConnect()/*: void*/ {} |
138 | protected function afterConnect()/*: void*/ {} |
139 | 139 | ||
140 | private function afterConnectMandatory()/*: void*/ { |
140 | private function afterConnectMandatory()/*: void*/ { |
141 | // Check if the config table exists. This is important because the database version is stored in it |
141 | // Check if the config table exists. This is important because the database version is stored in it |
142 | $this->initRequireTables(array('config')); |
142 | $this->initRequireTables(array('config')); |
143 | 143 | ||
144 | // Do the database tables need an update? |
144 | // Do the database tables need an update? |
145 | // It is important that we do it immediately after connecting, |
145 | // It is important that we do it immediately after connecting, |
146 | // because the database structure might change and therefore various things might fail. |
146 | // because the database structure might change and therefore various things might fail. |
147 | require_once __DIR__.'/../db_updates/run.inc.php'; |
147 | require_once __DIR__.'/../db_updates/run.inc.php'; |
148 | oidplus_dbupdate($this); |
148 | oidplus_dbupdate($this); |
149 | 149 | ||
150 | // Now that our database is up-to-date, we check if database tables are existing |
150 | // Now that our database is up-to-date, we check if database tables are existing |
151 | // without config table, because it was checked above |
151 | // without config table, because it was checked above |
152 | $this->initRequireTables(array('objects', 'asn1id', 'iri', 'ra'/*, 'config'*/)); |
152 | $this->initRequireTables(array('objects', 'asn1id', 'iri', 'ra'/*, 'config'*/)); |
153 | 153 | ||
154 | // In case an auto-detection of the slang is required (for generic providers like PDO or ODBC), |
154 | // In case an auto-detection of the slang is required (for generic providers like PDO or ODBC), |
155 | // we must not be inside a transaction, because the detection requires intentionally submitting |
155 | // we must not be inside a transaction, because the detection requires intentionally submitting |
156 | // invalid queries to detect the correct DBMS. If we would be inside a transaction, providers like |
156 | // invalid queries to detect the correct DBMS. If we would be inside a transaction, providers like |
157 | // PDO would automatically roll-back. Therefore, we detect the slang right at the beginning, |
157 | // PDO would automatically roll-back. Therefore, we detect the slang right at the beginning, |
158 | // before any transaction is used. |
158 | // before any transaction is used. |
159 | $this->getSlang(); |
159 | $this->getSlang(); |
160 | } |
160 | } |
161 | 161 | ||
162 | private function initRequireTables($tableNames)/*: void*/ { |
162 | private function initRequireTables($tableNames)/*: void*/ { |
163 | $msgs = array(); |
163 | $msgs = array(); |
164 | foreach ($tableNames as $tableName) { |
164 | foreach ($tableNames as $tableName) { |
165 | $prefix = OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''); |
165 | $prefix = OIDplus::baseConfig()->getValue('TABLENAME_PREFIX', ''); |
166 | if (!$this->tableExists($prefix.$tableName)) { |
166 | if (!$this->tableExists($prefix.$tableName)) { |
167 | $msgs[] = _L('Table %1 is missing!',$prefix.$tableName); |
167 | $msgs[] = _L('Table %1 is missing!',$prefix.$tableName); |
168 | } |
168 | } |
169 | } |
169 | } |
170 | if (count($msgs) > 0) { |
170 | if (count($msgs) > 0) { |
171 | throw new OIDplusConfigInitializationException(implode("\n\n",$msgs)); |
171 | throw new OIDplusConfigInitializationException(implode("\n\n",$msgs)); |
172 | } |
172 | } |
173 | } |
173 | } |
174 | 174 | ||
175 | public function tableExists($tableName): bool { |
175 | public function tableExists($tableName): bool { |
176 | try { |
176 | try { |
177 | // Attention: This query could interrupt transactions if Rollback-On-Error is enabled |
177 | // Attention: This query could interrupt transactions if Rollback-On-Error is enabled |
178 | $this->query("select 0 from ".$tableName." where 1=0"); |
178 | $this->query("select 0 from ".$tableName." where 1=0"); |
179 | return true; |
179 | return true; |
180 | } catch (Exception $e) { |
180 | } catch (\Exception $e) { |
181 | return false; |
181 | return false; |
182 | } |
182 | } |
183 | } |
183 | } |
184 | 184 | ||
185 | public function isConnected(): bool { |
185 | public function isConnected(): bool { |
186 | return $this->connected; |
186 | return $this->connected; |
187 | } |
187 | } |
188 | 188 | ||
189 | public function init($html = true)/*: void*/ { |
189 | public function init($html = true)/*: void*/ { |
190 | $this->html = $html; |
190 | $this->html = $html; |
191 | } |
191 | } |
192 | 192 | ||
193 | public function sqlDate(): string { |
193 | public function sqlDate(): string { |
194 | $slang = $this->getSlang(); |
194 | $slang = $this->getSlang(); |
195 | if (!is_null($slang)) { |
195 | if (!is_null($slang)) { |
196 | return $slang->sqlDate(); |
196 | return $slang->sqlDate(); |
197 | } else { |
197 | } else { |
198 | return "'" . date('Y-m-d H:i:s') . "'"; |
198 | return "'" . date('Y-m-d H:i:s') . "'"; |
199 | } |
199 | } |
200 | } |
200 | } |
201 | 201 | ||
202 | protected function doGetSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ { |
202 | protected function doGetSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ { |
203 | $res = null; |
203 | $res = null; |
204 | 204 | ||
205 | if (OIDplus::baseConfig()->exists('FORCE_DBMS_SLANG')) { |
205 | if (OIDplus::baseConfig()->exists('FORCE_DBMS_SLANG')) { |
206 | $name = OIDplus::baseConfig()->getValue('FORCE_DBMS_SLANG', ''); |
206 | $name = OIDplus::baseConfig()->getValue('FORCE_DBMS_SLANG', ''); |
207 | $res = OIDplus::getSqlSlangPlugin($name); |
207 | $res = OIDplus::getSqlSlangPlugin($name); |
208 | if ($mustExist && is_null($res)) { |
208 | if ($mustExist && is_null($res)) { |
209 | throw new OIDplusConfigInitializationException(_L('Enforced SQL slang (via setting FORCE_DBMS_SLANG) "%1" does not exist.',$name)); |
209 | throw new OIDplusConfigInitializationException(_L('Enforced SQL slang (via setting FORCE_DBMS_SLANG) "%1" does not exist.',$name)); |
210 | } |
210 | } |
211 | } else { |
211 | } else { |
212 | foreach (OIDplus::getSqlSlangPlugins() as $plugin) { |
212 | foreach (OIDplus::getSqlSlangPlugins() as $plugin) { |
213 | if ($plugin->detect($this)) { |
213 | if ($plugin->detect($this)) { |
214 | if (OIDplus::baseConfig()->getValue('DEBUG') && !is_null($res)) { |
214 | if (OIDplus::baseConfig()->getValue('DEBUG') && !is_null($res)) { |
215 | throw new OIDplusException(_L('DB-Slang detection failed: Multiple slangs were detected. Use base config setting FORCE_DBMS_SLANG to define one.')); |
215 | throw new OIDplusException(_L('DB-Slang detection failed: Multiple slangs were detected. Use base config setting FORCE_DBMS_SLANG to define one.')); |
216 | } |
216 | } |
217 | 217 | ||
218 | $res = $plugin; |
218 | $res = $plugin; |
219 | 219 | ||
220 | if (!OIDplus::baseConfig()->getValue('DEBUG')) { |
220 | if (!OIDplus::baseConfig()->getValue('DEBUG')) { |
221 | break; |
221 | break; |
222 | } |
222 | } |
223 | } |
223 | } |
224 | } |
224 | } |
225 | if ($mustExist && is_null($res)) { |
225 | if ($mustExist && is_null($res)) { |
226 | throw new OIDplusException(_L('Cannot determine the SQL slang of your DBMS. Your DBMS is probably not supported.')); |
226 | throw new OIDplusException(_L('Cannot determine the SQL slang of your DBMS. Your DBMS is probably not supported.')); |
227 | } |
227 | } |
228 | } |
228 | } |
229 | 229 | ||
230 | return $res; |
230 | return $res; |
231 | } |
231 | } |
232 | 232 | ||
233 | public final function getSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ { |
233 | public final function getSlang(bool $mustExist=true)/*: ?OIDplusSqlSlangPlugin*/ { |
234 | static /*?OIDplusSqlSlangPlugin*/ $slangCache = null; |
234 | static /*?OIDplusSqlSlangPlugin*/ $slangCache = null; |
235 | 235 | ||
236 | if ($this->slangDetectionDone) { |
236 | if ($this->slangDetectionDone) { |
237 | return $slangCache; |
237 | return $slangCache; |
238 | } |
238 | } |
239 | 239 | ||
240 | $slangCache = $this->doGetSlang(); |
240 | $slangCache = $this->doGetSlang(); |
241 | $this->slangDetectionDone = true; |
241 | $this->slangDetectionDone = true; |
242 | return $slangCache; |
242 | return $slangCache; |
243 | } |
243 | } |
244 | } |
244 | } |
245 | 245 |