1<?php /** @noinspection SpellCheckingInspection */ 2 3/** 4 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 5 * 6 * This source code is licensed under the GPL license found in the 7 * COPYING file in the root directory of this source tree. 8 * 9 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 10 * @author ComboStrap <support@combostrap.com> 11 * 12 */ 13 14namespace ComboStrap; 15 16require_once(__DIR__ . '/PluginUtility.php'); 17 18use helper_plugin_sqlite; 19use RuntimeException; 20 21class Sqlite 22{ 23 24 /** @var Sqlite[] $sqlite */ 25 private static $sqlites; 26 27 /** 28 * Principal database 29 * (Backup) 30 */ 31 private const MAIN_DATABASE_NAME = "combo"; 32 /** 33 * Backend Databse 34 * (Log, Pub/Sub,...) 35 */ 36 private const SECONDARY_DB = "combo-secondary"; 37 38 private static $sqliteVersion; 39 40 /** 41 * @var helper_plugin_sqlite 42 */ 43 private $sqlitePlugin; 44 45 /** 46 * Sqlite constructor. 47 * @var helper_plugin_sqlite $sqlitePlugin 48 */ 49 public function __construct(helper_plugin_sqlite $sqlitePlugin) 50 { 51 $this->sqlitePlugin = $sqlitePlugin; 52 } 53 54 55 /** 56 * 57 * @return Sqlite $sqlite 58 */ 59 public static function createOrGetSqlite($databaseName = self::MAIN_DATABASE_NAME): ?Sqlite 60 { 61 62 $sqlite = self::$sqlites[$databaseName]; 63 if ($sqlite !== null) { 64 $res = $sqlite->doWeNeedToCreateNewInstance(); 65 if ($res === false) { 66 return $sqlite; 67 } 68 } 69 70 71 /** 72 * Init 73 * @var helper_plugin_sqlite $sqlitePlugin 74 */ 75 $sqlitePlugin = plugin_load('helper', 'sqlite'); 76 /** 77 * Not enabled / loaded 78 */ 79 if ($sqlitePlugin === null) { 80 81 $sqliteMandatoryMessage = "The Sqlite Plugin is mandatory. Some functionalities of the ComboStrap Plugin may not work."; 82 LogUtility::log2FrontEnd($sqliteMandatoryMessage, LogUtility::LVL_MSG_ERROR); 83 return null; 84 } 85 86 $adapter = $sqlitePlugin->getAdapter(); 87 if ($adapter == null) { 88 self::sendMessageAsNotAvailable(); 89 return null; 90 } 91 92 $adapter->setUseNativeAlter(true); 93 94 global $conf; 95 96 if ($databaseName === self::MAIN_DATABASE_NAME) { 97 $oldDbName = '404manager'; 98 $oldDbFile = $conf['metadir'] . "/{$oldDbName}.sqlite"; 99 $oldDbFileSqlite3 = $conf['metadir'] . "/{$oldDbName}.sqlite3"; 100 if (file_exists($oldDbFile) || file_exists($oldDbFileSqlite3)) { 101 $databaseName = $oldDbName; 102 } 103 } 104 105 $updatedir = DOKU_PLUGIN . PluginUtility::PLUGIN_BASE_NAME . "/db/$databaseName"; 106 $init = $sqlitePlugin->init($databaseName, $updatedir); 107 if (!$init) { 108 # TODO: Message 'SqliteUnableToInitialize' 109 $message = "Unable to initialize Sqlite"; 110 LogUtility::msg($message, MSG_MANAGERS_ONLY); 111 return null; 112 } 113 // regexp implementation 114 // https://stackoverflow.com/questions/5071601/how-do-i-use-regex-in-a-sqlite-query/18484596#18484596 115 $adapter = $sqlitePlugin->getAdapter(); 116 $adapter->create_function('regexp', 117 function ($pattern, $data, $delimiter = '~', $modifiers = 'isuS') { 118 if (isset($pattern, $data) === true) { 119 return (preg_match(sprintf('%1$s%2$s%1$s%3$s', $delimiter, $pattern, $modifiers), $data) > 0); 120 } 121 return null; 122 }, 123 4 124 ); 125 126 $sqlite = new Sqlite($sqlitePlugin); 127 self::$sqlites[$databaseName] = $sqlite; 128 return $sqlite; 129 130 } 131 132 public static function createOrGetBackendSqlite(): ?Sqlite 133 { 134 return self::createOrGetSqlite(self::SECONDARY_DB); 135 } 136 137 public static function createSelectFromTableAndColumns(string $tableName, array $columns = null): string 138 { 139 if ($columns === null) { 140 $columnStatement = "*"; 141 } else { 142 $columnsStatement = []; 143 foreach ($columns as $columnName) { 144 $columnsStatement[] = "$columnName as \"$columnName\""; 145 } 146 $columnStatement = implode(", ", $columnsStatement); 147 } 148 return "select $columnStatement from $tableName"; 149 150 } 151 152 /** 153 * Print debug info to the console in order to resolve 154 * RuntimeException: HY000 8 attempt to write a readonly database 155 * https://phpunit.readthedocs.io/en/latest/writing-tests-for-phpunit.html#error-output 156 */ 157 public function printDbInfoAtConsole() 158 { 159 $dbFile = $this->sqlitePlugin->getAdapter()->getDbFile(); 160 fwrite(STDERR, "Stderr DbFile: " . $dbFile . "\n"); 161 if (file_exists($dbFile)) { 162 fwrite(STDERR, "File does exists\n"); 163 fwrite(STDERR, "Permission " . substr(sprintf('%o', fileperms($dbFile)), -4) . "\n"); 164 } else { 165 fwrite(STDERR, "File does not exist\n"); 166 } 167 168 global $conf; 169 $metadir = $conf['metadir']; 170 fwrite(STDERR, "MetaDir: " . $metadir . "\n"); 171 $subdir = strpos($dbFile, $metadir) === 0; 172 if ($subdir) { 173 fwrite(STDERR, "Meta is a subdirectory of the db \n"); 174 } else { 175 fwrite(STDERR, "Meta is a not subdirectory of the db \n"); 176 } 177 178 } 179 180 /** 181 * Json support 182 */ 183 public function supportJson(): bool 184 { 185 186 187 $res = $this->sqlitePlugin->query("PRAGMA compile_options"); 188 $isJsonEnabled = false; 189 foreach ($this->sqlitePlugin->res2arr($res) as $row) { 190 if ($row["compile_option"] === "ENABLE_JSON1") { 191 $isJsonEnabled = true; 192 break; 193 } 194 }; 195 $this->sqlitePlugin->res_close($res); 196 return $isJsonEnabled; 197 } 198 199 200 public 201 static function sendMessageAsNotAvailable(): void 202 { 203 $sqliteMandatoryMessage = "The Sqlite Php Extension is mandatory. It seems that it's not available on this installation."; 204 LogUtility::log2FrontEnd($sqliteMandatoryMessage, LogUtility::LVL_MSG_ERROR); 205 } 206 207 /** 208 * sqlite is stored in a static variable 209 * because when we run the {@link cli_plugin_combo}, 210 * we will run in the error: 211 * `` 212 * failed to open stream: Too many open files 213 * `` 214 * There is by default a limit of 1024 open files 215 * which means that if there is more than 1024 pages, you fail. 216 * 217 * 218 */ 219 private function doWeNeedToCreateNewInstance(): bool 220 { 221 222 global $conf; 223 $metaDir = $conf['metadir']; 224 225 226 /** 227 * Adapter may be null 228 * when the SQLite & PDO SQLite 229 * are not installed 230 * ie: SQLite & PDO SQLite support missing 231 */ 232 $adapter = $this->sqlitePlugin->getAdapter(); 233 if ($adapter === null) { 234 return true; 235 } 236 237 /** 238 * When the database is {@link \helper_plugin_sqlite_adapter::closedb()} 239 */ 240 if ($adapter->getDb() === null) { 241 /** 242 * We may also open it again 243 * {@link \helper_plugin_sqlite_adapter::opendb()} 244 * for now, reinit 245 */ 246 return true; 247 } 248 /** 249 * In test, we are running in different context (ie different root 250 * directory for DokuWiki and therefore different $conf 251 * and therefore different metadir where sqlite is stored) 252 * Because a sql file may be deleted, we may get: 253 * ``` 254 * RuntimeException: HY000 8 attempt to write a readonly database: 255 * ``` 256 * To avoid this error, we check that we are still in the same metadir 257 * where the sqlite database is stored. If not, we create a new instance 258 */ 259 $dbFile = $adapter->getDbFile(); 260 if (!file_exists($dbFile)) { 261 $this->close(); 262 return true; 263 } 264 // the file is in the meta directory 265 if (strpos($dbFile, $metaDir) === 0) { 266 // we are still in a class run 267 return false; 268 } 269 $this->close(); 270 return true; 271 } 272 273 public 274 function close() 275 { 276 277 $adapter = $this->sqlitePlugin->getAdapter(); 278 if ($adapter !== null) { 279 /** 280 * https://www.php.net/manual/en/pdo.connections.php#114822 281 * You put the connection on null 282 * CloseDb do that 283 */ 284 $adapter->closedb(); 285 286 /** 287 * Delete the file If we can't delete the file 288 * there is a resource still open 289 */ 290 $sqliteFile = $adapter->getDbFile(); 291 if (file_exists($sqliteFile)) { 292 $result = unlink($sqliteFile); 293 if ($result === false) { 294 throw new RuntimeException("Unable to delete the file ($sqliteFile). Did you close all resources ?"); 295 } 296 } 297 298 } 299 /** 300 * Forwhatever reason, closing in php 301 * is putting the variable to null 302 * We do it also in the static variable to be sure 303 */ 304 self::$sqlites[$this->getDbName()] == null; 305 306 307 } 308 309 public function getDbName(): string 310 { 311 return $this->sqlitePlugin->getAdapter()->getName(); 312 } 313 314 public static function closeAll() 315 { 316 317 $sqlites = self::$sqlites; 318 if ($sqlites !== null) { 319 foreach ($sqlites as $sqlite) { 320 $sqlite->close(); 321 } 322 /** 323 * Set it to null 324 */ 325 Sqlite::$sqlites = null; 326 } 327 } 328 329 330 public function getSqlitePlugin(): helper_plugin_sqlite 331 { 332 return $this->sqlitePlugin; 333 } 334 335 public function createRequest(): SqliteRequest 336 { 337 return new SqliteRequest($this); 338 } 339 340 public function getVersion() 341 { 342 if (self::$sqliteVersion === null) { 343 $request = $this->createRequest() 344 ->setQuery("select sqlite_version()"); 345 try { 346 self::$sqliteVersion = $request 347 ->execute() 348 ->getFirstCellValue(); 349 } catch (ExceptionCombo $e) { 350 self::$sqliteVersion = "unknown"; 351 } finally { 352 $request->close(); 353 } 354 } 355 return self::$sqliteVersion; 356 } 357 358 /** 359 * @param string $option 360 * @return bool - true if the option is available 361 */ 362 public function hasOption(string $option): bool 363 { 364 try { 365 $present = $this->createRequest() 366 ->setQueryParametrized("select count(1) from pragma_compile_options() where compile_options = ?", $option) 367 ->execute() 368 ->getFirstCellValueAsInt(); 369 return $present === 1; 370 } catch (ExceptionCombo $e) { 371 LogUtility::msg("Error while trying to see if the sqlite option is available"); 372 return false; 373 } 374 375 } 376} 377