Rev 231 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
150 | daniel-mar | 1 | <?php |
2 | |||
3 | /* |
||
4 | * OIDplus 2.0 |
||
5 | * Copyright 2019 Daniel Marschall, ViaThinkSoft |
||
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('IN_OIDPLUS')) die(); |
||
21 | |||
22 | define('OIDPLUS_MYSQL_QUERYLOG', false); |
||
23 | |||
227 | daniel-mar | 24 | class OIDplusDataBasePluginMySQLi extends OIDplusDataBasePlugin { |
150 | daniel-mar | 25 | private $mysqli; |
26 | private $last_query; |
||
27 | private $prepare_cache = array(); |
||
28 | |||
236 | daniel-mar | 29 | public static function getPluginInformation(): array { |
222 | daniel-mar | 30 | $out = array(); |
31 | $out['name'] = 'MySQLi'; |
||
32 | $out['author'] = 'ViaThinkSoft'; |
||
33 | $out['version'] = null; |
||
34 | $out['descriptionHTML'] = null; |
||
35 | return $out; |
||
36 | } |
||
37 | |||
236 | daniel-mar | 38 | public static function name(): string { |
150 | daniel-mar | 39 | return "MySQL"; |
40 | } |
||
41 | |||
236 | daniel-mar | 42 | public function query($sql, $prepared_args=null): OIDplusQueryResult { |
150 | daniel-mar | 43 | $this->last_query = $sql; |
236 | daniel-mar | 44 | if (OIDPLUS_MYSQL_QUERYLOG) file_put_contents(__DIR__."/query.log", "$sql <== ".get_calling_function()."\n", FILE_APPEND); |
150 | daniel-mar | 45 | if (is_null($prepared_args)) { |
236 | daniel-mar | 46 | $res = $this->mysqli->query($sql, MYSQLI_STORE_RESULT); |
47 | |||
48 | if ($res === false) { |
||
49 | throw new OIDplusSQLException($sql, $this->error()); |
||
50 | } else { |
||
51 | return new OIDplusQueryResultMySQL($res); |
||
52 | } |
||
150 | daniel-mar | 53 | } else { |
54 | if (!is_array($prepared_args)) { |
||
55 | throw new Exception("'prepared_args' must be either NULL or an ARRAY."); |
||
56 | } |
||
57 | if (isset($this->prepare_cache[$sql])) { |
||
58 | $ps = $this->prepare_cache[$sql]; |
||
59 | } else { |
||
60 | $ps = $this->mysqli->prepare($sql); |
||
61 | if (!$ps) { |
||
236 | daniel-mar | 62 | throw new OIDplusSQLException($sql, 'Cannot prepare statement'); |
150 | daniel-mar | 63 | } |
64 | $this->prepare_cache[$sql] = $ps; |
||
65 | } |
||
66 | |||
236 | daniel-mar | 67 | self::bind_placeholder_vars($ps,$prepared_args); |
68 | if (!$ps->execute()) { |
||
69 | throw new OIDplusSQLException($sql, $this->error()); |
||
70 | } |
||
208 | daniel-mar | 71 | |
236 | daniel-mar | 72 | if (self::nativeDriverAvailable()) { |
73 | return new OIDplusQueryResultMySQL($ps->get_result()); |
||
74 | } else { |
||
75 | return new OIDplusQueryResultMySQLNoNativeDriver($ps); |
||
76 | } |
||
150 | daniel-mar | 77 | } |
78 | } |
||
236 | daniel-mar | 79 | |
80 | public function insert_id(): int { |
||
150 | daniel-mar | 81 | return $this->mysqli->insert_id; |
82 | } |
||
236 | daniel-mar | 83 | |
84 | public function error(): string { |
||
150 | daniel-mar | 85 | return !empty($this->mysqli->connect_error) ? $this->mysqli->connect_error : $this->mysqli->error; |
86 | } |
||
231 | daniel-mar | 87 | |
230 | daniel-mar | 88 | private $html = null; |
236 | daniel-mar | 89 | public function init($html = true): void { |
230 | daniel-mar | 90 | $this->html = $html; |
91 | } |
||
150 | daniel-mar | 92 | |
236 | daniel-mar | 93 | public function connect(): void { |
150 | daniel-mar | 94 | if (OIDPLUS_MYSQL_QUERYLOG) file_put_contents("query.log", ''); |
95 | |||
96 | // Try connecting to the database |
||
97 | list($hostname,$port) = explode(':', OIDPLUS_MYSQL_HOST.':'.ini_get("mysqli.default_port")); |
||
98 | $this->mysqli = @new mysqli($hostname, OIDPLUS_MYSQL_USERNAME, base64_decode(OIDPLUS_MYSQL_PASSWORD), OIDPLUS_MYSQL_DATABASE, $port); |
||
99 | if (!empty($this->mysqli->connect_error) || ($this->mysqli->connect_errno != 0)) { |
||
230 | daniel-mar | 100 | if ($this->html) { |
173 | daniel-mar | 101 | echo "<h1>Error</h1><p>Database connection failed! (".$this->error().")</p>"; |
102 | if (is_dir(__DIR__.'/../../../setup')) { |
||
150 | daniel-mar | 103 | echo '<p>If you believe that the login credentials are wrong, please run <a href="setup/">setup</a> again.</p>'; |
104 | } |
||
105 | } else { |
||
173 | daniel-mar | 106 | echo "Error: Database connection failed! (".$this->error().")"; |
107 | if (is_dir(__DIR__.'/../../../setup')) { |
||
150 | daniel-mar | 108 | echo ' If you believe that the login credentials are wrong, please run setup again.'; |
109 | } |
||
110 | } |
||
111 | die(); |
||
112 | } |
||
113 | |||
114 | $this->query("SET NAMES 'utf8'"); |
||
230 | daniel-mar | 115 | $this->afterConnect($this->html); |
150 | daniel-mar | 116 | $this->connected = true; |
117 | } |
||
118 | |||
119 | private $intransaction = false; |
||
120 | |||
236 | daniel-mar | 121 | public function transaction_begin(): void { |
150 | daniel-mar | 122 | if ($this->intransaction) throw new Exception("Nested transactions are not supported by this database plugin."); |
123 | $this->mysqli->autocommit(true); |
||
124 | $this->intransaction = true; |
||
125 | } |
||
126 | |||
236 | daniel-mar | 127 | public function transaction_commit(): void { |
150 | daniel-mar | 128 | $this->mysqli->commit(); |
129 | $this->mysqli->autocommit(false); |
||
130 | $this->intransaction = false; |
||
131 | } |
||
132 | |||
236 | daniel-mar | 133 | public function transaction_rollback(): void { |
150 | daniel-mar | 134 | $this->mysqli->rollback(); |
135 | $this->mysqli->autocommit(false); |
||
136 | $this->intransaction = false; |
||
137 | } |
||
138 | |||
236 | daniel-mar | 139 | public static function nativeDriverAvailable(): bool { |
140 | return function_exists('mysqli_fetch_all'); |
||
141 | } |
||
150 | daniel-mar | 142 | |
236 | daniel-mar | 143 | private static function bind_placeholder_vars(&$stmt,$params): bool { |
144 | // Credit to: Dave Morgan |
||
145 | // Code taken from: http://www.devmorgan.com/blog/2009/03/27/dydl-part-3-dynamic-binding-with-mysqli-php/ |
||
146 | // https://stackoverflow.com/questions/17219214/how-to-bind-in-mysqli-dynamically |
||
147 | if ($params != null) { |
||
148 | $types = ''; //initial sting with types |
||
149 | foreach ($params as $param) { //for each element, determine type and add |
||
150 | if (is_int($param)) { |
||
151 | $types .= 'i'; //integer |
||
152 | } elseif (is_float($param)) { |
||
153 | $types .= 'd'; //double |
||
154 | } elseif (is_string($param)) { |
||
155 | $types .= 's'; //string |
||
156 | } else { |
||
157 | $types .= 'b'; //blob and unknown |
||
158 | } |
||
208 | daniel-mar | 159 | } |
150 | daniel-mar | 160 | |
236 | daniel-mar | 161 | $bind_names = array(); |
162 | $bind_names[] = $types; //first param needed is the type string, e.g.: 'issss' |
||
150 | daniel-mar | 163 | |
236 | daniel-mar | 164 | for ($i=0; $i<count($params);$i++) { //go through incoming params and added em to array |
165 | $bind_name = 'bind' . $i; //give them an arbitrary name |
||
166 | $$bind_name = $params[$i]; //add the parameter to the variable variable |
||
167 | $bind_names[] = &$$bind_name; //now associate the variable as an element in an array |
||
168 | } |
||
150 | daniel-mar | 169 | |
236 | daniel-mar | 170 | //error_log("better_mysqli has params ".print_r($bind_names, 1)); |
171 | //call the function bind_param with dynamic params |
||
172 | call_user_func_array(array($stmt,'bind_param'),$bind_names); |
||
173 | return true; |
||
174 | } else { |
||
175 | return false; |
||
208 | daniel-mar | 176 | } |
177 | } |
||
150 | daniel-mar | 178 | } |
179 | |||
236 | daniel-mar | 180 | class OIDplusQueryResultMySQL extends OIDplusQueryResult { |
181 | protected $no_resultset; |
||
182 | protected $res; |
||
183 | |||
184 | public function __construct($res) { |
||
185 | $this->no_resultset = $res === false; |
||
186 | $this->res = $res; |
||
208 | daniel-mar | 187 | } |
150 | daniel-mar | 188 | |
236 | daniel-mar | 189 | public function containsResultSet(): bool { |
190 | return !$this->no_resultset; |
||
191 | } |
||
224 | daniel-mar | 192 | |
236 | daniel-mar | 193 | public function num_rows(): int { |
194 | if ($this->no_resultset) throw new Exception("The query has returned no result set (i.e. it was not a SELECT query)"); |
||
195 | return $this->res->num_rows; |
||
196 | } |
||
150 | daniel-mar | 197 | |
236 | daniel-mar | 198 | public function fetch_array()/*: ?array*/ { |
199 | if ($this->no_resultset) throw new Exception("The query has returned no result set (i.e. it was not a SELECT query)"); |
||
200 | return $this->res->fetch_array(MYSQLI_BOTH); |
||
201 | } |
||
224 | daniel-mar | 202 | |
236 | daniel-mar | 203 | public function fetch_object()/*: ?object*/ { |
204 | if ($this->no_resultset) throw new Exception("The query has returned no result set (i.e. it was not a SELECT query)"); |
||
205 | return $this->res->fetch_object("stdClass"); |
||
206 | } |
||
207 | } |
||
224 | daniel-mar | 208 | |
236 | daniel-mar | 209 | class OIDplusQueryResultMySQLNoNativeDriver extends OIDplusQueryResult { |
210 | // Based on https://www.php.net/manual/de/mysqli-stmt.get-result.php#113398 |
||
208 | daniel-mar | 211 | |
236 | daniel-mar | 212 | protected $stmt; |
213 | protected $nCols; |
||
214 | protected $no_resultset; |
||
208 | daniel-mar | 215 | |
236 | daniel-mar | 216 | public function __construct($stmt) { |
217 | $metadata = mysqli_stmt_result_metadata($stmt); |
||
208 | daniel-mar | 218 | |
236 | daniel-mar | 219 | $this->no_resultset = $metadata === false; |
208 | daniel-mar | 220 | |
236 | daniel-mar | 221 | if (!$this->no_resultset) { |
222 | $this->nCols = mysqli_num_fields($metadata); |
||
223 | $this->stmt = $stmt; |
||
224 | daniel-mar | 224 | |
236 | daniel-mar | 225 | mysqli_free_result($metadata); |
208 | daniel-mar | 226 | } |
236 | daniel-mar | 227 | } |
224 | daniel-mar | 228 | |
236 | daniel-mar | 229 | public function containsResultSet(): bool { |
230 | return !$this->no_resultset; |
||
224 | daniel-mar | 231 | } |
232 | |||
236 | daniel-mar | 233 | public function num_rows(): int { |
234 | if ($this->no_resultset) throw new Exception("The query has returned no result set (i.e. it was not a SELECT query)"); |
||
224 | daniel-mar | 235 | |
236 | daniel-mar | 236 | $this->stmt->store_result(); |
237 | return $this->stmt->num_rows; |
||
238 | } |
||
224 | daniel-mar | 239 | |
236 | daniel-mar | 240 | function fetch_array()/*: ?array*/ { |
241 | if ($this->no_resultset) throw new Exception("The query has returned no result set (i.e. it was not a SELECT query)"); |
||
242 | |||
243 | // https://stackoverflow.com/questions/10752815/mysqli-get-result-alternative , modified |
||
244 | $stmt = $this->stmt; |
||
245 | $stmt->store_result(); |
||
246 | $resultkeys = array(); |
||
247 | $thisName = ""; |
||
248 | |||
249 | if ($stmt->num_rows==0) return null; |
||
250 | |||
251 | for ($i = 0; $i < $stmt->num_rows; $i++) { |
||
252 | $metadata = $stmt->result_metadata(); |
||
253 | while ($field = $metadata->fetch_field()) { |
||
254 | $thisName = $field->name; |
||
255 | $resultkeys[] = $thisName; |
||
256 | } |
||
208 | daniel-mar | 257 | } |
224 | daniel-mar | 258 | |
236 | daniel-mar | 259 | $ret = array(); |
260 | $args = array(); |
||
261 | for ($i=0; $i<$this->nCols; $i++) { |
||
262 | $ret[$i] = NULL; |
||
263 | $theValue = $resultkeys[$i]; |
||
264 | $ret[$theValue] = NULL; // will be overwritten by mysqli_stmt_bind_result |
||
265 | $args[] = &$ret[$theValue]; |
||
266 | } |
||
267 | if (!mysqli_stmt_bind_result($this->stmt, ...$args)) { |
||
268 | return null; |
||
269 | } |
||
224 | daniel-mar | 270 | |
236 | daniel-mar | 271 | // This should advance the "$stmt" cursor. |
272 | if (!mysqli_stmt_fetch($this->stmt)) { |
||
273 | return null; |
||
274 | } |
||
275 | |||
276 | // Return the array we built. |
||
208 | daniel-mar | 277 | return $ret; |
278 | } |
||
236 | daniel-mar | 279 | |
280 | public function fetch_object()/*: ?object*/ { |
||
281 | if ($this->no_resultset) throw new Exception("The query has returned no result set (i.e. it was not a SELECT query)"); |
||
282 | |||
283 | $ary = $this->fetch_array(); |
||
284 | if (!$ary) return null; |
||
285 | |||
286 | $obj = new stdClass; |
||
287 | foreach ($ary as $name => $val) { |
||
288 | $obj->$name = $val; |
||
289 | } |
||
290 | return $obj; |
||
291 | } |
||
208 | daniel-mar | 292 | } |