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 protected $adapter = null; 24 25 /** 26 * @return helper_plugin_sqlite_adapter_pdosqlite|helper_plugin_sqlite_adapter|\helper_plugin_sqlite_adapter_sqlite2|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 __construct() { 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 $this->create_function('PAGEEXISTS', array($this, '_pageexists'), 1); 113 114 return $this->_updatedb($init, $updatedir); 115 } 116 117 /** 118 * Return the current Database Version 119 */ 120 private function _currentDBversion() { 121 $sql = "SELECT val FROM opts WHERE opt = 'dbversion';"; 122 $res = $this->query($sql); 123 if(!$res) return false; 124 $row = $this->res2row($res, 0); 125 return (int) $row['val']; 126 } 127 128 /** 129 * Update the database if needed 130 * 131 * @param bool $init - true if this is a new database to initialize 132 * @param string $updatedir - Database update infos 133 * @return bool 134 */ 135 private function _updatedb($init, $updatedir) { 136 if($init) { 137 138 $current = 0; 139 } else { 140 $current = $this->_currentDBversion(); 141 if(!$current) { 142 msg("SQLite: no DB version found. '".$this->adapter->getDbname()."' DB probably broken.", -1); 143 return false; 144 } 145 } 146 147 // in case of init, add versioning table 148 if($init) { 149 if(!$this->_runupdatefile(dirname(__FILE__).'/db.sql', 0)) { 150 msg("SQLite: '".$this->adapter->getDbname()."' database upgrade failed for version ", -1); 151 return false; 152 } 153 } 154 155 $latest = (int) trim(io_readFile($updatedir.'/latest.version')); 156 157 // all up to date? 158 if($current >= $latest) return true; 159 for($i = $current + 1; $i <= $latest; $i++) { 160 $file = sprintf($updatedir.'/update%04d.sql', $i); 161 if(file_exists($file)) { 162 if(!$this->_runupdatefile($file, $i)) { 163 msg("SQLite: '".$this->adapter->getDbname()."' database upgrade failed for version ".$i, -1); 164 return false; 165 } 166 } else { 167 msg("SQLite: update file $file not found, skipped.", -1); 168 } 169 } 170 return true; 171 } 172 173 /** 174 * Updates the database structure using the given file to 175 * the given version. 176 */ 177 private function _runupdatefile($file, $version) { 178 if(!file_exists($file)) { 179 msg("SQLite: Failed to find DB update file $file"); 180 return false; 181 } 182 $sql = io_readFile($file, false); 183 184 $sql = $this->SQLstring2array($sql); 185 array_unshift($sql, 'BEGIN TRANSACTION'); 186 array_push($sql, "INSERT OR REPLACE INTO opts (val,opt) VALUES ($version,'dbversion')"); 187 array_push($sql, "COMMIT TRANSACTION"); 188 189 if(!$this->doTransaction($sql)) { 190 return false; 191 } 192 return ($version == $this->_currentDBversion()); 193 } 194 195 /** 196 * Callback checks the permissions for the current user 197 * 198 * This function is registered as a SQL function named GETACCESSLEVEL 199 * 200 * @param string $pageid page ID (needs to be resolved and cleaned) 201 * @return int permission level 202 */ 203 public function _getAccessLevel($pageid) { 204 static $aclcache = array(); 205 206 if(isset($aclcache[$pageid])) { 207 return $aclcache[$pageid]; 208 } 209 210 if(isHiddenPage($pageid)) { 211 $acl = AUTH_NONE; 212 } else { 213 $acl = auth_quickaclcheck($pageid); 214 } 215 $aclcache[$pageid] = $acl; 216 return $acl; 217 } 218 219 /** 220 * Wrapper around page_exists() with static caching 221 * 222 * This function is registered as a SQL function named PAGEEXISTS 223 * 224 * @param string $pageid 225 * @return int 0|1 226 */ 227 public function _pageexists($pageid) { 228 static $cache = array(); 229 if(!isset($cache[$pageid])) { 230 $cache[$pageid] = page_exists($pageid); 231 232 } 233 return (int) $cache[$pageid]; 234 } 235 236 /** 237 * Split sql queries on semicolons, unless when semicolons are quoted 238 * 239 * @param string $sql 240 * @return array sql queries 241 */ 242 public function SQLstring2array($sql) { 243 $statements = array(); 244 $len = strlen($sql); 245 246 // Simple state machine to "parse" sql into single statements 247 $in_str = false; 248 $in_com = false; 249 $statement = ''; 250 for($i=0; $i<$len; $i++){ 251 $prev = $i ? $sql{$i-1} : "\n"; 252 $char = $sql{$i}; 253 $next = $sql{$i+1}; 254 255 // in comment? ignore everything until line end 256 if($in_com){ 257 if($char == "\n"){ 258 $in_com = false; 259 } 260 continue; 261 } 262 263 // handle strings 264 if($in_str){ 265 if($char == "'"){ 266 if($next == "'"){ 267 // current char is an escape for the next 268 $statement .= $char . $next; 269 $i++; 270 continue; 271 }else{ 272 // end of string 273 $statement .= $char; 274 $in_str = false; 275 continue; 276 } 277 } 278 // still in string 279 $statement .= $char; 280 continue; 281 } 282 283 // new comment? 284 if($char == '-' && $next == '-' && $prev == "\n"){ 285 $in_com = true; 286 continue; 287 } 288 289 // new string? 290 if($char == "'"){ 291 $in_str = true; 292 $statement .= $char; 293 continue; 294 } 295 296 // the real delimiter 297 if($char == ';'){ 298 $statements[] = trim($statement); 299 $statement = ''; 300 continue; 301 } 302 303 // some standard query stuff 304 $statement .= $char; 305 } 306 if($statement) $statements[] = trim($statement); 307 308 return $statements; 309 } 310 311 /** 312 * @param array $sql queries without terminating semicolon 313 * @param bool $sqlpreparing 314 * @return bool 315 */ 316 public function doTransaction($sql, $sqlpreparing = true) { 317 foreach($sql as $s) { 318 $s = preg_replace('!^\s*--.*$!m', '', $s); 319 $s = trim($s); 320 if(!$s) continue; 321 322 if($sqlpreparing) { 323 $res = $this->query("$s;"); 324 } else { 325 $res = $this->adapter->executeQuery("$s;"); 326 } 327 if($res === false) { 328 //TODO check rollback for sqlite PDO 329 if($this->adapter->getName() == DOKU_EXT_SQLITE) { 330 $this->query('ROLLBACK TRANSACTION'); 331 } else { 332 $err = $this->adapter->getDb()->errorInfo(); 333 msg($err[0].' '.$err[1].' '.$err[2].':<br /><pre>'.hsc($s).'</pre>', -1); 334 } 335 return false; 336 } 337 } 338 return true; 339 } 340 341 /** 342 * Dump db into a file in meta directory 343 * 344 */ 345 public function dumpDatabase($dbname, $from = DOKU_EXT_SQLITE) { 346 global $conf; 347 $adapterDumpDb = null; 348 //connect to desired database 349 if($this->adapter->getName() == $from) { 350 $adapterDumpDb =& $this->adapter; 351 } else { 352 if($from == DOKU_EXT_SQLITE) { 353 //TODO test connecting to sqlite2 database 354 if($this->existsSqlite2()) { 355 require_once(DOKU_PLUGIN.'sqlite/classes/adapter_sqlite2.php'); 356 $adapterDumpDb = new helper_plugin_sqlite_adapter_sqlite2(); 357 } else { 358 msg('PHP Sqlite Extension(needed for sqlite2) not available, database "'.hsc($dbname).'" is not dumped to file.'); 359 return false; 360 } 361 } 362 } 363 if($adapterDumpDb === null) { 364 msg('No adapter loaded'); 365 return false; 366 } 367 $init = false; 368 if(!$adapterDumpDb->initdb($dbname, $init)) { 369 msg('Opening database fails.', -1); 370 return false; 371 } 372 373 $res = $adapterDumpDb->query(array("SELECT name,sql FROM sqlite_master WHERE type='table'")); 374 $tables = $adapterDumpDb->res2arr($res); 375 376 $filename = $conf['metadir'].'/dumpfile_'.$dbname.'.sql'; 377 if($fp = fopen($filename, 'w')) { 378 379 fwrite($fp, 'BEGIN TRANSACTION;'."\n"); 380 381 foreach($tables as $table) { 382 383 fwrite($fp, $table['sql'].";\n"); 384 385 $sql = "SELECT * FROM ".$table['name']; 386 $res = $adapterDumpDb->query(array($sql)); 387 388 while($row = $adapterDumpDb->res_fetch_array($res)) { 389 390 $line = 'INSERT INTO '.$table['name'].' VALUES('; 391 foreach($row as $no_entry => $entry) { 392 if($no_entry !== 0) { 393 $line .= ','; 394 } 395 396 if(is_null($entry)) { 397 $line .= 'NULL'; 398 } elseif(!is_numeric($entry)) { 399 $line .= $adapterDumpDb->quote_string($entry); 400 } else { 401 //TODO depending on locale extra leading zeros are truncated e.g 1.300 (thousand three hunderd)-> 1.3 402 $line .= $entry; 403 } 404 } 405 $line .= ');'."\n"; 406 407 fwrite($fp, $line); 408 } 409 } 410 411 $res = $adapterDumpDb->query(array("SELECT name,sql FROM sqlite_master WHERE type='index'")); 412 $indexes = $adapterDumpDb->res2arr($res); 413 foreach($indexes as $index) { 414 fwrite($fp, $index['sql'].";\n"); 415 } 416 417 fwrite($fp, 'COMMIT;'."\n"); 418 419 fclose($fp); 420 return $filename; 421 } else { 422 msg('Dumping "'.hsc($dbname).'" has failed. Could not open '.$filename); 423 return false; 424 } 425 } 426 427 /** 428 * Read $dumpfile and try to add it to database. 429 * A existing database is backuped first as e.g. dbname.copy2.sqlite3 430 * 431 * @param string $dbname 432 * @param string $dumpfile 433 * @return bool true on succes 434 */ 435 public function fillDatabaseFromDump($dbname, $dumpfile) { 436 global $conf; 437 //backup existing stuff 438 $dbf = $conf['metadir'].'/'.$dbname; 439 $dbext = $this->adapter->getFileextension(); 440 $dbfile = $dbf.$dbext; 441 if(@file_exists($dbfile)) { 442 443 $i = 0; 444 $backupdbfile = $dbfile; 445 do { 446 $i++; 447 $backupdbfile = $dbf.".copy$i".$dbext; 448 } while(@file_exists($backupdbfile)); 449 450 io_rename($dbfile, $backupdbfile); 451 } 452 453 $init = false; 454 if(!$this->adapter->initdb($dbname, $init, $sqliteupgrade = true)) { 455 msg('Initialize db fails'); 456 return false; 457 } 458 459 $sql = io_readFile($dumpfile, false); 460 $sql = $this->SQLstring2array($sql); 461 462 //skip preparing, because it interprets question marks as placeholders. 463 return $this->doTransaction($sql, $sqlpreparing = false); 464 } 465 466 /** 467 * Registers a User Defined Function for use in SQL statements 468 */ 469 public function create_function($function_name, $callback, $num_args) { 470 $this->adapter->create_function($function_name, $callback, $num_args); 471 } 472 473 /** 474 * Convenience function to run an INSERT OR REPLACE operation 475 * 476 * The function takes a key-value array with the column names in the key and the actual value in the value, 477 * build the appropriate query and executes it. 478 * 479 * @param string $table the table the entry should be saved to (will not be escaped) 480 * @param array $entry A simple key-value pair array (only values will be escaped) 481 * @return bool|SQLiteResult 482 */ 483 public function storeEntry($table, $entry) { 484 $keys = join(',', array_keys($entry)); 485 $vals = join(',', array_fill(0,count($entry),'?')); 486 487 $sql = "INSERT INTO $table ($keys) VALUES ($vals)"; 488 return $this->query($sql, array_values($entry)); 489 } 490 491 492 /** 493 * Execute a query with the given parameters. 494 * 495 * Takes care of escaping 496 * 497 * 498 * @param string ...$args - the arguments of query(), the first is the sql and others are values 499 * @return bool|\SQLiteResult 500 */ 501 public function query() { 502 // get function arguments 503 $args = func_get_args(); 504 505 return $this->adapter->query($args); 506 } 507 508 /** 509 * Join the given values and quote them for SQL insertion 510 */ 511 public function quote_and_join($vals, $sep = ',') { 512 return $this->adapter->quote_and_join($vals, $sep); 513 } 514 515 /** 516 * Run sqlite_escape_string() on the given string and surround it 517 * with quotes 518 */ 519 public function quote_string($string) { 520 return $this->adapter->quote_string($string); 521 } 522 523 /** 524 * Escape string for sql 525 */ 526 public function escape_string($str) { 527 return $this->adapter->escape_string($str); 528 } 529 530 /** 531 * Closes the result set (and it's cursors) 532 * 533 * If you're doing SELECT queries inside a TRANSACTION, be sure to call this 534 * function on all your results sets, before COMMITing the transaction. 535 * 536 * Also required when not all rows of a result are fetched 537 * 538 * @param $res 539 * @return bool 540 */ 541 public function res_close($res){ 542 return $this->adapter->res_close($res); 543 } 544 545 /** 546 * Returns a complete result set as array 547 */ 548 public function res2arr($res, $assoc = true) { 549 return $this->adapter->res2arr($res, $assoc); 550 } 551 552 /** 553 * Return the wanted row from a given result set as 554 * associative array 555 */ 556 public function res2row($res, $rownum = 0) { 557 return $this->adapter->res2row($res, $rownum); 558 } 559 560 /** 561 * Return the first value from the next row. 562 */ 563 public function res2single($res) { 564 return $this->adapter->res2single($res); 565 } 566 567 /** 568 * fetch the next row as zero indexed array 569 */ 570 public function res_fetch_array($res) { 571 return $this->adapter->res_fetch_array($res); 572 } 573 574 /** 575 * fetch the next row as assocative array 576 */ 577 public function res_fetch_assoc($res) { 578 return $this->adapter->res_fetch_assoc($res); 579 } 580 581 /** 582 * Count the number of records in result 583 * 584 * This function is really inperformant in PDO and should be avoided! 585 */ 586 public function res2count($res) { 587 return $this->adapter->res2count($res); 588 } 589 590 /** 591 * Count the number of records changed last time 592 */ 593 public function countChanges($res) { 594 return $this->adapter->countChanges($res); 595 } 596 597} 598