1c3437056SNickeau<?php /** @noinspection SpellCheckingInspection */ 2c3437056SNickeau 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 1637748cd8SNickeau 1737748cd8SNickeauuse helper_plugin_sqlite; 18c3437056SNickeauuse RuntimeException; 1937748cd8SNickeau 2037748cd8SNickeauclass Sqlite 2137748cd8SNickeau{ 2237748cd8SNickeau 23c3437056SNickeau 24c3437056SNickeau /** 25c3437056SNickeau * Principal database 26c3437056SNickeau * (Backup) 27c3437056SNickeau */ 28c3437056SNickeau private const MAIN_DATABASE_NAME = "combo"; 29c3437056SNickeau /** 30c3437056SNickeau * Backend Databse 31c3437056SNickeau * (Log, Pub/Sub,...) 32c3437056SNickeau */ 33c3437056SNickeau private const SECONDARY_DB = "combo-secondary"; 34c3437056SNickeau 35c3437056SNickeau private static $sqliteVersion; 36c3437056SNickeau 37*04fd306cSNickeau 38*04fd306cSNickeau private helper_plugin_sqlite $sqlitePlugin; 39*04fd306cSNickeau 40c3437056SNickeau /** 41*04fd306cSNickeau * @var SqliteRequest the actual request. If not closed, it will be close. 42*04fd306cSNickeau * Otherwise, it's not possible to delete the database file. See {@link self::deleteDatabasesFile()} 43c3437056SNickeau */ 44*04fd306cSNickeau private SqliteRequest $actualRequest; 45*04fd306cSNickeau 46c3437056SNickeau 47c3437056SNickeau /** 48c3437056SNickeau * Sqlite constructor. 49c3437056SNickeau * @var helper_plugin_sqlite $sqlitePlugin 50c3437056SNickeau */ 51c3437056SNickeau public function __construct(helper_plugin_sqlite $sqlitePlugin) 52c3437056SNickeau { 53c3437056SNickeau $this->sqlitePlugin = $sqlitePlugin; 54c3437056SNickeau } 55c3437056SNickeau 5637748cd8SNickeau 5737748cd8SNickeau /** 5837748cd8SNickeau * 59c3437056SNickeau * @return Sqlite $sqlite 60*04fd306cSNickeau * @throws ExceptionSqliteNotAvailable 6137748cd8SNickeau */ 62*04fd306cSNickeau public static function createOrGetSqlite($databaseName = self::MAIN_DATABASE_NAME): Sqlite 6337748cd8SNickeau { 64c3437056SNickeau 65*04fd306cSNickeau $sqliteExecutionObjectIdentifier = Sqlite::class . "-$databaseName"; 66*04fd306cSNickeau $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 67*04fd306cSNickeau 68*04fd306cSNickeau try { 69*04fd306cSNickeau /** 70*04fd306cSNickeau * @var Sqlite $sqlite 71*04fd306cSNickeau * 72*04fd306cSNickeau * 73*04fd306cSNickeau * sqlite is stored globally 74*04fd306cSNickeau * because when we create a new instance, it will open the 75*04fd306cSNickeau * sqlite file. 76*04fd306cSNickeau * 77*04fd306cSNickeau * In a {@link cli_plugin_combo} run, you will run in the error: 78*04fd306cSNickeau * `` 79*04fd306cSNickeau * failed to open stream: Too many open files 80*04fd306cSNickeau * `` 81*04fd306cSNickeau * As there is by default a limit of 1024 open files 82*04fd306cSNickeau * which means that if there is more than 1024 pages 83*04fd306cSNickeau * that you replicate using a new sqlite instance each time, 84*04fd306cSNickeau * you fail. 85*04fd306cSNickeau * 86*04fd306cSNickeau */ 87*04fd306cSNickeau $sqlite = $executionContext->getRuntimeObject($sqliteExecutionObjectIdentifier); 88*04fd306cSNickeau } catch (ExceptionNotFound $e) { 89*04fd306cSNickeau $sqlite = null; 90*04fd306cSNickeau } 91*04fd306cSNickeau 92c3437056SNickeau if ($sqlite !== null) { 93c3437056SNickeau $res = $sqlite->doWeNeedToCreateNewInstance(); 94c3437056SNickeau if ($res === false) { 95c3437056SNickeau return $sqlite; 9637748cd8SNickeau } 9737748cd8SNickeau } 98c3437056SNickeau 9937748cd8SNickeau /** 10037748cd8SNickeau * Init 101c3437056SNickeau * @var helper_plugin_sqlite $sqlitePlugin 10237748cd8SNickeau */ 103c3437056SNickeau $sqlitePlugin = plugin_load('helper', 'sqlite'); 104c3437056SNickeau /** 105c3437056SNickeau * Not enabled / loaded 106c3437056SNickeau */ 107c3437056SNickeau if ($sqlitePlugin === null) { 108c3437056SNickeau 10937748cd8SNickeau $sqliteMandatoryMessage = "The Sqlite Plugin is mandatory. Some functionalities of the ComboStrap Plugin may not work."; 110*04fd306cSNickeau throw new ExceptionSqliteNotAvailable($sqliteMandatoryMessage); 11137748cd8SNickeau } 112c3437056SNickeau 113c3437056SNickeau $adapter = $sqlitePlugin->getAdapter(); 11437748cd8SNickeau if ($adapter == null) { 11537748cd8SNickeau self::sendMessageAsNotAvailable(); 11637748cd8SNickeau } 11737748cd8SNickeau 11837748cd8SNickeau $adapter->setUseNativeAlter(true); 11937748cd8SNickeau 120*04fd306cSNickeau list($databaseName, $databaseDefinitionDir) = self::getDatabaseNameAndDefinitionDirectory($databaseName); 121*04fd306cSNickeau $init = $sqlitePlugin->init($databaseName, $databaseDefinitionDir); 12237748cd8SNickeau if (!$init) { 12337748cd8SNickeau $message = "Unable to initialize Sqlite"; 124*04fd306cSNickeau throw new ExceptionSqliteNotAvailable($message); 125c3437056SNickeau } 12637748cd8SNickeau // regexp implementation 12737748cd8SNickeau // https://stackoverflow.com/questions/5071601/how-do-i-use-regex-in-a-sqlite-query/18484596#18484596 128c3437056SNickeau $adapter = $sqlitePlugin->getAdapter(); 12937748cd8SNickeau $adapter->create_function('regexp', 13037748cd8SNickeau function ($pattern, $data, $delimiter = '~', $modifiers = 'isuS') { 13137748cd8SNickeau if (isset($pattern, $data) === true) { 13237748cd8SNickeau return (preg_match(sprintf('%1$s%2$s%1$s%3$s', $delimiter, $pattern, $modifiers), $data) > 0); 13337748cd8SNickeau } 13437748cd8SNickeau return null; 13537748cd8SNickeau }, 13637748cd8SNickeau 4 13737748cd8SNickeau ); 138c3437056SNickeau 139c3437056SNickeau $sqlite = new Sqlite($sqlitePlugin); 140*04fd306cSNickeau $executionContext->setRuntimeObject($sqliteExecutionObjectIdentifier, $sqlite); 141c3437056SNickeau return $sqlite; 142c3437056SNickeau 14337748cd8SNickeau } 144c3437056SNickeau 145*04fd306cSNickeau /** 146*04fd306cSNickeau * @throws ExceptionSqliteNotAvailable 147*04fd306cSNickeau */ 148c3437056SNickeau public static function createOrGetBackendSqlite(): ?Sqlite 149c3437056SNickeau { 150c3437056SNickeau return self::createOrGetSqlite(self::SECONDARY_DB); 15137748cd8SNickeau } 152c3437056SNickeau 153c3437056SNickeau public static function createSelectFromTableAndColumns(string $tableName, array $columns = null): string 154c3437056SNickeau { 155c3437056SNickeau if ($columns === null) { 156c3437056SNickeau $columnStatement = "*"; 157c3437056SNickeau } else { 158c3437056SNickeau $columnsStatement = []; 159c3437056SNickeau foreach ($columns as $columnName) { 160c3437056SNickeau $columnsStatement[] = "$columnName as \"$columnName\""; 161c3437056SNickeau } 162c3437056SNickeau $columnStatement = implode(", ", $columnsStatement); 163c3437056SNickeau } 164c3437056SNickeau return "select $columnStatement from $tableName"; 16537748cd8SNickeau 16637748cd8SNickeau } 16737748cd8SNickeau 16837748cd8SNickeau /** 169*04fd306cSNickeau * Used in test to delete the database file 170*04fd306cSNickeau * @return void 171*04fd306cSNickeau * @throws ExceptionFileSystem - if we can delete the databases 172*04fd306cSNickeau */ 173*04fd306cSNickeau public static function deleteDatabasesFile() 174*04fd306cSNickeau { 175*04fd306cSNickeau /** 176*04fd306cSNickeau * The plugin does not give you the option to 177*04fd306cSNickeau * where to create the database file 178*04fd306cSNickeau * See {@link \helper_plugin_sqlite_adapter::initdb()} 179*04fd306cSNickeau * $this->dbfile = $conf['metadir'].'/'.$dbname.$this->fileextension; 180*04fd306cSNickeau * 181*04fd306cSNickeau * If error on delete, see {@link self::close()} 182*04fd306cSNickeau */ 183*04fd306cSNickeau $metadatDirectory = ExecutionContext::getActualOrCreateFromEnv() 184*04fd306cSNickeau ->getConfig() 185*04fd306cSNickeau ->getMetaDataDirectory(); 186*04fd306cSNickeau $fileChildren = FileSystems::getChildrenLeaf($metadatDirectory); 187*04fd306cSNickeau foreach ($fileChildren as $child) { 188*04fd306cSNickeau try { 189*04fd306cSNickeau $extension = $child->getExtension(); 190*04fd306cSNickeau } catch (ExceptionNotFound $e) { 191*04fd306cSNickeau // ok no extension 192*04fd306cSNickeau continue; 193*04fd306cSNickeau } 194*04fd306cSNickeau if (in_array($extension, ["sqlite", "sqlite3"])) { 195*04fd306cSNickeau FileSystems::delete($child); 196*04fd306cSNickeau } 197*04fd306cSNickeau 198*04fd306cSNickeau } 199*04fd306cSNickeau } 200*04fd306cSNickeau 201*04fd306cSNickeau private static function getDatabaseNameAndDefinitionDirectory($databaseName): array 202*04fd306cSNickeau { 203*04fd306cSNickeau global $conf; 204*04fd306cSNickeau 205*04fd306cSNickeau if ($databaseName === self::MAIN_DATABASE_NAME) { 206*04fd306cSNickeau $oldDbName = '404manager'; 207*04fd306cSNickeau $oldDbFile = $conf['metadir'] . "/{$oldDbName}.sqlite"; 208*04fd306cSNickeau $oldDbFileSqlite3 = $conf['metadir'] . "/{$oldDbName}.sqlite3"; 209*04fd306cSNickeau if (file_exists($oldDbFile) || file_exists($oldDbFileSqlite3)) { 210*04fd306cSNickeau $databaseName = $oldDbName; 211*04fd306cSNickeau } 212*04fd306cSNickeau } 213*04fd306cSNickeau 214*04fd306cSNickeau $databaseDir = DOKU_PLUGIN . PluginUtility::PLUGIN_BASE_NAME . "/db/$databaseName"; 215*04fd306cSNickeau return [$databaseName, $databaseDir]; 216*04fd306cSNickeau 217*04fd306cSNickeau } 218*04fd306cSNickeau 219*04fd306cSNickeau /** 22037748cd8SNickeau * Print debug info to the console in order to resolve 22137748cd8SNickeau * RuntimeException: HY000 8 attempt to write a readonly database 22237748cd8SNickeau * https://phpunit.readthedocs.io/en/latest/writing-tests-for-phpunit.html#error-output 22337748cd8SNickeau */ 224c3437056SNickeau public function printDbInfoAtConsole() 22537748cd8SNickeau { 226c3437056SNickeau $dbFile = $this->sqlitePlugin->getAdapter()->getDbFile(); 22737748cd8SNickeau fwrite(STDERR, "Stderr DbFile: " . $dbFile . "\n"); 22837748cd8SNickeau if (file_exists($dbFile)) { 22937748cd8SNickeau fwrite(STDERR, "File does exists\n"); 23037748cd8SNickeau fwrite(STDERR, "Permission " . substr(sprintf('%o', fileperms($dbFile)), -4) . "\n"); 23137748cd8SNickeau } else { 23237748cd8SNickeau fwrite(STDERR, "File does not exist\n"); 23337748cd8SNickeau } 23437748cd8SNickeau 23537748cd8SNickeau global $conf; 23637748cd8SNickeau $metadir = $conf['metadir']; 23737748cd8SNickeau fwrite(STDERR, "MetaDir: " . $metadir . "\n"); 23837748cd8SNickeau $subdir = strpos($dbFile, $metadir) === 0; 23937748cd8SNickeau if ($subdir) { 24037748cd8SNickeau fwrite(STDERR, "Meta is a subdirectory of the db \n"); 24137748cd8SNickeau } else { 24237748cd8SNickeau fwrite(STDERR, "Meta is a not subdirectory of the db \n"); 24337748cd8SNickeau } 24437748cd8SNickeau 24537748cd8SNickeau } 24637748cd8SNickeau 24737748cd8SNickeau /** 24837748cd8SNickeau * Json support 24937748cd8SNickeau */ 250c3437056SNickeau public function supportJson(): bool 25137748cd8SNickeau { 25237748cd8SNickeau 25337748cd8SNickeau 254c3437056SNickeau $res = $this->sqlitePlugin->query("PRAGMA compile_options"); 25537748cd8SNickeau $isJsonEnabled = false; 256c3437056SNickeau foreach ($this->sqlitePlugin->res2arr($res) as $row) { 25737748cd8SNickeau if ($row["compile_option"] === "ENABLE_JSON1") { 25837748cd8SNickeau $isJsonEnabled = true; 25937748cd8SNickeau break; 26037748cd8SNickeau } 26137748cd8SNickeau }; 262c3437056SNickeau $this->sqlitePlugin->res_close($res); 26337748cd8SNickeau return $isJsonEnabled; 26437748cd8SNickeau } 26537748cd8SNickeau 26637748cd8SNickeau 267*04fd306cSNickeau /** 268*04fd306cSNickeau * @throws ExceptionSqliteNotAvailable 269*04fd306cSNickeau */ 270c3437056SNickeau public 271c3437056SNickeau static function sendMessageAsNotAvailable(): void 27237748cd8SNickeau { 27337748cd8SNickeau $sqliteMandatoryMessage = "The Sqlite Php Extension is mandatory. It seems that it's not available on this installation."; 274*04fd306cSNickeau throw new ExceptionSqliteNotAvailable($sqliteMandatoryMessage); 27537748cd8SNickeau } 276c3437056SNickeau 277c3437056SNickeau /** 278*04fd306cSNickeau * 279*04fd306cSNickeau * Old check when there was no {@link ExecutionContext} 280*04fd306cSNickeau * to reset the Sqlite variable 281*04fd306cSNickeau * TODO: delete ? 282c3437056SNickeau * 283c3437056SNickeau * 284c3437056SNickeau */ 285c3437056SNickeau private function doWeNeedToCreateNewInstance(): bool 286c3437056SNickeau { 287c3437056SNickeau 288c3437056SNickeau global $conf; 289c3437056SNickeau $metaDir = $conf['metadir']; 290c3437056SNickeau 291c3437056SNickeau /** 292c3437056SNickeau * Adapter may be null 293c3437056SNickeau * when the SQLite & PDO SQLite 294c3437056SNickeau * are not installed 295c3437056SNickeau * ie: SQLite & PDO SQLite support missing 296c3437056SNickeau */ 297c3437056SNickeau $adapter = $this->sqlitePlugin->getAdapter(); 298c3437056SNickeau if ($adapter === null) { 299c3437056SNickeau return true; 300c3437056SNickeau } 301c3437056SNickeau 302c3437056SNickeau /** 303c3437056SNickeau * When the database is {@link \helper_plugin_sqlite_adapter::closedb()} 304c3437056SNickeau */ 305c3437056SNickeau if ($adapter->getDb() === null) { 306c3437056SNickeau /** 307c3437056SNickeau * We may also open it again 308c3437056SNickeau * {@link \helper_plugin_sqlite_adapter::opendb()} 309c3437056SNickeau * for now, reinit 310c3437056SNickeau */ 311c3437056SNickeau return true; 312c3437056SNickeau } 313c3437056SNickeau /** 314c3437056SNickeau * In test, we are running in different context (ie different root 315c3437056SNickeau * directory for DokuWiki and therefore different $conf 316c3437056SNickeau * and therefore different metadir where sqlite is stored) 317c3437056SNickeau * Because a sql file may be deleted, we may get: 318c3437056SNickeau * ``` 319c3437056SNickeau * RuntimeException: HY000 8 attempt to write a readonly database: 320c3437056SNickeau * ``` 321c3437056SNickeau * To avoid this error, we check that we are still in the same metadir 322c3437056SNickeau * where the sqlite database is stored. If not, we create a new instance 323c3437056SNickeau */ 324c3437056SNickeau $dbFile = $adapter->getDbFile(); 325c3437056SNickeau if (!file_exists($dbFile)) { 326c3437056SNickeau $this->close(); 327c3437056SNickeau return true; 328c3437056SNickeau } 329c3437056SNickeau // the file is in the meta directory 330c3437056SNickeau if (strpos($dbFile, $metaDir) === 0) { 331c3437056SNickeau // we are still in a class run 332c3437056SNickeau return false; 333c3437056SNickeau } 334c3437056SNickeau $this->close(); 335c3437056SNickeau return true; 336c3437056SNickeau } 337c3437056SNickeau 338*04fd306cSNickeau public function close() 339c3437056SNickeau { 340c3437056SNickeau 341*04fd306cSNickeau /** 342*04fd306cSNickeau * https://www.php.net/manual/en/pdo.connections.php#114822 343*04fd306cSNickeau * You put the variable connection on null 344*04fd306cSNickeau * the {@link \helper_plugin_sqlite_adapter::closedb() function} do that 345*04fd306cSNickeau * 346*04fd306cSNickeau * If we don't do that, the file is still locked 347*04fd306cSNickeau * by the sqlite process and the clean up process 348*04fd306cSNickeau * of dokuwiki test cannot delete it 349*04fd306cSNickeau * 350*04fd306cSNickeau * ie to avoid 351*04fd306cSNickeau * RuntimeException: Unable to delete the file 352*04fd306cSNickeau * (C:/Users/GERARD~1/AppData/Local/Temp/dwtests-1676813655.6773/data/meta/combo-secondary.sqlite3) in D:\dokuwiki\_test\core\TestUtils.php on line 58 353*04fd306cSNickeau * {@link TestUtils::rdelete} 354*04fd306cSNickeau * 355*04fd306cSNickeau * Windows sort of handling/ bug explained here 356*04fd306cSNickeau * https://bugs.php.net/bug.php?id=78930&edit=3 357*04fd306cSNickeau * 358*04fd306cSNickeau * Null to close the db explanation and bug 359*04fd306cSNickeau * https://bugs.php.net/bug.php?id=62065 360*04fd306cSNickeau * 361*04fd306cSNickeau */ 362*04fd306cSNickeau 363*04fd306cSNickeau $this->closeActualRequestIfNotClosed(); 364*04fd306cSNickeau 365c3437056SNickeau $adapter = $this->sqlitePlugin->getAdapter(); 366c3437056SNickeau if ($adapter !== null) { 367*04fd306cSNickeau 368c3437056SNickeau $adapter->closedb(); 369c3437056SNickeau 370*04fd306cSNickeau unset($adapter); 371*04fd306cSNickeau 372*04fd306cSNickeau gc_collect_cycles(); 373c3437056SNickeau 374c3437056SNickeau } 375c3437056SNickeau 376c3437056SNickeau } 377c3437056SNickeau 378c3437056SNickeau public function getDbName(): string 379c3437056SNickeau { 380c3437056SNickeau return $this->sqlitePlugin->getAdapter()->getName(); 381c3437056SNickeau } 382c3437056SNickeau 383c3437056SNickeau 384c3437056SNickeau public function getSqlitePlugin(): helper_plugin_sqlite 385c3437056SNickeau { 386c3437056SNickeau return $this->sqlitePlugin; 387c3437056SNickeau } 388c3437056SNickeau 389c3437056SNickeau public function createRequest(): SqliteRequest 390c3437056SNickeau { 391*04fd306cSNickeau $this->closeActualRequestIfNotClosed(); 392*04fd306cSNickeau $this->actualRequest = new SqliteRequest($this); 393*04fd306cSNickeau return $this->actualRequest; 394c3437056SNickeau } 395c3437056SNickeau 396c3437056SNickeau public function getVersion() 397c3437056SNickeau { 398c3437056SNickeau if (self::$sqliteVersion === null) { 399c3437056SNickeau $request = $this->createRequest() 400c3437056SNickeau ->setQuery("select sqlite_version()"); 401c3437056SNickeau try { 402c3437056SNickeau self::$sqliteVersion = $request 403c3437056SNickeau ->execute() 404c3437056SNickeau ->getFirstCellValue(); 405*04fd306cSNickeau } catch (ExceptionCompile $e) { 406c3437056SNickeau self::$sqliteVersion = "unknown"; 407c3437056SNickeau } finally { 408c3437056SNickeau $request->close(); 409c3437056SNickeau } 410c3437056SNickeau } 411c3437056SNickeau return self::$sqliteVersion; 412c3437056SNickeau } 413c3437056SNickeau 414c3437056SNickeau /** 415c3437056SNickeau * @param string $option 416c3437056SNickeau * @return bool - true if the option is available 417c3437056SNickeau */ 418c3437056SNickeau public function hasOption(string $option): bool 419c3437056SNickeau { 420c3437056SNickeau try { 421c3437056SNickeau $present = $this->createRequest() 422*04fd306cSNickeau ->setQueryParametrized("select count(1) from pragma_compile_options() where compile_options = ?", [$option]) 423c3437056SNickeau ->execute() 424c3437056SNickeau ->getFirstCellValueAsInt(); 425c3437056SNickeau return $present === 1; 426*04fd306cSNickeau } catch (ExceptionCompile $e) { 427c3437056SNickeau LogUtility::msg("Error while trying to see if the sqlite option is available"); 428c3437056SNickeau return false; 429c3437056SNickeau } 430c3437056SNickeau 431c3437056SNickeau } 432*04fd306cSNickeau 433*04fd306cSNickeau /** 434*04fd306cSNickeau * Internal function that closes the actual request 435*04fd306cSNickeau * This is to be able to close all resources even if the developer 436*04fd306cSNickeau * forget. 437*04fd306cSNickeau * 438*04fd306cSNickeau * This is needed to be able to delete the database file. 439*04fd306cSNickeau * See {@link self::close()} for more information 440*04fd306cSNickeau * 441*04fd306cSNickeau * @return void 442*04fd306cSNickeau */ 443*04fd306cSNickeau private function closeActualRequestIfNotClosed() 444*04fd306cSNickeau { 445*04fd306cSNickeau if(isset($this->actualRequest)){ 446*04fd306cSNickeau $this->actualRequest->close(); 447*04fd306cSNickeau unset($this->actualRequest); 448*04fd306cSNickeau } 449*04fd306cSNickeau } 45037748cd8SNickeau} 451