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