xref: /plugin/combo/ComboStrap/Sqlite.php (revision c3437056399326d621a01da73b649707fbb0ae69)
1*c3437056SNickeau<?php /** @noinspection SpellCheckingInspection */
2*c3437056SNickeau
337748cd8SNickeau/**
437748cd8SNickeau * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved.
537748cd8SNickeau *
637748cd8SNickeau * This source code is licensed under the GPL license found in the
737748cd8SNickeau * COPYING  file in the root directory of this source tree.
837748cd8SNickeau *
937748cd8SNickeau * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
1037748cd8SNickeau * @author   ComboStrap <support@combostrap.com>
1137748cd8SNickeau *
1237748cd8SNickeau */
1337748cd8SNickeau
1437748cd8SNickeaunamespace ComboStrap;
1537748cd8SNickeau
16*c3437056SNickeaurequire_once(__DIR__ . '/PluginUtility.php');
1737748cd8SNickeau
1837748cd8SNickeauuse helper_plugin_sqlite;
19*c3437056SNickeauuse RuntimeException;
2037748cd8SNickeau
2137748cd8SNickeauclass Sqlite
2237748cd8SNickeau{
2337748cd8SNickeau
24*c3437056SNickeau    /** @var Sqlite[] $sqlite */
25*c3437056SNickeau    private static $sqlites;
26*c3437056SNickeau
27*c3437056SNickeau    /**
28*c3437056SNickeau     * Principal database
29*c3437056SNickeau     * (Backup)
30*c3437056SNickeau     */
31*c3437056SNickeau    private const  MAIN_DATABASE_NAME = "combo";
32*c3437056SNickeau    /**
33*c3437056SNickeau     * Backend Databse
34*c3437056SNickeau     * (Log, Pub/Sub,...)
35*c3437056SNickeau     */
36*c3437056SNickeau    private const  SECONDARY_DB = "combo-secondary";
37*c3437056SNickeau
38*c3437056SNickeau    private static $sqliteVersion;
39*c3437056SNickeau
40*c3437056SNickeau    /**
41*c3437056SNickeau     * @var helper_plugin_sqlite
42*c3437056SNickeau     */
43*c3437056SNickeau    private $sqlitePlugin;
44*c3437056SNickeau
45*c3437056SNickeau    /**
46*c3437056SNickeau     * Sqlite constructor.
47*c3437056SNickeau     * @var helper_plugin_sqlite $sqlitePlugin
48*c3437056SNickeau     */
49*c3437056SNickeau    public function __construct(helper_plugin_sqlite $sqlitePlugin)
50*c3437056SNickeau    {
51*c3437056SNickeau        $this->sqlitePlugin = $sqlitePlugin;
52*c3437056SNickeau    }
53*c3437056SNickeau
5437748cd8SNickeau
5537748cd8SNickeau    /**
5637748cd8SNickeau     *
57*c3437056SNickeau     * @return Sqlite $sqlite
5837748cd8SNickeau     */
59*c3437056SNickeau    public static function createOrGetSqlite($databaseName = self::MAIN_DATABASE_NAME): ?Sqlite
6037748cd8SNickeau    {
61*c3437056SNickeau
62*c3437056SNickeau        $sqlite = self::$sqlites[$databaseName];
63*c3437056SNickeau        if ($sqlite !== null) {
64*c3437056SNickeau            $res = $sqlite->doWeNeedToCreateNewInstance();
65*c3437056SNickeau            if ($res === false) {
66*c3437056SNickeau                return $sqlite;
6737748cd8SNickeau            }
6837748cd8SNickeau        }
69*c3437056SNickeau
7037748cd8SNickeau
7137748cd8SNickeau        /**
7237748cd8SNickeau         * Init
73*c3437056SNickeau         * @var helper_plugin_sqlite $sqlitePlugin
7437748cd8SNickeau         */
75*c3437056SNickeau        $sqlitePlugin = plugin_load('helper', 'sqlite');
76*c3437056SNickeau        /**
77*c3437056SNickeau         * Not enabled / loaded
78*c3437056SNickeau         */
79*c3437056SNickeau        if ($sqlitePlugin === null) {
80*c3437056SNickeau
8137748cd8SNickeau            $sqliteMandatoryMessage = "The Sqlite Plugin is mandatory. Some functionalities of the ComboStrap Plugin may not work.";
8237748cd8SNickeau            LogUtility::log2FrontEnd($sqliteMandatoryMessage, LogUtility::LVL_MSG_ERROR);
8337748cd8SNickeau            return null;
8437748cd8SNickeau        }
85*c3437056SNickeau
86*c3437056SNickeau        $adapter = $sqlitePlugin->getAdapter();
8737748cd8SNickeau        if ($adapter == null) {
8837748cd8SNickeau            self::sendMessageAsNotAvailable();
8937748cd8SNickeau            return null;
9037748cd8SNickeau        }
9137748cd8SNickeau
9237748cd8SNickeau        $adapter->setUseNativeAlter(true);
9337748cd8SNickeau
9437748cd8SNickeau        global $conf;
9537748cd8SNickeau
96*c3437056SNickeau        if ($databaseName === self::MAIN_DATABASE_NAME) {
9737748cd8SNickeau            $oldDbName = '404manager';
9837748cd8SNickeau            $oldDbFile = $conf['metadir'] . "/{$oldDbName}.sqlite";
9937748cd8SNickeau            $oldDbFileSqlite3 = $conf['metadir'] . "/{$oldDbName}.sqlite3";
10037748cd8SNickeau            if (file_exists($oldDbFile) || file_exists($oldDbFileSqlite3)) {
101*c3437056SNickeau                $databaseName = $oldDbName;
102*c3437056SNickeau            }
10337748cd8SNickeau        }
10437748cd8SNickeau
105*c3437056SNickeau        $updatedir = DOKU_PLUGIN . PluginUtility::PLUGIN_BASE_NAME . "/db/$databaseName";
106*c3437056SNickeau        $init = $sqlitePlugin->init($databaseName, $updatedir);
10737748cd8SNickeau        if (!$init) {
10837748cd8SNickeau            # TODO: Message 'SqliteUnableToInitialize'
10937748cd8SNickeau            $message = "Unable to initialize Sqlite";
11037748cd8SNickeau            LogUtility::msg($message, MSG_MANAGERS_ONLY);
111*c3437056SNickeau            return null;
112*c3437056SNickeau        }
11337748cd8SNickeau        // regexp implementation
11437748cd8SNickeau        // https://stackoverflow.com/questions/5071601/how-do-i-use-regex-in-a-sqlite-query/18484596#18484596
115*c3437056SNickeau        $adapter = $sqlitePlugin->getAdapter();
11637748cd8SNickeau        $adapter->create_function('regexp',
11737748cd8SNickeau            function ($pattern, $data, $delimiter = '~', $modifiers = 'isuS') {
11837748cd8SNickeau                if (isset($pattern, $data) === true) {
11937748cd8SNickeau                    return (preg_match(sprintf('%1$s%2$s%1$s%3$s', $delimiter, $pattern, $modifiers), $data) > 0);
12037748cd8SNickeau                }
12137748cd8SNickeau                return null;
12237748cd8SNickeau            },
12337748cd8SNickeau            4
12437748cd8SNickeau        );
125*c3437056SNickeau
126*c3437056SNickeau        $sqlite = new Sqlite($sqlitePlugin);
127*c3437056SNickeau        self::$sqlites[$databaseName] = $sqlite;
128*c3437056SNickeau        return $sqlite;
129*c3437056SNickeau
13037748cd8SNickeau    }
131*c3437056SNickeau
132*c3437056SNickeau    public static function createOrGetBackendSqlite(): ?Sqlite
133*c3437056SNickeau    {
134*c3437056SNickeau        return self::createOrGetSqlite(self::SECONDARY_DB);
13537748cd8SNickeau    }
136*c3437056SNickeau
137*c3437056SNickeau    public static function createSelectFromTableAndColumns(string $tableName, array $columns = null): string
138*c3437056SNickeau    {
139*c3437056SNickeau        if ($columns === null) {
140*c3437056SNickeau            $columnStatement = "*";
141*c3437056SNickeau        } else {
142*c3437056SNickeau            $columnsStatement = [];
143*c3437056SNickeau            foreach ($columns as $columnName) {
144*c3437056SNickeau                $columnsStatement[] = "$columnName as \"$columnName\"";
145*c3437056SNickeau            }
146*c3437056SNickeau            $columnStatement = implode(", ", $columnsStatement);
147*c3437056SNickeau        }
148*c3437056SNickeau        return "select $columnStatement from $tableName";
14937748cd8SNickeau
15037748cd8SNickeau    }
15137748cd8SNickeau
15237748cd8SNickeau    /**
15337748cd8SNickeau     * Print debug info to the console in order to resolve
15437748cd8SNickeau     * RuntimeException: HY000 8 attempt to write a readonly database
15537748cd8SNickeau     * https://phpunit.readthedocs.io/en/latest/writing-tests-for-phpunit.html#error-output
15637748cd8SNickeau     */
157*c3437056SNickeau    public function printDbInfoAtConsole()
15837748cd8SNickeau    {
159*c3437056SNickeau        $dbFile = $this->sqlitePlugin->getAdapter()->getDbFile();
16037748cd8SNickeau        fwrite(STDERR, "Stderr DbFile: " . $dbFile . "\n");
16137748cd8SNickeau        if (file_exists($dbFile)) {
16237748cd8SNickeau            fwrite(STDERR, "File does exists\n");
16337748cd8SNickeau            fwrite(STDERR, "Permission " . substr(sprintf('%o', fileperms($dbFile)), -4) . "\n");
16437748cd8SNickeau        } else {
16537748cd8SNickeau            fwrite(STDERR, "File does not exist\n");
16637748cd8SNickeau        }
16737748cd8SNickeau
16837748cd8SNickeau        global $conf;
16937748cd8SNickeau        $metadir = $conf['metadir'];
17037748cd8SNickeau        fwrite(STDERR, "MetaDir: " . $metadir . "\n");
17137748cd8SNickeau        $subdir = strpos($dbFile, $metadir) === 0;
17237748cd8SNickeau        if ($subdir) {
17337748cd8SNickeau            fwrite(STDERR, "Meta is a subdirectory of the db \n");
17437748cd8SNickeau        } else {
17537748cd8SNickeau            fwrite(STDERR, "Meta is a not subdirectory of the db \n");
17637748cd8SNickeau        }
17737748cd8SNickeau
17837748cd8SNickeau    }
17937748cd8SNickeau
18037748cd8SNickeau    /**
18137748cd8SNickeau     * Json support
18237748cd8SNickeau     */
183*c3437056SNickeau    public function supportJson(): bool
18437748cd8SNickeau    {
18537748cd8SNickeau
18637748cd8SNickeau
187*c3437056SNickeau        $res = $this->sqlitePlugin->query("PRAGMA compile_options");
18837748cd8SNickeau        $isJsonEnabled = false;
189*c3437056SNickeau        foreach ($this->sqlitePlugin->res2arr($res) as $row) {
19037748cd8SNickeau            if ($row["compile_option"] === "ENABLE_JSON1") {
19137748cd8SNickeau                $isJsonEnabled = true;
19237748cd8SNickeau                break;
19337748cd8SNickeau            }
19437748cd8SNickeau        };
195*c3437056SNickeau        $this->sqlitePlugin->res_close($res);
19637748cd8SNickeau        return $isJsonEnabled;
19737748cd8SNickeau    }
19837748cd8SNickeau
19937748cd8SNickeau
200*c3437056SNickeau    public
201*c3437056SNickeau    static function sendMessageAsNotAvailable(): void
20237748cd8SNickeau    {
20337748cd8SNickeau        $sqliteMandatoryMessage = "The Sqlite Php Extension is mandatory. It seems that it's not available on this installation.";
20437748cd8SNickeau        LogUtility::log2FrontEnd($sqliteMandatoryMessage, LogUtility::LVL_MSG_ERROR);
20537748cd8SNickeau    }
206*c3437056SNickeau
207*c3437056SNickeau    /**
208*c3437056SNickeau     * sqlite is stored in a static variable
209*c3437056SNickeau     * because when we run the {@link cli_plugin_combo},
210*c3437056SNickeau     * we will run in the error:
211*c3437056SNickeau     * ``
212*c3437056SNickeau     * failed to open stream: Too many open files
213*c3437056SNickeau     * ``
214*c3437056SNickeau     * There is by default a limit of 1024 open files
215*c3437056SNickeau     * which means that if there is more than 1024 pages, you fail.
216*c3437056SNickeau     *
217*c3437056SNickeau     *
218*c3437056SNickeau     */
219*c3437056SNickeau    private function doWeNeedToCreateNewInstance(): bool
220*c3437056SNickeau    {
221*c3437056SNickeau
222*c3437056SNickeau        global $conf;
223*c3437056SNickeau        $metaDir = $conf['metadir'];
224*c3437056SNickeau
225*c3437056SNickeau
226*c3437056SNickeau        /**
227*c3437056SNickeau         * Adapter may be null
228*c3437056SNickeau         * when the SQLite & PDO SQLite
229*c3437056SNickeau         * are not installed
230*c3437056SNickeau         * ie: SQLite & PDO SQLite support missing
231*c3437056SNickeau         */
232*c3437056SNickeau        $adapter = $this->sqlitePlugin->getAdapter();
233*c3437056SNickeau        if ($adapter === null) {
234*c3437056SNickeau            return true;
235*c3437056SNickeau        }
236*c3437056SNickeau
237*c3437056SNickeau        /**
238*c3437056SNickeau         * When the database is {@link \helper_plugin_sqlite_adapter::closedb()}
239*c3437056SNickeau         */
240*c3437056SNickeau        if ($adapter->getDb() === null) {
241*c3437056SNickeau            /**
242*c3437056SNickeau             * We may also open it again
243*c3437056SNickeau             * {@link \helper_plugin_sqlite_adapter::opendb()}
244*c3437056SNickeau             * for now, reinit
245*c3437056SNickeau             */
246*c3437056SNickeau            return true;
247*c3437056SNickeau        }
248*c3437056SNickeau        /**
249*c3437056SNickeau         * In test, we are running in different context (ie different root
250*c3437056SNickeau         * directory for DokuWiki and therefore different $conf
251*c3437056SNickeau         * and therefore different metadir where sqlite is stored)
252*c3437056SNickeau         * Because a sql file may be deleted, we may get:
253*c3437056SNickeau         * ```
254*c3437056SNickeau         * RuntimeException: HY000 8 attempt to write a readonly database:
255*c3437056SNickeau         * ```
256*c3437056SNickeau         * To avoid this error, we check that we are still in the same metadir
257*c3437056SNickeau         * where the sqlite database is stored. If not, we create a new instance
258*c3437056SNickeau         */
259*c3437056SNickeau        $dbFile = $adapter->getDbFile();
260*c3437056SNickeau        if (!file_exists($dbFile)) {
261*c3437056SNickeau            $this->close();
262*c3437056SNickeau            return true;
263*c3437056SNickeau        }
264*c3437056SNickeau        // the file is in the meta directory
265*c3437056SNickeau        if (strpos($dbFile, $metaDir) === 0) {
266*c3437056SNickeau            // we are still in a class run
267*c3437056SNickeau            return false;
268*c3437056SNickeau        }
269*c3437056SNickeau        $this->close();
270*c3437056SNickeau        return true;
271*c3437056SNickeau    }
272*c3437056SNickeau
273*c3437056SNickeau    public
274*c3437056SNickeau    function close()
275*c3437056SNickeau    {
276*c3437056SNickeau
277*c3437056SNickeau        $adapter = $this->sqlitePlugin->getAdapter();
278*c3437056SNickeau        if ($adapter !== null) {
279*c3437056SNickeau            /**
280*c3437056SNickeau             * https://www.php.net/manual/en/pdo.connections.php#114822
281*c3437056SNickeau             * You put the connection on null
282*c3437056SNickeau             * CloseDb do that
283*c3437056SNickeau             */
284*c3437056SNickeau            $adapter->closedb();
285*c3437056SNickeau
286*c3437056SNickeau            /**
287*c3437056SNickeau             * Delete the file If we can't delete the file
288*c3437056SNickeau             * there is a resource still open
289*c3437056SNickeau             */
290*c3437056SNickeau            $sqliteFile = $adapter->getDbFile();
291*c3437056SNickeau            if (file_exists($sqliteFile)) {
292*c3437056SNickeau                $result = unlink($sqliteFile);
293*c3437056SNickeau                if ($result === false) {
294*c3437056SNickeau                    throw new RuntimeException("Unable to delete the file ($sqliteFile). Did you close all resources ?");
295*c3437056SNickeau                }
296*c3437056SNickeau            }
297*c3437056SNickeau
298*c3437056SNickeau        }
299*c3437056SNickeau        /**
300*c3437056SNickeau         * Forwhatever reason, closing in php
301*c3437056SNickeau         * is putting the variable to null
302*c3437056SNickeau         * We do it also in the static variable to be sure
303*c3437056SNickeau         */
304*c3437056SNickeau        self::$sqlites[$this->getDbName()] == null;
305*c3437056SNickeau
306*c3437056SNickeau
307*c3437056SNickeau    }
308*c3437056SNickeau
309*c3437056SNickeau    public function getDbName(): string
310*c3437056SNickeau    {
311*c3437056SNickeau        return $this->sqlitePlugin->getAdapter()->getName();
312*c3437056SNickeau    }
313*c3437056SNickeau
314*c3437056SNickeau    public static function closeAll()
315*c3437056SNickeau    {
316*c3437056SNickeau
317*c3437056SNickeau        $sqlites = self::$sqlites;
318*c3437056SNickeau        if ($sqlites !== null) {
319*c3437056SNickeau            foreach ($sqlites as $sqlite) {
320*c3437056SNickeau                $sqlite->close();
321*c3437056SNickeau            }
322*c3437056SNickeau            /**
323*c3437056SNickeau             * Set it to null
324*c3437056SNickeau             */
325*c3437056SNickeau            Sqlite::$sqlites = null;
326*c3437056SNickeau        }
327*c3437056SNickeau    }
328*c3437056SNickeau
329*c3437056SNickeau
330*c3437056SNickeau    public function getSqlitePlugin(): helper_plugin_sqlite
331*c3437056SNickeau    {
332*c3437056SNickeau        return $this->sqlitePlugin;
333*c3437056SNickeau    }
334*c3437056SNickeau
335*c3437056SNickeau    public function createRequest(): SqliteRequest
336*c3437056SNickeau    {
337*c3437056SNickeau        return new SqliteRequest($this);
338*c3437056SNickeau    }
339*c3437056SNickeau
340*c3437056SNickeau    public function getVersion()
341*c3437056SNickeau    {
342*c3437056SNickeau        if (self::$sqliteVersion === null) {
343*c3437056SNickeau            $request = $this->createRequest()
344*c3437056SNickeau                ->setQuery("select sqlite_version()");
345*c3437056SNickeau            try {
346*c3437056SNickeau                self::$sqliteVersion = $request
347*c3437056SNickeau                    ->execute()
348*c3437056SNickeau                    ->getFirstCellValue();
349*c3437056SNickeau            } catch (ExceptionCombo $e) {
350*c3437056SNickeau                self::$sqliteVersion = "unknown";
351*c3437056SNickeau            } finally {
352*c3437056SNickeau                $request->close();
353*c3437056SNickeau            }
354*c3437056SNickeau        }
355*c3437056SNickeau        return self::$sqliteVersion;
356*c3437056SNickeau    }
357*c3437056SNickeau
358*c3437056SNickeau    /**
359*c3437056SNickeau     * @param string $option
360*c3437056SNickeau     * @return bool - true if the option is available
361*c3437056SNickeau     */
362*c3437056SNickeau    public function hasOption(string $option): bool
363*c3437056SNickeau    {
364*c3437056SNickeau        try {
365*c3437056SNickeau            $present = $this->createRequest()
366*c3437056SNickeau                ->setQueryParametrized("select count(1) from pragma_compile_options() where compile_options = ?", $option)
367*c3437056SNickeau                ->execute()
368*c3437056SNickeau                ->getFirstCellValueAsInt();
369*c3437056SNickeau            return $present === 1;
370*c3437056SNickeau        } catch (ExceptionCombo $e) {
371*c3437056SNickeau            LogUtility::msg("Error while trying to see if the sqlite option is available");
372*c3437056SNickeau            return false;
373*c3437056SNickeau        }
374*c3437056SNickeau
375*c3437056SNickeau    }
37637748cd8SNickeau}
377