1<?php 2/** 3 * DokuWiki Plugin sqlite (Helper Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Andreas Gohr <gohr@cosmocode.de> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12if(!defined('DOKU_EXT_SQLITE')) define('DOKU_EXT_SQLITE', 'sqlite'); 13if(!defined('DOKU_EXT_PDO')) define('DOKU_EXT_PDO', 'pdo'); 14if(!defined('DOKU_EXT_NULL')) define('DOKU_EXT_NULL', 'null'); 15 16require_once(DOKU_PLUGIN.'sqlite/classes/adapter.php'); 17 18/** 19 * Class helper_plugin_sqlite 20 */ 21class helper_plugin_sqlite extends DokuWiki_Plugin { 22 /** @var helper_plugin_sqlite_adapter_pdosqlite|helper_plugin_sqlite_adapter|\helper_plugin_sqlite_adapter_sqlite2|null */ 23 var $adapter = null; 24 25 /** 26 * @return helper_plugin_sqlite_adapter|null 27 */ 28 public function getAdapter() { 29 return $this->adapter; 30 } 31 32 /** 33 * Keep separate instances for every call to keep database connections 34 */ 35 public function isSingleton() { 36 return false; 37 } 38 39 /** 40 * constructor 41 */ 42 public function helper_plugin_sqlite() { 43 44 if(!$this->adapter) { 45 if($this->existsPDOSqlite() && empty($_ENV['SQLITE_SKIP_PDO'])) { 46 require_once(DOKU_PLUGIN.'sqlite/classes/adapter_pdosqlite.php'); 47 $this->adapter = new helper_plugin_sqlite_adapter_pdosqlite(); 48 } 49 } 50 51 if(!$this->adapter) { 52 if($this->existsSqlite2()) { 53 require_once(DOKU_PLUGIN.'sqlite/classes/adapter_sqlite2.php'); 54 $this->adapter = new helper_plugin_sqlite_adapter_sqlite2(); 55 } 56 } 57 58 if(!$this->adapter) { 59 msg('SQLite & PDO SQLite support missing in this PHP install - plugin will not work', -1); 60 } 61 } 62 63 /** 64 * check availabilty of PHPs sqlite extension (for sqlite2 support) 65 */ 66 public function existsSqlite2() { 67 if(!extension_loaded('sqlite')) { 68 $prefix = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_' : ''; 69 if(function_exists('dl')) @dl($prefix.'sqlite.'.PHP_SHLIB_SUFFIX); 70 } 71 72 return function_exists('sqlite_open'); 73 } 74 75 /** 76 * check availabilty of PHP PDO sqlite3 77 */ 78 public function existsPDOSqlite() { 79 if(!extension_loaded('pdo_sqlite')) { 80 $prefix = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_' : ''; 81 if(function_exists('dl')) @dl($prefix.'pdo_sqlite.'.PHP_SHLIB_SUFFIX); 82 } 83 84 if(class_exists('pdo')) { 85 foreach(PDO::getAvailableDrivers() as $driver) { 86 if($driver == 'sqlite') { 87 return true; 88 } 89 } 90 } 91 return false; 92 } 93 94 /** 95 * Initializes and opens the database 96 * 97 * Needs to be called right after loading this helper plugin 98 * 99 * @param string $dbname 100 * @param string $updatedir - Database update infos 101 * @return bool 102 */ 103 public function init($dbname, $updatedir) { 104 $init = null; // set by initdb() 105 if( !$this->adapter or !$this->adapter->initdb($dbname, $init) ){ 106 require_once(DOKU_PLUGIN.'sqlite/classes/adapter_null.php'); 107 $this->adapter = new helper_plugin_sqlite_adapter_null(); 108 return false; 109 } 110 111 $this->create_function('GETACCESSLEVEL', array($this, '_getAccessLevel'), 1); 112 113 return $this->_updatedb($init, $updatedir); 114 } 115 116 /** 117 * Return the current Database Version 118 */ 119 private function _currentDBversion() { 120 $sql = "SELECT val FROM opts WHERE opt = 'dbversion';"; 121 $res = $this->query($sql); 122 if(!$res) return false; 123 $row = $this->res2row($res, 0); 124 return (int) $row['val']; 125 } 126 127 /** 128 * Update the database if needed 129 * 130 * @param bool $init - true if this is a new database to initialize 131 * @param string $updatedir - Database update infos 132 * @return bool 133 */ 134 private function _updatedb($init, $updatedir) { 135 if($init) { 136 137 $current = 0; 138 } else { 139 $current = $this->_currentDBversion(); 140 if(!$current) { 141 msg("SQLite: no DB version found. '".$this->adapter->getDbname()."' DB probably broken.", -1); 142 return false; 143 } 144 } 145 146 // in case of init, add versioning table 147 if($init) { 148 if(!$this->_runupdatefile(dirname(__FILE__).'/db.sql', 0)) { 149 msg("SQLite: '".$this->adapter->getDbname()."' database upgrade failed for version ", -1); 150 return false; 151 } 152 } 153 154 $latest = (int) trim(io_readFile($updatedir.'/latest.version')); 155 156 // all up to date? 157 if($current >= $latest) return true; 158 for($i = $current + 1; $i <= $latest; $i++) { 159 $file = sprintf($updatedir.'/update%04d.sql', $i); 160 if(file_exists($file)) { 161 if(!$this->_runupdatefile($file, $i)) { 162 msg("SQLite: '".$this->adapter->getDbname()."' database upgrade failed for version ".$i, -1); 163 return false; 164 } 165 } 166 } 167 return true; 168 } 169 170 /** 171 * Updates the database structure using the given file to 172 * the given version. 173 */ 174 private function _runupdatefile($file, $version) { 175 $sql = io_readFile($file, false); 176 177 $sql = $this->SQLstring2array($sql); 178 array_unshift($sql, 'BEGIN TRANSACTION'); 179 array_push($sql, "INSERT OR REPLACE INTO opts (val,opt) VALUES ($version,'dbversion')"); 180 array_push($sql, "COMMIT TRANSACTION"); 181 182 if(!$this->doTransaction($sql)) { 183 return false; 184 } 185 return ($version == $this->_currentDBversion()); 186 } 187 188 /** 189 * Callback checks the permissions for the current user 190 * 191 * This function is registered as a SQL function named GETACCESSLEVEL 192 * 193 * @param string $pageid page ID (needs to be resolved and cleaned) 194 * @return int permission level 195 */ 196 public function _getAccessLevel($pageid) { 197 static $aclcache = array(); 198 199 if(isset($aclcache[$pageid])) { 200 return $aclcache[$pageid]; 201 } 202 203 if(isHiddenPage($pageid)) { 204 $acl = AUTH_NONE; 205 } else { 206 $acl = auth_quickaclcheck($pageid); 207 } 208 $aclcache[$pageid] = $acl; 209 return $acl; 210 } 211 212 /** 213 * Split sql queries on semicolons, unless when semicolons are quoted 214 * 215 * @param string $sql 216 * @return array sql queries 217 */ 218 public function SQLstring2array($sql) { 219 $statements = array(); 220 $len = strlen($sql); 221 222 // Simple state machine to "parse" sql into single statements 223 $in_str = false; 224 $in_com = false; 225 $statement = ''; 226 for($i=0; $i<$len; $i++){ 227 $prev = $i ? $sql{$i-1} : "\n"; 228 $char = $sql{$i}; 229 $next = $sql{$i+1}; 230 231 // in comment? ignore everything until line end 232 if($in_com){ 233 if($char == "\n"){ 234 $in_com = false; 235 } 236 continue; 237 } 238 239 // handle strings 240 if($in_str){ 241 if($char == "'"){ 242 if($next == "'"){ 243 // current char is an escape for the next 244 $statement .= $char . $next; 245 $i++; 246 continue; 247 }else{ 248 // end of string 249 $statement .= $char; 250 $in_str = false; 251 continue; 252 } 253 } 254 // still in string 255 $statement .= $char; 256 continue; 257 } 258 259 // new comment? 260 if($char == '-' && $next == '-' && $prev == "\n"){ 261 $in_com = true; 262 continue; 263 } 264 265 // new string? 266 if($char == "'"){ 267 $in_str = true; 268 $statement .= $char; 269 continue; 270 } 271 272 // the real delimiter 273 if($char == ';'){ 274 $statements[] = trim($statement); 275 $statement = ''; 276 continue; 277 } 278 279 // some standard query stuff 280 $statement .= $char; 281 } 282 if($statement) $statements[] = trim($statement); 283 284 return $statements; 285 } 286 287 /** 288 * @param array $sql queries without terminating semicolon 289 * @param bool $sqlpreparing 290 * @return bool 291 */ 292 public function doTransaction($sql, $sqlpreparing = true) { 293 foreach($sql as $s) { 294 $s = preg_replace('!^\s*--.*$!m', '', $s); 295 $s = trim($s); 296 if(!$s) continue; 297 298 if($sqlpreparing) { 299 $res = $this->query("$s;"); 300 } else { 301 $res = $this->adapter->executeQuery("$s;"); 302 } 303 if($res === false) { 304 //TODO check rollback for sqlite PDO 305 if($this->adapter->getName() == DOKU_EXT_SQLITE) { 306 $this->query('ROLLBACK TRANSACTION'); 307 } 308 return false; 309 } 310 } 311 return true; 312 } 313 314 /** 315 * Dump db into a file in meta directory 316 * 317 */ 318 public function dumpDatabase($dbname, $from = DOKU_EXT_SQLITE) { 319 global $conf; 320 $adapterDumpDb = null; 321 //connect to desired database 322 if($this->adapter->getName() == $from) { 323 $adapterDumpDb =& $this->adapter; 324 } else { 325 if($from == DOKU_EXT_SQLITE) { 326 //TODO test connecting to sqlite2 database 327 if($this->existsSqlite2()) { 328 require_once(DOKU_PLUGIN.'sqlite/classes/adapter_sqlite2.php'); 329 $adapterDumpDb = new helper_plugin_sqlite_adapter_sqlite2(); 330 } else { 331 msg('PHP Sqlite Extension(needed for sqlite2) not available, database "'.hsc($dbname).'" is not dumped to file.'); 332 return false; 333 } 334 } 335 } 336 if($adapterDumpDb === null) { 337 msg('No adapter loaded'); 338 return false; 339 } 340 $init = false; 341 if(!$adapterDumpDb->initdb($dbname, $init)) { 342 msg('Opening database fails.', -1); 343 return false; 344 } 345 346 $res = $adapterDumpDb->query(array("SELECT name,sql FROM sqlite_master WHERE type='table'")); 347 $tables = $adapterDumpDb->res2arr($res); 348 349 $filename = $conf['metadir'].'/dumpfile_'.$dbname.'.sql'; 350 if($fp = fopen($filename, 'w')) { 351 352 fwrite($fp, 'BEGIN TRANSACTION;'."\n"); 353 354 foreach($tables as $table) { 355 356 fwrite($fp, $table['sql'].";\n"); 357 358 $sql = "SELECT * FROM ".$table['name']; 359 $res = $adapterDumpDb->query(array($sql)); 360 361 while($row = $adapterDumpDb->res_fetch_array($res)) { 362 363 $line = 'INSERT INTO '.$table['name'].' VALUES('; 364 foreach($row as $no_entry => $entry) { 365 if($no_entry !== 0) { 366 $line .= ','; 367 } 368 369 if(is_null($entry)) { 370 $line .= 'NULL'; 371 } elseif(!is_numeric($entry)) { 372 $line .= $adapterDumpDb->quote_string($entry); 373 } else { 374 //TODO depending on locale extra leading zeros are truncated e.g 1.300 (thousand three hunderd)-> 1.3 375 $line .= $entry; 376 } 377 } 378 $line .= ');'."\n"; 379 380 fwrite($fp, $line); 381 } 382 } 383 384 $res = $adapterDumpDb->query(array("SELECT name,sql FROM sqlite_master WHERE type='index'")); 385 $indexes = $adapterDumpDb->res2arr($res); 386 foreach($indexes as $index) { 387 fwrite($fp, $index['sql'].";\n"); 388 } 389 390 fwrite($fp, 'COMMIT;'."\n"); 391 392 fclose($fp); 393 return $filename; 394 } else { 395 msg('Dumping "'.hsc($dbname).'" has failed. Could not open '.$filename); 396 return false; 397 } 398 } 399 400 /** 401 * Read $dumpfile and try to add it to database. 402 * A existing database is backuped first as e.g. dbname.copy2.sqlite3 403 * 404 * @param string $dbname 405 * @param string $dumpfile 406 * @return bool true on succes 407 */ 408 public function fillDatabaseFromDump($dbname, $dumpfile) { 409 global $conf; 410 //backup existing stuff 411 $dbf = $conf['metadir'].'/'.$dbname; 412 $dbext = $this->adapter->getFileextension(); 413 $dbfile = $dbf.$dbext; 414 if(@file_exists($dbfile)) { 415 416 $i = 0; 417 $backupdbfile = $dbfile; 418 do { 419 $i++; 420 $backupdbfile = $dbf.".copy$i".$dbext; 421 } while(@file_exists($backupdbfile)); 422 423 io_rename($dbfile, $backupdbfile); 424 } 425 426 $init = false; 427 if(!$this->adapter->initdb($dbname, $init, $sqliteupgrade = true)) { 428 msg('Initialize db fails'); 429 return false; 430 } 431 432 $sql = io_readFile($dumpfile, false); 433 $sql = $this->SQLstring2array($sql); 434 435 //skip preparing, because it interprets question marks as placeholders. 436 return $this->doTransaction($sql, $sqlpreparing = false); 437 } 438 439 /** 440 * Registers a User Defined Function for use in SQL statements 441 */ 442 public function create_function($function_name, $callback, $num_args) { 443 $this->adapter->create_function($function_name, $callback, $num_args); 444 } 445 446 /** 447 * Execute a query with the given parameters. 448 * 449 * Takes care of escaping 450 * 451 * 452 * @param string ...$args - the arguments of query(), the first is the sql and others are values 453 * @return bool|\SQLiteResult 454 */ 455 public function query() { 456 // get function arguments 457 $args = func_get_args(); 458 459 return $this->adapter->query($args); 460 } 461 462 /** 463 * Join the given values and quote them for SQL insertion 464 */ 465 public function quote_and_join($vals, $sep = ',') { 466 return $this->adapter->quote_and_join($vals, $sep); 467 } 468 469 /** 470 * Run sqlite_escape_string() on the given string and surround it 471 * with quotes 472 */ 473 public function quote_string($string) { 474 return $this->adapter->quote_string($string); 475 } 476 477 /** 478 * Escape string for sql 479 */ 480 public function escape_string($str) { 481 return $this->adapter->escape_string($str); 482 } 483 484 /** 485 * Closes the result set (and it's cursors) 486 * 487 * If you're doing SELECT queries inside a TRANSACTION, be sure to call this 488 * function on all your results sets, before COMMITing the transaction. 489 * 490 * Also required when not all rows of a result are fetched 491 * 492 * @param $res 493 * @return bool 494 */ 495 public function res_close($res){ 496 return $this->adapter->res_close($res); 497 } 498 499 /** 500 * Returns a complete result set as array 501 */ 502 public function res2arr($res, $assoc = true) { 503 return $this->adapter->res2arr($res, $assoc); 504 } 505 506 /** 507 * Return the wanted row from a given result set as 508 * associative array 509 */ 510 public function res2row($res, $rownum = 0) { 511 return $this->adapter->res2row($res, $rownum); 512 } 513 514 /** 515 * Return the first value from the next row. 516 */ 517 public function res2single($res) { 518 return $this->adapter->res2single($res); 519 } 520 521 /** 522 * fetch the next row as zero indexed array 523 */ 524 public function res_fetch_array($res) { 525 return $this->adapter->res_fetch_array($res); 526 } 527 528 /** 529 * fetch the next row as assocative array 530 */ 531 public function res_fetch_assoc($res) { 532 return $this->adapter->res_fetch_assoc($res); 533 } 534 535 /** 536 * Count the number of records in result 537 * 538 * This function is really inperformant in PDO and should be avoided! 539 */ 540 public function res2count($res) { 541 return $this->adapter->res2count($res); 542 } 543 544 /** 545 * Count the number of records changed last time 546 */ 547 public function countChanges($res) { 548 return $this->adapter->countChanges($res); 549 } 550 551} 552