xref: /plugin/sqlite/helper.php (revision 1973f1229a561d91eb28c83f56c8a5b30e935305)
1a1e6784eSAndreas Gohr<?php
28da7d805SAndreas Gohr
38da7d805SAndreas Gohr/**
48da7d805SAndreas Gohr * @noinspection SqlNoDataSourceInspection
58da7d805SAndreas Gohr * @noinspection SqlDialectInspection
68da7d805SAndreas Gohr * @noinspection PhpComposerExtensionStubsInspection
78da7d805SAndreas Gohr */
88da7d805SAndreas Gohr
98da7d805SAndreas Gohruse dokuwiki\plugin\sqlite\SQLiteDB;
108da7d805SAndreas Gohruse dokuwiki\plugin\sqlite\Tools;
118da7d805SAndreas Gohr
128da7d805SAndreas Gohr
13801b921eSSzymon Olewniczak
14801b921eSSzymon Olewniczak/**
15801b921eSSzymon Olewniczak * For compatibility with previous adapter implementation.
16801b921eSSzymon Olewniczak */
17801b921eSSzymon Olewniczakif(!defined('DOKU_EXT_PDO')) define('DOKU_EXT_PDO', 'pdo');
18801b921eSSzymon Olewniczakclass helper_plugin_sqlite_adapter_dummy
19801b921eSSzymon Olewniczak{
20801b921eSSzymon Olewniczak    public function getName() {
21801b921eSSzymon Olewniczak        return DOKU_EXT_PDO;
22801b921eSSzymon Olewniczak    }
23801b921eSSzymon Olewniczak
24801b921eSSzymon Olewniczak    public function setUseNativeAlter($set) {}
25801b921eSSzymon Olewniczak}
26801b921eSSzymon Olewniczak
27a1e6784eSAndreas Gohr/**
28a1e6784eSAndreas Gohr * DokuWiki Plugin sqlite (Helper Component)
29a1e6784eSAndreas Gohr *
30a1e6784eSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
31a1e6784eSAndreas Gohr * @author  Andreas Gohr <gohr@cosmocode.de>
328da7d805SAndreas Gohr * @deprecated 2023-03-15
33a1e6784eSAndreas Gohr */
348da7d805SAndreas Gohrclass helper_plugin_sqlite extends DokuWiki_Plugin
358da7d805SAndreas Gohr{
368da7d805SAndreas Gohr    /** @var SQLiteDB|null */
376c7ee3f2SAndreas Gohr    protected $adapter = null;
38aa81d781SKlap-in
398da7d805SAndreas Gohr    /** @var array result cache */
408da7d805SAndreas Gohr    protected $data;
418da7d805SAndreas Gohr
423e9ac593SGerrit Uitslag    /**
438da7d805SAndreas Gohr     * constructor
443e9ac593SGerrit Uitslag     */
458da7d805SAndreas Gohr    public function __construct()
468da7d805SAndreas Gohr    {
478da7d805SAndreas Gohr        if (!$this->existsPDOSqlite()) {
488da7d805SAndreas Gohr            msg('PDO SQLite support missing in this PHP install - The sqlite plugin will not work', -1);
498da7d805SAndreas Gohr        }
50801b921eSSzymon Olewniczak        $this->adapter = new helper_plugin_sqlite_adapter_dummy();
518da7d805SAndreas Gohr    }
528da7d805SAndreas Gohr
538da7d805SAndreas Gohr    /**
548da7d805SAndreas Gohr     * Get the current Adapter
558da7d805SAndreas Gohr     * @return SQLiteDB|null
568da7d805SAndreas Gohr     */
578da7d805SAndreas Gohr    public function getAdapter()
588da7d805SAndreas Gohr    {
59ca2f2adaSKlap-in        return $this->adapter;
60ca2f2adaSKlap-in    }
61ca2f2adaSKlap-in
62a1e6784eSAndreas Gohr    /**
63aa81d781SKlap-in     * Keep separate instances for every call to keep database connections
64aa81d781SKlap-in     */
658da7d805SAndreas Gohr    public function isSingleton()
668da7d805SAndreas Gohr    {
67aa81d781SKlap-in        return false;
68aa81d781SKlap-in    }
69aa81d781SKlap-in
70aa81d781SKlap-in    /**
71aa81d781SKlap-in     * check availabilty of PHP PDO sqlite3
72aa81d781SKlap-in     */
738da7d805SAndreas Gohr    public function existsPDOSqlite()
748da7d805SAndreas Gohr    {
7587fa2c18Sstretchyboy        if (class_exists('pdo')) {
768da7d805SAndreas Gohr            return in_array('sqlite', \PDO::getAvailableDrivers());
777ed6069fSAdrian Lang        }
78aa81d781SKlap-in        return false;
79a1e6784eSAndreas Gohr    }
80a1e6784eSAndreas Gohr
81a1e6784eSAndreas Gohr    /**
82a1e6784eSAndreas Gohr     * Initializes and opens the database
83a1e6784eSAndreas Gohr     *
84a1e6784eSAndreas Gohr     * Needs to be called right after loading this helper plugin
85aa81d781SKlap-in     *
86aa81d781SKlap-in     * @param string $dbname
87aa81d781SKlap-in     * @param string $updatedir - Database update infos
88aa81d781SKlap-in     * @return bool
89a1e6784eSAndreas Gohr     */
908da7d805SAndreas Gohr    public function init($dbname, $updatedir)
918da7d805SAndreas Gohr    {
92801b921eSSzymon Olewniczak        if(!defined('DOKU_UNITTEST')) { // for now we don't want to trigger the deprecation warning in the tests
933a56750bSAndreas Gohr            dbg_deprecated(SQLiteDB::class);
94d0a5ba7aSAndreas Gohr        }
953a56750bSAndreas Gohr
968da7d805SAndreas Gohr        try {
978da7d805SAndreas Gohr            $this->adapter = new SQLiteDB($dbname, $updatedir, $this);
988da7d805SAndreas Gohr        } catch (Exception $e) {
998da7d805SAndreas Gohr            msg('SQLite: ' . $e->getMessage(), -1);
100c137e95fSAndreas Gohr            return false;
101c137e95fSAndreas Gohr        }
102a1e6784eSAndreas Gohr        return true;
103a1e6784eSAndreas Gohr    }
104a1e6784eSAndreas Gohr
105a1e6784eSAndreas Gohr    /**
1068da7d805SAndreas Gohr     * This is called from the adapter itself for backwards compatibility
1078da7d805SAndreas Gohr     *
1088da7d805SAndreas Gohr     * @param SQLiteDB $adapter
1098da7d805SAndreas Gohr     * @return void
110a1e6784eSAndreas Gohr     */
1118da7d805SAndreas Gohr    function setAdapter($adapter)
1128da7d805SAndreas Gohr    {
1138da7d805SAndreas Gohr        $this->adapter = $adapter;
114a34ef333SKlap-in    }
115f10ea6c1SKlap-in
116a1e6784eSAndreas Gohr    /**
1173ae3f79eSKlap-in     * Registers a User Defined Function for use in SQL statements
1183ae3f79eSKlap-in     */
1198da7d805SAndreas Gohr    public function create_function($function_name, $callback, $num_args)
1208da7d805SAndreas Gohr    {
12154f742ccSSzymon Olewniczak        $this->adapter->getDb()->sqliteCreateFunction($function_name, $callback, $num_args);
1223ae3f79eSKlap-in    }
1233ae3f79eSKlap-in
1248da7d805SAndreas Gohr    // region query and result handling functions
1258da7d805SAndreas Gohr
1263ae3f79eSKlap-in    /**
127e7b0736cSAndreas Gohr     * Convenience function to run an INSERT OR REPLACE operation
128e7b0736cSAndreas Gohr     *
129e7b0736cSAndreas Gohr     * The function takes a key-value array with the column names in the key and the actual value in the value,
130e7b0736cSAndreas Gohr     * build the appropriate query and executes it.
131e7b0736cSAndreas Gohr     *
132e7b0736cSAndreas Gohr     * @param string $table the table the entry should be saved to (will not be escaped)
133e7b0736cSAndreas Gohr     * @param array $entry A simple key-value pair array (only values will be escaped)
1348da7d805SAndreas Gohr     * @return bool
135e7b0736cSAndreas Gohr     */
1368da7d805SAndreas Gohr    public function storeEntry($table, $entry)
1378da7d805SAndreas Gohr    {
1388da7d805SAndreas Gohr        try {
1398da7d805SAndreas Gohr            $this->adapter->saveRecord($table, $entry);
1408da7d805SAndreas Gohr        } catch (\Exception $e) {
1418da7d805SAndreas Gohr            msg('SQLite: ' . $e->getMessage(), -1);
1428da7d805SAndreas Gohr            return false;
143e7b0736cSAndreas Gohr        }
144e7b0736cSAndreas Gohr
1458da7d805SAndreas Gohr        return true;
1468da7d805SAndreas Gohr    }
147e7b0736cSAndreas Gohr
148e7b0736cSAndreas Gohr    /**
149a1e6784eSAndreas Gohr     * Execute a query with the given parameters.
150a1e6784eSAndreas Gohr     *
151a1e6784eSAndreas Gohr     * Takes care of escaping
152a1e6784eSAndreas Gohr     *
153a2a82480SAndreas Gohr     *
154a2a82480SAndreas Gohr     * @param string ...$args - the arguments of query(), the first is the sql and others are values
155a1e6784eSAndreas Gohr     */
1568da7d805SAndreas Gohr    public function query()
1578da7d805SAndreas Gohr    {
158a1e6784eSAndreas Gohr        // get function arguments
159a1e6784eSAndreas Gohr        $args = func_get_args();
1606917135cSSzymon Olewniczak
1616917135cSSzymon Olewniczak        // clear the cache
1626917135cSSzymon Olewniczak        $this->data = null;
1636917135cSSzymon Olewniczak
1648da7d805SAndreas Gohr        try {
165*1973f122SSzymon Olewniczak            $sql = $this->prepareSql($args);
166*1973f122SSzymon Olewniczak            return $this->adapter->query($sql);
1678da7d805SAndreas Gohr        } catch (\Exception $e) {
1688da7d805SAndreas Gohr            msg('SQLite: ' . $e->getMessage(), -1);
1698da7d805SAndreas Gohr            return false;
1708da7d805SAndreas Gohr        }
17187fa2c18Sstretchyboy    }
172a1e6784eSAndreas Gohr
173*1973f122SSzymon Olewniczak    /**
174*1973f122SSzymon Olewniczak     * Prepare a query with the given arguments.
175*1973f122SSzymon Olewniczak     *
176*1973f122SSzymon Olewniczak     * Takes care of escaping
177*1973f122SSzymon Olewniczak     *
178*1973f122SSzymon Olewniczak     * @param array $args
179*1973f122SSzymon Olewniczak     *    array of arguments:
180*1973f122SSzymon Olewniczak     *      - string $sql - the statement
181*1973f122SSzymon Olewniczak     *      - arguments...
182*1973f122SSzymon Olewniczak     * @return bool|string
183*1973f122SSzymon Olewniczak     * @throws Exception
184*1973f122SSzymon Olewniczak     */
185*1973f122SSzymon Olewniczak    public function prepareSql($args) {
186*1973f122SSzymon Olewniczak
187*1973f122SSzymon Olewniczak        $sql = trim(array_shift($args));
188*1973f122SSzymon Olewniczak        $sql = rtrim($sql, ';');
189*1973f122SSzymon Olewniczak
190*1973f122SSzymon Olewniczak        if(!$sql) {
191*1973f122SSzymon Olewniczak            throw new \Exception('No SQL statement given', -1);
192*1973f122SSzymon Olewniczak        }
193*1973f122SSzymon Olewniczak
194*1973f122SSzymon Olewniczak        $argc = count($args);
195*1973f122SSzymon Olewniczak        if($argc > 0 && is_array($args[0])) {
196*1973f122SSzymon Olewniczak            $args = $args[0];
197*1973f122SSzymon Olewniczak            $argc = count($args);
198*1973f122SSzymon Olewniczak        }
199*1973f122SSzymon Olewniczak
200*1973f122SSzymon Olewniczak        // check number of arguments
201*1973f122SSzymon Olewniczak        $qmc = substr_count($sql, '?');
202*1973f122SSzymon Olewniczak        if ($argc < $qmc) {
203*1973f122SSzymon Olewniczak            throw new \Exception('Not enough arguments passed for statement. ' .
204*1973f122SSzymon Olewniczak                'Expected '.$qmc.' got '. $argc.' - '.hsc($sql));
205*1973f122SSzymon Olewniczak        } elseif($argc > $qmc) {
206*1973f122SSzymon Olewniczak            throw new \Exception('Too much arguments passed for statement. ' .
207*1973f122SSzymon Olewniczak                'Expected '.$qmc.' got '. $argc.' - '.hsc($sql));
208*1973f122SSzymon Olewniczak        }
209*1973f122SSzymon Olewniczak
210*1973f122SSzymon Olewniczak        // explode at wildcard, then join again
211*1973f122SSzymon Olewniczak        $parts = explode('?', $sql, $argc + 1);
212*1973f122SSzymon Olewniczak        $args  = array_map([$this->adapter->getDb(), 'quote'], $args);
213*1973f122SSzymon Olewniczak        $sql   = '';
214*1973f122SSzymon Olewniczak
215*1973f122SSzymon Olewniczak        while(($part = array_shift($parts)) !== null) {
216*1973f122SSzymon Olewniczak            $sql .= $part;
217*1973f122SSzymon Olewniczak            $sql .= array_shift($args);
218*1973f122SSzymon Olewniczak        }
219*1973f122SSzymon Olewniczak
220*1973f122SSzymon Olewniczak        return $sql;
221*1973f122SSzymon Olewniczak    }
222*1973f122SSzymon Olewniczak
223ff97cc8fSstretchyboy
224ff97cc8fSstretchyboy    /**
225b122d121SAndreas Gohr     * Closes the result set (and it's cursors)
226b122d121SAndreas Gohr     *
227b122d121SAndreas Gohr     * If you're doing SELECT queries inside a TRANSACTION, be sure to call this
228b122d121SAndreas Gohr     * function on all your results sets, before COMMITing the transaction.
229b122d121SAndreas Gohr     *
2304b4b2db0SGerrit Uitslag     * Also required when not all rows of a result are fetched
2314b4b2db0SGerrit Uitslag     *
2328da7d805SAndreas Gohr     * @param \PDOStatement $res
233b122d121SAndreas Gohr     * @return bool
234b122d121SAndreas Gohr     */
2358da7d805SAndreas Gohr    public function res_close($res)
2368da7d805SAndreas Gohr    {
2378da7d805SAndreas Gohr        if (!$res) return false;
2388da7d805SAndreas Gohr
2398da7d805SAndreas Gohr        return $res->closeCursor();
240b122d121SAndreas Gohr    }
241b122d121SAndreas Gohr
242b122d121SAndreas Gohr    /**
243aa81d781SKlap-in     * Returns a complete result set as array
2448da7d805SAndreas Gohr     *
2458da7d805SAndreas Gohr     * @param \PDOStatement $res
2468da7d805SAndreas Gohr     * @return array
247ff97cc8fSstretchyboy     */
2488da7d805SAndreas Gohr    public function res2arr($res, $assoc = true)
2498da7d805SAndreas Gohr    {
2508da7d805SAndreas Gohr        if (!$res) return [];
2518da7d805SAndreas Gohr
2528da7d805SAndreas Gohr        // this is a bullshit workaround for having res2arr and res2count work on one result
2538da7d805SAndreas Gohr        if (!$this->data) {
2548da7d805SAndreas Gohr            $mode = $assoc ? PDO::FETCH_ASSOC : PDO::FETCH_NUM;
2558da7d805SAndreas Gohr            $this->data = $res->fetchAll($mode);
2568da7d805SAndreas Gohr        }
2578da7d805SAndreas Gohr        return $this->data;
258b5b947d7SAndreas Gohr    }
259b5b947d7SAndreas Gohr
260b5b947d7SAndreas Gohr    /**
2618da7d805SAndreas Gohr     * Return the next row from the result set as associative array
2628da7d805SAndreas Gohr     *
2638da7d805SAndreas Gohr     * @param \PDOStatement $res
2648da7d805SAndreas Gohr     * @param int $rownum will be ignored
265b5b947d7SAndreas Gohr     */
2668da7d805SAndreas Gohr    public function res2row($res, $rownum = 0)
2678da7d805SAndreas Gohr    {
2688da7d805SAndreas Gohr        if (!$res) return false;
2698da7d805SAndreas Gohr
2708da7d805SAndreas Gohr        return $res->fetch(\PDO::FETCH_ASSOC);
271b5b947d7SAndreas Gohr    }
272b5b947d7SAndreas Gohr
273e7112ccbSAdrian Lang    /**
27444685fc6SKlap-in     * Return the first value from the next row.
2758da7d805SAndreas Gohr     *
2768da7d805SAndreas Gohr     * @param \PDOStatement $res
2778da7d805SAndreas Gohr     * @return mixed
278e7112ccbSAdrian Lang     */
2798da7d805SAndreas Gohr    public function res2single($res)
2808da7d805SAndreas Gohr    {
2818da7d805SAndreas Gohr        if (!$res) return false;
2828da7d805SAndreas Gohr
2838da7d805SAndreas Gohr        $data = $res->fetch(PDO::FETCH_NUM, PDO::FETCH_ORI_ABS, 0);
2848da7d805SAndreas Gohr        if (empty($data)) {
2858da7d805SAndreas Gohr            return false;
2868da7d805SAndreas Gohr        }
2878da7d805SAndreas Gohr        return $data[0];
288e7112ccbSAdrian Lang    }
289fee3b689Sstretchyboy
290fee3b689Sstretchyboy    /**
291fee3b689Sstretchyboy     * fetch the next row as zero indexed array
2928da7d805SAndreas Gohr     *
2938da7d805SAndreas Gohr     * @param \PDOStatement $res
2948da7d805SAndreas Gohr     * @return array|bool
295fee3b689Sstretchyboy     */
2968da7d805SAndreas Gohr    public function res_fetch_array($res)
2978da7d805SAndreas Gohr    {
2988da7d805SAndreas Gohr        if (!$res) return false;
2998da7d805SAndreas Gohr
3008da7d805SAndreas Gohr        return $res->fetch(PDO::FETCH_NUM);
30187fa2c18Sstretchyboy    }
302fee3b689Sstretchyboy
303fee3b689Sstretchyboy    /**
304fee3b689Sstretchyboy     * fetch the next row as assocative array
3058da7d805SAndreas Gohr     *
3068da7d805SAndreas Gohr     * @param \PDOStatement $res
3078da7d805SAndreas Gohr     * @return array|bool
308fee3b689Sstretchyboy     */
3098da7d805SAndreas Gohr    public function res_fetch_assoc($res)
3108da7d805SAndreas Gohr    {
3118da7d805SAndreas Gohr        if (!$res) return false;
3128da7d805SAndreas Gohr
3138da7d805SAndreas Gohr        return $res->fetch(PDO::FETCH_ASSOC);
314fee3b689Sstretchyboy    }
315fee3b689Sstretchyboy
316fee3b689Sstretchyboy    /**
31778977d74SKlap-in     * Count the number of records in result
3183157674bSAndreas Gohr     *
319db58e525SKlap-in     * This function is really inperformant in PDO and should be avoided!
3208da7d805SAndreas Gohr     *
3218da7d805SAndreas Gohr     * @param \PDOStatement $res
3228da7d805SAndreas Gohr     * @return int
323fee3b689Sstretchyboy     */
3248da7d805SAndreas Gohr    public function res2count($res)
3258da7d805SAndreas Gohr    {
3268da7d805SAndreas Gohr        if (!$res) return 0;
3278da7d805SAndreas Gohr
3288da7d805SAndreas Gohr        // this is a bullshit workaround for having res2arr and res2count work on one result
3298da7d805SAndreas Gohr        if (!$this->data) {
3308da7d805SAndreas Gohr            $this->data = $this->res2arr($res);
3318da7d805SAndreas Gohr        }
3328da7d805SAndreas Gohr
3338da7d805SAndreas Gohr        return count($this->data);
334fee3b689Sstretchyboy    }
33524a03f6cSstretchyboy
33624a03f6cSstretchyboy    /**
33724a03f6cSstretchyboy     * Count the number of records changed last time
3388da7d805SAndreas Gohr     *
3398da7d805SAndreas Gohr     * @param \PDOStatement $res
3408da7d805SAndreas Gohr     * @return int
34124a03f6cSstretchyboy     */
3428da7d805SAndreas Gohr    public function countChanges($res)
3438da7d805SAndreas Gohr    {
3448da7d805SAndreas Gohr        if (!$res) return 0;
3458da7d805SAndreas Gohr
3468da7d805SAndreas Gohr        return $res->rowCount();
347a1e6784eSAndreas Gohr    }
348a1e6784eSAndreas Gohr
3498da7d805SAndreas Gohr    // endregion
3508da7d805SAndreas Gohr
3518da7d805SAndreas Gohr    // region quoting/escaping functions
3528da7d805SAndreas Gohr
3538da7d805SAndreas Gohr    /**
3548da7d805SAndreas Gohr     * Join the given values and quote them for SQL insertion
3558da7d805SAndreas Gohr     */
3568da7d805SAndreas Gohr    public function quote_and_join($vals, $sep = ',')
3578da7d805SAndreas Gohr    {
35854f742ccSSzymon Olewniczak        $vals = array_map([$this->adapter->getDb(), 'quote'], $vals);
3598da7d805SAndreas Gohr        return join($sep, $vals);
3608da7d805SAndreas Gohr    }
3618da7d805SAndreas Gohr
3628da7d805SAndreas Gohr    /**
3638da7d805SAndreas Gohr     * Quotes a string, by escaping it and adding quotes
3648da7d805SAndreas Gohr     */
3658da7d805SAndreas Gohr    public function quote_string($string)
3668da7d805SAndreas Gohr    {
36754f742ccSSzymon Olewniczak        return $this->adapter->getDb()->quote($string);
3688da7d805SAndreas Gohr    }
3698da7d805SAndreas Gohr
3708da7d805SAndreas Gohr    /**
3718da7d805SAndreas Gohr     * Similar to quote_string, but without the quotes, useful to construct LIKE patterns
3728da7d805SAndreas Gohr     */
3738da7d805SAndreas Gohr    public function escape_string($str)
3748da7d805SAndreas Gohr    {
37554f742ccSSzymon Olewniczak        return trim($this->adapter->getDb()->quote($str), "'");
3768da7d805SAndreas Gohr    }
3778da7d805SAndreas Gohr
3788da7d805SAndreas Gohr    // endregion
3798da7d805SAndreas Gohr
3808da7d805SAndreas Gohr    // region speciality functions
3818da7d805SAndreas Gohr
3828da7d805SAndreas Gohr    /**
3838da7d805SAndreas Gohr     * Split sql queries on semicolons, unless when semicolons are quoted
3848da7d805SAndreas Gohr     *
3858da7d805SAndreas Gohr     * Usually you don't need this. It's only really needed if you need individual results for
3868da7d805SAndreas Gohr     * multiple queries. For example in the admin interface.
3878da7d805SAndreas Gohr     *
3888da7d805SAndreas Gohr     * @param string $sql
3898da7d805SAndreas Gohr     * @return array sql queries
3908da7d805SAndreas Gohr     * @deprecated
3918da7d805SAndreas Gohr     */
3928da7d805SAndreas Gohr    public function SQLstring2array($sql)
3938da7d805SAndreas Gohr    {
394d0a5ba7aSAndreas Gohr        if(!DOKU_UNITTEST) { // for now we don't want to trigger the deprecation warning in the tests
3958da7d805SAndreas Gohr            dbg_deprecated(Tools::class . '::SQLstring2array');
396d0a5ba7aSAndreas Gohr        }
3978da7d805SAndreas Gohr        return Tools::SQLstring2array($sql);
3988da7d805SAndreas Gohr    }
3998da7d805SAndreas Gohr
4008da7d805SAndreas Gohr    /**
4018da7d805SAndreas Gohr     * @deprecated needs to be fixed in stuct and structpublish
4028da7d805SAndreas Gohr     */
4038da7d805SAndreas Gohr    public function doTransaction($sql, $sqlpreparing = true) {
4048da7d805SAndreas Gohr        throw new \Exception(
4058da7d805SAndreas Gohr            'This method seems to never have done what it suggests. Please use the query() function instead.'
4068da7d805SAndreas Gohr        );
4078da7d805SAndreas Gohr    }
4088da7d805SAndreas Gohr
4098da7d805SAndreas Gohr    // endregion
410aa81d781SKlap-in}
411