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