xref: /plugin/sqlite/helper.php (revision d294e082ac489c2308f20868710ca78af9844e61)
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()
162    {
163        // get function arguments
164        $args = func_get_args();
165
166        // clear the cache
167        $this->data = null;
168
169        try {
170            $sql = $this->prepareSql($args);
171            return $this->adapter->query($sql);
172        } catch (\Exception $e) {
173            msg('SQLite: ' . $e->getMessage(), -1);
174            return false;
175        }
176    }
177
178    /**
179     * Prepare a query with the given arguments.
180     *
181     * Takes care of escaping
182     *
183     * @param array $args
184     *    array of arguments:
185     *      - string $sql - the statement
186     *      - arguments...
187     * @return bool|string
188     * @throws Exception
189     */
190    public function prepareSql($args)
191    {
192
193        $sql = trim(array_shift($args));
194        $sql = rtrim($sql, ';');
195
196        if (!$sql) {
197            throw new \Exception('No SQL statement given', -1);
198        }
199
200        $argc = count($args);
201        if ($argc > 0 && is_array($args[0])) {
202            $args = $args[0];
203            $argc = count($args);
204        }
205
206        // check number of arguments
207        $qmc = substr_count($sql, '?');
208        if ($argc < $qmc) {
209            throw new \Exception('Not enough arguments passed for statement. ' .
210                'Expected ' . $qmc . ' got ' . $argc . ' - ' . hsc($sql));
211        } elseif ($argc > $qmc) {
212            throw new \Exception('Too much arguments passed for statement. ' .
213                'Expected ' . $qmc . ' got ' . $argc . ' - ' . hsc($sql));
214        }
215
216        // explode at wildcard, then join again
217        $parts = explode('?', $sql, $argc + 1);
218        $args  = array_map([$this->adapter->getPdo(), 'quote'], $args);
219        $sql   = '';
220
221        while (($part = array_shift($parts)) !== null) {
222            $sql .= $part;
223            $sql .= array_shift($args);
224        }
225
226        return $sql;
227    }
228
229
230    /**
231     * Closes the result set (and it's cursors)
232     *
233     * If you're doing SELECT queries inside a TRANSACTION, be sure to call this
234     * function on all your results sets, before COMMITing the transaction.
235     *
236     * Also required when not all rows of a result are fetched
237     *
238     * @param \PDOStatement $res
239     * @return bool
240     */
241    public function res_close($res)
242    {
243        if (!$res) return false;
244
245        return $res->closeCursor();
246    }
247
248    /**
249     * Returns a complete result set as array
250     *
251     * @param \PDOStatement $res
252     * @return array
253     */
254    public function res2arr($res, $assoc = true)
255    {
256        if (!$res) return [];
257
258        // this is a bullshit workaround for having res2arr and res2count work on one result
259        if (!$this->data) {
260            $mode = $assoc ? PDO::FETCH_ASSOC : PDO::FETCH_NUM;
261            $this->data = $res->fetchAll($mode);
262        }
263        return $this->data;
264    }
265
266    /**
267     * Return the next row from the result set as associative array
268     *
269     * @param \PDOStatement $res
270     * @param int $rownum will be ignored
271     */
272    public function res2row($res, $rownum = 0)
273    {
274        if (!$res) return false;
275
276        return $res->fetch(\PDO::FETCH_ASSOC);
277    }
278
279    /**
280     * Return the first value from the next row.
281     *
282     * @param \PDOStatement $res
283     * @return mixed
284     */
285    public function res2single($res)
286    {
287        if (!$res) return false;
288
289        $data = $res->fetch(PDO::FETCH_NUM, PDO::FETCH_ORI_ABS, 0);
290        if (empty($data)) {
291            return false;
292        }
293        return $data[0];
294    }
295
296    /**
297     * fetch the next row as zero indexed array
298     *
299     * @param \PDOStatement $res
300     * @return array|bool
301     */
302    public function res_fetch_array($res)
303    {
304        if (!$res) return false;
305
306        return $res->fetch(PDO::FETCH_NUM);
307    }
308
309    /**
310     * fetch the next row as assocative array
311     *
312     * @param \PDOStatement $res
313     * @return array|bool
314     */
315    public function res_fetch_assoc($res)
316    {
317        if (!$res) return false;
318
319        return $res->fetch(PDO::FETCH_ASSOC);
320    }
321
322    /**
323     * Count the number of records in result
324     *
325     * This function is really inperformant in PDO and should be avoided!
326     *
327     * @param \PDOStatement $res
328     * @return int
329     */
330    public function res2count($res)
331    {
332        if (!$res) return 0;
333
334        // this is a bullshit workaround for having res2arr and res2count work on one result
335        if (!$this->data) {
336            $this->data = $this->res2arr($res);
337        }
338
339        return count($this->data);
340    }
341
342    /**
343     * Count the number of records changed last time
344     *
345     * @param \PDOStatement $res
346     * @return int
347     */
348    public function countChanges($res)
349    {
350        if (!$res) return 0;
351
352        return $res->rowCount();
353    }
354
355    // endregion
356
357    // region quoting/escaping functions
358
359    /**
360     * Join the given values and quote them for SQL insertion
361     */
362    public function quote_and_join($vals, $sep = ',')
363    {
364        $vals = array_map([$this->adapter->getPdo(), 'quote'], $vals);
365        return implode($sep, $vals);
366    }
367
368    /**
369     * Quotes a string, by escaping it and adding quotes
370     */
371    public function quote_string($string)
372    {
373        return $this->adapter->getPdo()->quote($string);
374    }
375
376    /**
377     * Similar to quote_string, but without the quotes, useful to construct LIKE patterns
378     */
379    public function escape_string($str)
380    {
381        return trim($this->adapter->getPdo()->quote($str), "'");
382    }
383
384    // endregion
385
386    // region speciality functions
387
388    /**
389     * Split sql queries on semicolons, unless when semicolons are quoted
390     *
391     * Usually you don't need this. It's only really needed if you need individual results for
392     * multiple queries. For example in the admin interface.
393     *
394     * @param string $sql
395     * @return array sql queries
396     * @deprecated
397     */
398    public function SQLstring2array($sql)
399    {
400        if (!DOKU_UNITTEST) { // for now we don't want to trigger the deprecation warning in the tests
401            dbg_deprecated(Tools::class . '::SQLstring2array');
402        }
403        return Tools::SQLstring2array($sql);
404    }
405
406    /**
407     * @deprecated needs to be fixed in stuct and structpublish
408     */
409    public function doTransaction($sql, $sqlpreparing = true)
410    {
411        throw new \Exception(
412            'This method seems to never have done what it suggests. Please use the query() function instead.'
413        );
414    }
415
416    // endregion
417}
418