1 <?php
2 
3 /**
4  * @noinspection SqlNoDataSourceInspection
5  * @noinspection SqlDialectInspection
6  * @noinspection PhpComposerExtensionStubsInspection
7  */
8 
9 use dokuwiki\Extension\Plugin;
10 use dokuwiki\plugin\sqlite\SQLiteDB;
11 use dokuwiki\plugin\sqlite\Tools;
12 
13 // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols, PSR1.Classes.ClassDeclaration.MultipleClasses
14 // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
15 
16 /**
17  * For compatibility with previous adapter implementation.
18  */
19 if (!defined('DOKU_EXT_PDO')) define('DOKU_EXT_PDO', 'pdo');
20 class helper_plugin_sqlite_adapter_dummy
21 {
22     public function getName()
23     {
24         return DOKU_EXT_PDO;
25     }
26 
27     public function setUseNativeAlter($set)
28     {
29     }
30 }
31 
32 /**
33  * DokuWiki Plugin sqlite (Helper Component)
34  *
35  * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
36  * @author  Andreas Gohr <gohr@cosmocode.de>
37  * @deprecated 2023-03-15
38  */
39 class helper_plugin_sqlite extends Plugin
40 {
41     /** @var SQLiteDB|null */
42     protected $adapter;
43 
44     /** @var array result cache */
45     protected $data;
46 
47     /**
48      * constructor
49      */
50     public function __construct()
51     {
52         if (!$this->existsPDOSqlite()) {
53             msg('PDO SQLite support missing in this PHP install - The sqlite plugin will not work', -1);
54         }
55         $this->adapter = new helper_plugin_sqlite_adapter_dummy();
56     }
57 
58     /**
59      * Get the current Adapter
60      * @return SQLiteDB|null
61      */
62     public function getAdapter()
63     {
64         return $this->adapter;
65     }
66 
67     /**
68      * Keep separate instances for every call to keep database connections
69      */
70     public function isSingleton()
71     {
72         return false;
73     }
74 
75     /**
76      * check availabilty of PHP PDO sqlite3
77      */
78     public function existsPDOSqlite()
79     {
80         if (class_exists('pdo')) {
81             return in_array('sqlite', \PDO::getAvailableDrivers());
82         }
83         return false;
84     }
85 
86     /**
87      * Initializes and opens the database
88      *
89      * Needs to be called right after loading this helper plugin
90      *
91      * @param string $dbname
92      * @param string $updatedir - Database update infos
93      * @return bool
94      */
95     public function init($dbname, $updatedir)
96     {
97         if (!defined('DOKU_UNITTEST')) { // for now we don't want to trigger the deprecation warning in the tests
98             dbg_deprecated(SQLiteDB::class);
99         }
100 
101         try {
102             $this->adapter = new SQLiteDB($dbname, $updatedir, $this);
103         } catch (Exception $e) {
104             msg('SQLite: ' . $e->getMessage(), -1);
105             return false;
106         }
107         return true;
108     }
109 
110     /**
111      * This is called from the adapter itself for backwards compatibility
112      *
113      * @param SQLiteDB $adapter
114      * @return void
115      */
116     public function setAdapter($adapter)
117     {
118         $this->adapter = $adapter;
119     }
120 
121     /**
122      * Registers a User Defined Function for use in SQL statements
123      */
124     public function create_function($function_name, $callback, $num_args)
125     {
126         $this->adapter->getPdo()->sqliteCreateFunction($function_name, $callback, $num_args);
127     }
128 
129     // region query and result handling functions
130 
131     /**
132      * Convenience function to run an INSERT OR REPLACE operation
133      *
134      * The function takes a key-value array with the column names in the key and the actual value in the value,
135      * build the appropriate query and executes it.
136      *
137      * @param string $table the table the entry should be saved to (will not be escaped)
138      * @param array $entry A simple key-value pair array (only values will be escaped)
139      * @return bool
140      */
141     public function storeEntry($table, $entry)
142     {
143         try {
144             $this->adapter->saveRecord($table, $entry);
145         } catch (\Exception $e) {
146             msg('SQLite: ' . $e->getMessage(), -1);
147             return false;
148         }
149 
150         return true;
151     }
152 
153     /**
154      * Execute a query with the given parameters.
155      *
156      * Takes care of escaping
157      *
158      *
159      * @param string ...$args - the arguments of query(), the first is the sql and others are values
160      */
161     public function query(...$args)
162     {
163         // clear the cache
164         $this->data = null;
165 
166         try {
167             $sql = $this->prepareSql($args);
168             return $this->adapter->query($sql);
169         } catch (\Exception $e) {
170             msg('SQLite: ' . $e->getMessage(), -1);
171             return false;
172         }
173     }
174 
175     /**
176      * Prepare a query with the given arguments.
177      *
178      * Takes care of escaping
179      *
180      * @param array $args
181      *    array of arguments:
182      *      - string $sql - the statement
183      *      - arguments...
184      * @return bool|string
185      * @throws Exception
186      */
187     public function prepareSql($args)
188     {
189 
190         $sql = trim(array_shift($args));
191         $sql = rtrim($sql, ';');
192 
193         if (!$sql) {
194             throw new \Exception('No SQL statement given', -1);
195         }
196 
197         $argc = count($args);
198         if ($argc > 0 && is_array($args[0])) {
199             $args = $args[0];
200             $argc = count($args);
201         }
202 
203         // check number of arguments
204         $qmc = substr_count($sql, '?');
205         if ($argc < $qmc) {
206             throw new \Exception('Not enough arguments passed for statement. ' .
207                 'Expected ' . $qmc . ' got ' . $argc . ' - ' . hsc($sql));
208         } elseif ($argc > $qmc) {
209             throw new \Exception('Too much arguments passed for statement. ' .
210                 'Expected ' . $qmc . ' got ' . $argc . ' - ' . hsc($sql));
211         }
212 
213         // explode at wildcard, then join again
214         $parts = explode('?', $sql, $argc + 1);
215         $args  = array_map([$this->adapter->getPdo(), 'quote'], $args);
216         $sql   = '';
217 
218         while (($part = array_shift($parts)) !== null) {
219             $sql .= $part;
220             $sql .= array_shift($args);
221         }
222 
223         return $sql;
224     }
225 
226 
227     /**
228      * Closes the result set (and it's cursors)
229      *
230      * If you're doing SELECT queries inside a TRANSACTION, be sure to call this
231      * function on all your results sets, before COMMITing the transaction.
232      *
233      * Also required when not all rows of a result are fetched
234      *
235      * @param \PDOStatement $res
236      * @return bool
237      */
238     public function res_close($res)
239     {
240         if (!$res) return false;
241 
242         return $res->closeCursor();
243     }
244 
245     /**
246      * Returns a complete result set as array
247      *
248      * @param \PDOStatement $res
249      * @return array
250      */
251     public function res2arr($res, $assoc = true)
252     {
253         if (!$res) return [];
254 
255         // this is a bullshit workaround for having res2arr and res2count work on one result
256         if (!$this->data) {
257             $mode = $assoc ? PDO::FETCH_ASSOC : PDO::FETCH_NUM;
258             $this->data = $res->fetchAll($mode);
259         }
260         return $this->data;
261     }
262 
263     /**
264      * Return the next row from the result set as associative array
265      *
266      * @param \PDOStatement $res
267      * @param int $rownum will be ignored
268      */
269     public function res2row($res, $rownum = 0)
270     {
271         if (!$res) return false;
272 
273         return $res->fetch(\PDO::FETCH_ASSOC);
274     }
275 
276     /**
277      * Return the first value from the next row.
278      *
279      * @param \PDOStatement $res
280      * @return mixed
281      */
282     public function res2single($res)
283     {
284         if (!$res) return false;
285 
286         $data = $res->fetch(PDO::FETCH_NUM, PDO::FETCH_ORI_ABS, 0);
287         if (empty($data)) {
288             return false;
289         }
290         return $data[0];
291     }
292 
293     /**
294      * fetch the next row as zero indexed array
295      *
296      * @param \PDOStatement $res
297      * @return array|bool
298      */
299     public function res_fetch_array($res)
300     {
301         if (!$res) return false;
302 
303         return $res->fetch(PDO::FETCH_NUM);
304     }
305 
306     /**
307      * fetch the next row as assocative array
308      *
309      * @param \PDOStatement $res
310      * @return array|bool
311      */
312     public function res_fetch_assoc($res)
313     {
314         if (!$res) return false;
315 
316         return $res->fetch(PDO::FETCH_ASSOC);
317     }
318 
319     /**
320      * Count the number of records in result
321      *
322      * This function is really inperformant in PDO and should be avoided!
323      *
324      * @param \PDOStatement $res
325      * @return int
326      */
327     public function res2count($res)
328     {
329         if (!$res) return 0;
330 
331         // this is a bullshit workaround for having res2arr and res2count work on one result
332         if (!$this->data) {
333             $this->data = $this->res2arr($res);
334         }
335 
336         return count($this->data);
337     }
338 
339     /**
340      * Count the number of records changed last time
341      *
342      * @param \PDOStatement $res
343      * @return int
344      */
345     public function countChanges($res)
346     {
347         if (!$res) return 0;
348 
349         return $res->rowCount();
350     }
351 
352     // endregion
353 
354     // region quoting/escaping functions
355 
356     /**
357      * Join the given values and quote them for SQL insertion
358      */
359     public function quote_and_join($vals, $sep = ',')
360     {
361         $vals = array_map([$this->adapter->getPdo(), 'quote'], $vals);
362         return implode($sep, $vals);
363     }
364 
365     /**
366      * Quotes a string, by escaping it and adding quotes
367      */
368     public function quote_string($string)
369     {
370         return $this->adapter->getPdo()->quote($string);
371     }
372 
373     /**
374      * Similar to quote_string, but without the quotes, useful to construct LIKE patterns
375      */
376     public function escape_string($str)
377     {
378         return trim($this->adapter->getPdo()->quote($str), "'");
379     }
380 
381     // endregion
382 
383     // region speciality functions
384 
385     /**
386      * Split sql queries on semicolons, unless when semicolons are quoted
387      *
388      * Usually you don't need this. It's only really needed if you need individual results for
389      * multiple queries. For example in the admin interface.
390      *
391      * @param string $sql
392      * @return array sql queries
393      * @deprecated
394      */
395     public function SQLstring2array($sql)
396     {
397         if (!DOKU_UNITTEST) { // for now we don't want to trigger the deprecation warning in the tests
398             dbg_deprecated(Tools::class . '::SQLstring2array');
399         }
400         return Tools::SQLstring2array($sql);
401     }
402 
403     /**
404      * @deprecated needs to be fixed in stuct and structpublish
405      */
406     public function doTransaction($sql, $sqlpreparing = true)
407     {
408         throw new \Exception(
409             'This method seems to never have done what it suggests. Please use the query() function instead.'
410         );
411     }
412 
413     // endregion
414 }
415