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 return preg_split("/;(?=([^']*'[^']*')*[^']*$)/", $sql); 191 } 192 193 /** 194 * @param array $sql queries without terminating semicolon 195 * @param bool $sqlpreparing 196 * @return bool 197 */ 198 public function doTransaction($sql, $sqlpreparing = true) { 199 foreach($sql as $s) { 200 $s = preg_replace('!^\s*--.*$!m', '', $s); 201 $s = trim($s); 202 if(!$s) continue; 203 204 if($sqlpreparing) { 205 $res = $this->query("$s;"); 206 } else { 207 $res = $this->adapter->executeQuery("$s;"); 208 } 209 if($res === false) { 210 //TODO check rollback for sqlite PDO 211 if($this->adapter->getName() == DOKU_EXT_SQLITE) { 212 $this->query('ROLLBACK TRANSACTION'); 213 } 214 return false; 215 } 216 } 217 return true; 218 } 219 220 /** 221 * Dump db into a file in meta directory 222 * 223 */ 224 public function dumpDatabase($dbname, $from = DOKU_EXT_SQLITE) { 225 global $conf; 226 $adapterDumpDb = null; 227 //connect to desired database 228 if($this->adapter->getName() == $from) { 229 $adapterDumpDb =& $this->adapter; 230 } else { 231 if($from == DOKU_EXT_SQLITE) { 232 //TODO test connecting to sqlite2 database 233 if($this->existsSqlite2()) { 234 require_once(DOKU_PLUGIN.'sqlite/classes/adapter_sqlite2.php'); 235 $adapterDumpDb = new helper_plugin_sqlite_adapter_sqlite2(); 236 } else { 237 msg('PHP Sqlite Extension(needed for sqlite2) not available, database "'.hsc($dbname).'" is not dumped to file.'); 238 return false; 239 } 240 } 241 } 242 if($adapterDumpDb === null) { 243 msg('No adapter loaded'); 244 return false; 245 } 246 $init = false; 247 if(!$adapterDumpDb->initdb($dbname, $init)) { 248 msg('Opening database fails.', -1); 249 return false; 250 } 251 252 $res = $adapterDumpDb->query(array("SELECT name,sql FROM sqlite_master WHERE type='table'")); 253 $tables = $adapterDumpDb->res2arr($res); 254 255 $filename = $conf['metadir'].'/dumpfile_'.$dbname.'.sql'; 256 if($fp = fopen($filename, 'w')) { 257 258 fwrite($fp, 'BEGIN TRANSACTION;'."\n"); 259 260 foreach($tables as $table) { 261 262 fwrite($fp, $table['sql'].";\n"); 263 264 $sql = "SELECT * FROM ".$table['name']; 265 $res = $adapterDumpDb->query(array($sql)); 266 267 while($row = $adapterDumpDb->res_fetch_array($res)) { 268 269 $line = 'INSERT INTO '.$table['name'].' VALUES('; 270 foreach($row as $no_entry => $entry) { 271 if($no_entry !== 0) { 272 $line .= ','; 273 } 274 275 if(is_null($entry)) { 276 $line .= 'NULL'; 277 } elseif(!is_numeric($entry)) { 278 $line .= $adapterDumpDb->quote_string($entry); 279 } else { 280 //TODO depending on locale extra leading zeros are truncated e.g 1.300 (thousand three hunderd)-> 1.3 281 $line .= $entry; 282 } 283 } 284 $line .= ');'."\n"; 285 286 fwrite($fp, $line); 287 } 288 } 289 290 $res = $adapterDumpDb->query(array("SELECT name,sql FROM sqlite_master WHERE type='index'")); 291 $indexes = $adapterDumpDb->res2arr($res); 292 foreach($indexes as $index) { 293 fwrite($fp, $index['sql'].";\n"); 294 } 295 296 fwrite($fp, 'COMMIT;'."\n"); 297 298 fclose($fp); 299 return $filename; 300 } else { 301 msg('Dumping "'.hsc($dbname).'" has failed. Could not open '.$filename); 302 return false; 303 } 304 } 305 306 /** 307 * Read $dumpfile and try to add it to database. 308 * A existing database is backuped first as e.g. dbname.copy2.sqlite3 309 * 310 * @param string $dbname 311 * @param string $dumpfile 312 * @return bool true on succes 313 */ 314 public function fillDatabaseFromDump($dbname, $dumpfile) { 315 global $conf; 316 //backup existing stuff 317 $dbf = $conf['metadir'].'/'.$dbname; 318 $dbext = $this->adapter->getFileextension(); 319 $dbfile = $dbf.$dbext; 320 if(@file_exists($dbfile)) { 321 322 $i = 0; 323 $backupdbfile = $dbfile; 324 do { 325 $i++; 326 $backupdbfile = $dbf.".copy$i".$dbext; 327 } while(@file_exists($backupdbfile)); 328 329 io_rename($dbfile, $backupdbfile); 330 } 331 332 $init = false; 333 if(!$this->adapter->initdb($dbname, $init, $sqliteupgrade = true)) { 334 msg('Initialize db fails'); 335 return false; 336 } 337 338 $sql = io_readFile($dumpfile, false); 339 $sql = $this->SQLstring2array($sql); 340 341 //skip preparing, because it interprets question marks as placeholders. 342 return $this->doTransaction($sql, $sqlpreparing = false); 343 } 344 345 /** 346 * Registers a User Defined Function for use in SQL statements 347 */ 348 public function create_function($function_name, $callback, $num_args) { 349 $this->adapter->create_function($function_name, $callback, $num_args); 350 } 351 352 /** 353 * Execute a query with the given parameters. 354 * 355 * Takes care of escaping 356 * 357 * @internal param string $sql - the statement 358 * @internal param $arguments ... 359 * @return bool|\SQLiteResult 360 */ 361 public function query() { 362 // get function arguments 363 $args = func_get_args(); 364 365 return $this->adapter->query($args); 366 } 367 368 /** 369 * Join the given values and quote them for SQL insertion 370 */ 371 public function quote_and_join($vals, $sep = ',') { 372 return $this->adapter->quote_and_join($vals, $sep); 373 } 374 375 /** 376 * Run sqlite_escape_string() on the given string and surround it 377 * with quotes 378 */ 379 public function quote_string($string) { 380 return $this->adapter->quote_string($string); 381 } 382 383 /** 384 * Escape string for sql 385 */ 386 public function escape_string($str) { 387 return $this->adapter->escape_string($str); 388 } 389 390 /** 391 * Returns a complete result set as array 392 */ 393 public function res2arr($res, $assoc = true) { 394 return $this->adapter->res2arr($res, $assoc); 395 } 396 397 /** 398 * Return the wanted row from a given result set as 399 * associative array 400 */ 401 public function res2row($res, $rownum = 0) { 402 return $this->adapter->res2row($res, $rownum); 403 } 404 405 /** 406 * Return the first value from the first row. 407 */ 408 public function res2single($res) { 409 return $this->adapter->res2single($res); 410 } 411 412 /** 413 * fetch the next row as zero indexed array 414 */ 415 public function res_fetch_array($res) { 416 return $this->adapter->res_fetch_array($res); 417 } 418 419 /** 420 * fetch the next row as assocative array 421 */ 422 public function res_fetch_assoc($res) { 423 return $this->adapter->res_fetch_assoc($res); 424 } 425 426 /** 427 * Count the number of records in result 428 * 429 * This function is really inperformant in PDO and should be avoided! 430 */ 431 public function res2count($res) { 432 return $this->adapter->res2count($res); 433 } 434 435 /** 436 * Count the number of records changed last time 437 */ 438 public function countChanges($db, $res) { 439 return $this->adapter->countChanges($db, $res); 440 } 441 442} 443