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