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