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