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 3704fd306cSNickeau 3804fd306cSNickeau private helper_plugin_sqlite $sqlitePlugin; 3904fd306cSNickeau 40c3437056SNickeau /** 4104fd306cSNickeau * @var SqliteRequest the actual request. If not closed, it will be close. 4204fd306cSNickeau * Otherwise, it's not possible to delete the database file. See {@link self::deleteDatabasesFile()} 43c3437056SNickeau */ 4404fd306cSNickeau private SqliteRequest $actualRequest; 4504fd306cSNickeau 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 6004fd306cSNickeau * @throws ExceptionSqliteNotAvailable 6137748cd8SNickeau */ 6204fd306cSNickeau public static function createOrGetSqlite($databaseName = self::MAIN_DATABASE_NAME): Sqlite 6337748cd8SNickeau { 64c3437056SNickeau 6504fd306cSNickeau $sqliteExecutionObjectIdentifier = Sqlite::class . "-$databaseName"; 6604fd306cSNickeau $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 6704fd306cSNickeau 6804fd306cSNickeau try { 6904fd306cSNickeau /** 7004fd306cSNickeau * @var Sqlite $sqlite 7104fd306cSNickeau * 7204fd306cSNickeau * 7304fd306cSNickeau * sqlite is stored globally 7404fd306cSNickeau * because when we create a new instance, it will open the 7504fd306cSNickeau * sqlite file. 7604fd306cSNickeau * 7704fd306cSNickeau * In a {@link cli_plugin_combo} run, you will run in the error: 7804fd306cSNickeau * `` 7904fd306cSNickeau * failed to open stream: Too many open files 8004fd306cSNickeau * `` 8104fd306cSNickeau * As there is by default a limit of 1024 open files 8204fd306cSNickeau * which means that if there is more than 1024 pages 8304fd306cSNickeau * that you replicate using a new sqlite instance each time, 8404fd306cSNickeau * you fail. 8504fd306cSNickeau * 8604fd306cSNickeau */ 8704fd306cSNickeau $sqlite = $executionContext->getRuntimeObject($sqliteExecutionObjectIdentifier); 8804fd306cSNickeau } catch (ExceptionNotFound $e) { 8904fd306cSNickeau $sqlite = null; 9004fd306cSNickeau } 9104fd306cSNickeau 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."; 11004fd306cSNickeau throw new ExceptionSqliteNotAvailable($sqliteMandatoryMessage); 11137748cd8SNickeau } 112c3437056SNickeau 113c3437056SNickeau $adapter = $sqlitePlugin->getAdapter(); 11437748cd8SNickeau if ($adapter == null) { 11537748cd8SNickeau self::sendMessageAsNotAvailable(); 11637748cd8SNickeau } 11737748cd8SNickeau 11837748cd8SNickeau $adapter->setUseNativeAlter(true); 11937748cd8SNickeau 12004fd306cSNickeau list($databaseName, $databaseDefinitionDir) = self::getDatabaseNameAndDefinitionDirectory($databaseName); 12104fd306cSNickeau $init = $sqlitePlugin->init($databaseName, $databaseDefinitionDir); 12237748cd8SNickeau if (!$init) { 12337748cd8SNickeau $message = "Unable to initialize Sqlite"; 12404fd306cSNickeau 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); 14004fd306cSNickeau $executionContext->setRuntimeObject($sqliteExecutionObjectIdentifier, $sqlite); 141c3437056SNickeau return $sqlite; 142c3437056SNickeau 14337748cd8SNickeau } 144c3437056SNickeau 14504fd306cSNickeau /** 14604fd306cSNickeau * @throws ExceptionSqliteNotAvailable 14704fd306cSNickeau */ 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 } 164*031d4b49Sgerardnico /** 165*031d4b49Sgerardnico * RowId added to have a primary key to identify and delete the row uniquely 166*031d4b49Sgerardnico */ 167*031d4b49Sgerardnico return "select rowid, $columnStatement from $tableName"; 16837748cd8SNickeau 16937748cd8SNickeau } 17037748cd8SNickeau 17137748cd8SNickeau /** 17204fd306cSNickeau * Used in test to delete the database file 17304fd306cSNickeau * @return void 17404fd306cSNickeau * @throws ExceptionFileSystem - if we can delete the databases 17504fd306cSNickeau */ 17604fd306cSNickeau public static function deleteDatabasesFile() 17704fd306cSNickeau { 17804fd306cSNickeau /** 17904fd306cSNickeau * The plugin does not give you the option to 18004fd306cSNickeau * where to create the database file 18104fd306cSNickeau * See {@link \helper_plugin_sqlite_adapter::initdb()} 18204fd306cSNickeau * $this->dbfile = $conf['metadir'].'/'.$dbname.$this->fileextension; 18304fd306cSNickeau * 18404fd306cSNickeau * If error on delete, see {@link self::close()} 18504fd306cSNickeau */ 18604fd306cSNickeau $metadatDirectory = ExecutionContext::getActualOrCreateFromEnv() 18704fd306cSNickeau ->getConfig() 18804fd306cSNickeau ->getMetaDataDirectory(); 18904fd306cSNickeau $fileChildren = FileSystems::getChildrenLeaf($metadatDirectory); 19004fd306cSNickeau foreach ($fileChildren as $child) { 19104fd306cSNickeau try { 19204fd306cSNickeau $extension = $child->getExtension(); 19304fd306cSNickeau } catch (ExceptionNotFound $e) { 19404fd306cSNickeau // ok no extension 19504fd306cSNickeau continue; 19604fd306cSNickeau } 19704fd306cSNickeau if (in_array($extension, ["sqlite", "sqlite3"])) { 19804fd306cSNickeau FileSystems::delete($child); 19904fd306cSNickeau } 20004fd306cSNickeau 20104fd306cSNickeau } 20204fd306cSNickeau } 20304fd306cSNickeau 20404fd306cSNickeau private static function getDatabaseNameAndDefinitionDirectory($databaseName): array 20504fd306cSNickeau { 20604fd306cSNickeau global $conf; 20704fd306cSNickeau 20804fd306cSNickeau if ($databaseName === self::MAIN_DATABASE_NAME) { 20904fd306cSNickeau $oldDbName = '404manager'; 21004fd306cSNickeau $oldDbFile = $conf['metadir'] . "/{$oldDbName}.sqlite"; 21104fd306cSNickeau $oldDbFileSqlite3 = $conf['metadir'] . "/{$oldDbName}.sqlite3"; 21204fd306cSNickeau if (file_exists($oldDbFile) || file_exists($oldDbFileSqlite3)) { 21304fd306cSNickeau $databaseName = $oldDbName; 21404fd306cSNickeau } 21504fd306cSNickeau } 21604fd306cSNickeau 21704fd306cSNickeau $databaseDir = DOKU_PLUGIN . PluginUtility::PLUGIN_BASE_NAME . "/db/$databaseName"; 21804fd306cSNickeau return [$databaseName, $databaseDir]; 21904fd306cSNickeau 22004fd306cSNickeau } 22104fd306cSNickeau 22204fd306cSNickeau /** 22337748cd8SNickeau * Print debug info to the console in order to resolve 22437748cd8SNickeau * RuntimeException: HY000 8 attempt to write a readonly database 22537748cd8SNickeau * https://phpunit.readthedocs.io/en/latest/writing-tests-for-phpunit.html#error-output 22637748cd8SNickeau */ 227c3437056SNickeau public function printDbInfoAtConsole() 22837748cd8SNickeau { 229c3437056SNickeau $dbFile = $this->sqlitePlugin->getAdapter()->getDbFile(); 23037748cd8SNickeau fwrite(STDERR, "Stderr DbFile: " . $dbFile . "\n"); 23137748cd8SNickeau if (file_exists($dbFile)) { 23237748cd8SNickeau fwrite(STDERR, "File does exists\n"); 23337748cd8SNickeau fwrite(STDERR, "Permission " . substr(sprintf('%o', fileperms($dbFile)), -4) . "\n"); 23437748cd8SNickeau } else { 23537748cd8SNickeau fwrite(STDERR, "File does not exist\n"); 23637748cd8SNickeau } 23737748cd8SNickeau 23837748cd8SNickeau global $conf; 23937748cd8SNickeau $metadir = $conf['metadir']; 24037748cd8SNickeau fwrite(STDERR, "MetaDir: " . $metadir . "\n"); 24137748cd8SNickeau $subdir = strpos($dbFile, $metadir) === 0; 24237748cd8SNickeau if ($subdir) { 24337748cd8SNickeau fwrite(STDERR, "Meta is a subdirectory of the db \n"); 24437748cd8SNickeau } else { 24537748cd8SNickeau fwrite(STDERR, "Meta is a not subdirectory of the db \n"); 24637748cd8SNickeau } 24737748cd8SNickeau 24837748cd8SNickeau } 24937748cd8SNickeau 25037748cd8SNickeau /** 25137748cd8SNickeau * Json support 25237748cd8SNickeau */ 253c3437056SNickeau public function supportJson(): bool 25437748cd8SNickeau { 25537748cd8SNickeau 25637748cd8SNickeau 257c3437056SNickeau $res = $this->sqlitePlugin->query("PRAGMA compile_options"); 25837748cd8SNickeau $isJsonEnabled = false; 259c3437056SNickeau foreach ($this->sqlitePlugin->res2arr($res) as $row) { 26037748cd8SNickeau if ($row["compile_option"] === "ENABLE_JSON1") { 26137748cd8SNickeau $isJsonEnabled = true; 26237748cd8SNickeau break; 26337748cd8SNickeau } 26437748cd8SNickeau }; 265c3437056SNickeau $this->sqlitePlugin->res_close($res); 26637748cd8SNickeau return $isJsonEnabled; 26737748cd8SNickeau } 26837748cd8SNickeau 26937748cd8SNickeau 27004fd306cSNickeau /** 27104fd306cSNickeau * @throws ExceptionSqliteNotAvailable 27204fd306cSNickeau */ 273c3437056SNickeau public 274c3437056SNickeau static function sendMessageAsNotAvailable(): void 27537748cd8SNickeau { 27637748cd8SNickeau $sqliteMandatoryMessage = "The Sqlite Php Extension is mandatory. It seems that it's not available on this installation."; 27704fd306cSNickeau throw new ExceptionSqliteNotAvailable($sqliteMandatoryMessage); 27837748cd8SNickeau } 279c3437056SNickeau 280c3437056SNickeau /** 28104fd306cSNickeau * 28204fd306cSNickeau * Old check when there was no {@link ExecutionContext} 28304fd306cSNickeau * to reset the Sqlite variable 28404fd306cSNickeau * TODO: delete ? 285c3437056SNickeau * 286c3437056SNickeau * 287c3437056SNickeau */ 288c3437056SNickeau private function doWeNeedToCreateNewInstance(): bool 289c3437056SNickeau { 290c3437056SNickeau 291c3437056SNickeau global $conf; 292c3437056SNickeau $metaDir = $conf['metadir']; 293c3437056SNickeau 294c3437056SNickeau /** 295c3437056SNickeau * Adapter may be null 296c3437056SNickeau * when the SQLite & PDO SQLite 297c3437056SNickeau * are not installed 298c3437056SNickeau * ie: SQLite & PDO SQLite support missing 299c3437056SNickeau */ 300c3437056SNickeau $adapter = $this->sqlitePlugin->getAdapter(); 301c3437056SNickeau if ($adapter === null) { 302c3437056SNickeau return true; 303c3437056SNickeau } 304c3437056SNickeau 305c3437056SNickeau /** 306c3437056SNickeau * When the database is {@link \helper_plugin_sqlite_adapter::closedb()} 307c3437056SNickeau */ 308c3437056SNickeau if ($adapter->getDb() === null) { 309c3437056SNickeau /** 310c3437056SNickeau * We may also open it again 311c3437056SNickeau * {@link \helper_plugin_sqlite_adapter::opendb()} 312c3437056SNickeau * for now, reinit 313c3437056SNickeau */ 314c3437056SNickeau return true; 315c3437056SNickeau } 316c3437056SNickeau /** 317c3437056SNickeau * In test, we are running in different context (ie different root 318c3437056SNickeau * directory for DokuWiki and therefore different $conf 319c3437056SNickeau * and therefore different metadir where sqlite is stored) 320c3437056SNickeau * Because a sql file may be deleted, we may get: 321c3437056SNickeau * ``` 322c3437056SNickeau * RuntimeException: HY000 8 attempt to write a readonly database: 323c3437056SNickeau * ``` 324c3437056SNickeau * To avoid this error, we check that we are still in the same metadir 325c3437056SNickeau * where the sqlite database is stored. If not, we create a new instance 326c3437056SNickeau */ 327c3437056SNickeau $dbFile = $adapter->getDbFile(); 328c3437056SNickeau if (!file_exists($dbFile)) { 329c3437056SNickeau $this->close(); 330c3437056SNickeau return true; 331c3437056SNickeau } 332c3437056SNickeau // the file is in the meta directory 333c3437056SNickeau if (strpos($dbFile, $metaDir) === 0) { 334c3437056SNickeau // we are still in a class run 335c3437056SNickeau return false; 336c3437056SNickeau } 337c3437056SNickeau $this->close(); 338c3437056SNickeau return true; 339c3437056SNickeau } 340c3437056SNickeau 34104fd306cSNickeau public function close() 342c3437056SNickeau { 343c3437056SNickeau 34404fd306cSNickeau /** 34504fd306cSNickeau * https://www.php.net/manual/en/pdo.connections.php#114822 34604fd306cSNickeau * You put the variable connection on null 34704fd306cSNickeau * the {@link \helper_plugin_sqlite_adapter::closedb() function} do that 34804fd306cSNickeau * 34904fd306cSNickeau * If we don't do that, the file is still locked 35004fd306cSNickeau * by the sqlite process and the clean up process 35104fd306cSNickeau * of dokuwiki test cannot delete it 35204fd306cSNickeau * 35304fd306cSNickeau * ie to avoid 35404fd306cSNickeau * RuntimeException: Unable to delete the file 35504fd306cSNickeau * (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 35604fd306cSNickeau * {@link TestUtils::rdelete} 35704fd306cSNickeau * 35804fd306cSNickeau * Windows sort of handling/ bug explained here 35904fd306cSNickeau * https://bugs.php.net/bug.php?id=78930&edit=3 36004fd306cSNickeau * 36104fd306cSNickeau * Null to close the db explanation and bug 36204fd306cSNickeau * https://bugs.php.net/bug.php?id=62065 36304fd306cSNickeau * 36404fd306cSNickeau */ 36504fd306cSNickeau 36604fd306cSNickeau $this->closeActualRequestIfNotClosed(); 36704fd306cSNickeau 368c3437056SNickeau $adapter = $this->sqlitePlugin->getAdapter(); 369c3437056SNickeau if ($adapter !== null) { 37004fd306cSNickeau 371c3437056SNickeau $adapter->closedb(); 372c3437056SNickeau 37304fd306cSNickeau unset($adapter); 37404fd306cSNickeau 37504fd306cSNickeau gc_collect_cycles(); 376c3437056SNickeau 377c3437056SNickeau } 378c3437056SNickeau 379c3437056SNickeau } 380c3437056SNickeau 381c3437056SNickeau public function getDbName(): string 382c3437056SNickeau { 383c3437056SNickeau return $this->sqlitePlugin->getAdapter()->getName(); 384c3437056SNickeau } 385c3437056SNickeau 386c3437056SNickeau 387c3437056SNickeau public function getSqlitePlugin(): helper_plugin_sqlite 388c3437056SNickeau { 389c3437056SNickeau return $this->sqlitePlugin; 390c3437056SNickeau } 391c3437056SNickeau 392c3437056SNickeau public function createRequest(): SqliteRequest 393c3437056SNickeau { 39404fd306cSNickeau $this->closeActualRequestIfNotClosed(); 39504fd306cSNickeau $this->actualRequest = new SqliteRequest($this); 39604fd306cSNickeau return $this->actualRequest; 397c3437056SNickeau } 398c3437056SNickeau 399c3437056SNickeau public function getVersion() 400c3437056SNickeau { 401c3437056SNickeau if (self::$sqliteVersion === null) { 402c3437056SNickeau $request = $this->createRequest() 403c3437056SNickeau ->setQuery("select sqlite_version()"); 404c3437056SNickeau try { 405c3437056SNickeau self::$sqliteVersion = $request 406c3437056SNickeau ->execute() 407c3437056SNickeau ->getFirstCellValue(); 40804fd306cSNickeau } catch (ExceptionCompile $e) { 409c3437056SNickeau self::$sqliteVersion = "unknown"; 410c3437056SNickeau } finally { 411c3437056SNickeau $request->close(); 412c3437056SNickeau } 413c3437056SNickeau } 414c3437056SNickeau return self::$sqliteVersion; 415c3437056SNickeau } 416c3437056SNickeau 417c3437056SNickeau /** 418c3437056SNickeau * @param string $option 419c3437056SNickeau * @return bool - true if the option is available 420c3437056SNickeau */ 421c3437056SNickeau public function hasOption(string $option): bool 422c3437056SNickeau { 423c3437056SNickeau try { 424c3437056SNickeau $present = $this->createRequest() 42504fd306cSNickeau ->setQueryParametrized("select count(1) from pragma_compile_options() where compile_options = ?", [$option]) 426c3437056SNickeau ->execute() 427c3437056SNickeau ->getFirstCellValueAsInt(); 428c3437056SNickeau return $present === 1; 42904fd306cSNickeau } catch (ExceptionCompile $e) { 430c3437056SNickeau LogUtility::msg("Error while trying to see if the sqlite option is available"); 431c3437056SNickeau return false; 432c3437056SNickeau } 433c3437056SNickeau 434c3437056SNickeau } 43504fd306cSNickeau 43604fd306cSNickeau /** 43704fd306cSNickeau * Internal function that closes the actual request 43804fd306cSNickeau * This is to be able to close all resources even if the developer 43904fd306cSNickeau * forget. 44004fd306cSNickeau * 44104fd306cSNickeau * This is needed to be able to delete the database file. 44204fd306cSNickeau * See {@link self::close()} for more information 44304fd306cSNickeau * 44404fd306cSNickeau * @return void 44504fd306cSNickeau */ 44604fd306cSNickeau private function closeActualRequestIfNotClosed() 44704fd306cSNickeau { 44804fd306cSNickeau if(isset($this->actualRequest)){ 44904fd306cSNickeau $this->actualRequest->close(); 45004fd306cSNickeau unset($this->actualRequest); 45104fd306cSNickeau } 45204fd306cSNickeau } 45337748cd8SNickeau} 454