1<?php
2
3/**
4 * @noinspection SqlNoDataSourceInspection
5 * @noinspection SqlDialectInspection
6 * @noinspection PhpComposerExtensionStubsInspection
7 */
8
9use dokuwiki\Extension\Plugin;
10use dokuwiki\plugin\sqlite\SQLiteDB;
11use 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 */
19if (!defined('DOKU_EXT_PDO')) define('DOKU_EXT_PDO', 'pdo');
20class 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 */
39class 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