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