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 16class helper_plugin_sqlite extends DokuWiki_Plugin { 17 var $db = null; 18 var $dbname = ''; 19 20 function getInfo() { 21 return confToHash(dirname(__FILE__).'plugin.info.txt'); 22 } 23 24 /** 25 * constructor 26 */ 27 function helper_plugin_sqlite(){ 28 if (!extension_loaded('sqlite')) { 29 $prefix = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_' : ''; 30 if(function_exists('dl')) @dl($prefix . 'sqlite.' . PHP_SHLIB_SUFFIX); 31 } 32 33 if(!function_exists('sqlite_open')){ 34 msg('SQLite support missing in this PHP install - plugin will not work',-1); 35 } 36 } 37 38 /** 39 * Initializes and opens the database 40 * 41 * Needs to be called right after loading this helper plugin 42 */ 43 function init($dbname,$updatedir){ 44 global $conf; 45 46 // check for already open DB 47 if($this->db){ 48 if($this->dbname == $dbname){ 49 // db already open 50 return true; 51 } 52 // close other db 53 sqlite_close($this->db); 54 $this->db = null; 55 $this->dbname = ''; 56 } 57 58 $this->dbname = $dbname; 59 $dbfile = $conf['metadir'].'/'.$dbname.'.sqlite'; 60 $init = (!@file_exists($dbfile) || ((int) @filesize($dbfile)) < 3); 61 62 $error=''; 63 $this->db = sqlite_open($dbfile, 0666, $error); 64 if(!$this->db){ 65 msg("SQLite: failed to open SQLite ".$this->dbname." database ($error)",-1); 66 return false; 67 } 68 69 // register our custom aggregate function 70 sqlite_create_aggregate($this->db,'group_concat', 71 array($this,'_sqlite_group_concat_step'), 72 array($this,'_sqlite_group_concat_finalize'), 2); 73 74 $this->_updatedb($init,$updatedir); 75 return true; 76 } 77 78 /** 79 * Return the current Database Version 80 */ 81 function _currentDBversion(){ 82 $sql = "SELECT val FROM opts WHERE opt = 'dbversion';"; 83 $res = $this->query($sql); 84 if(!$res) return false; 85 $row = $this->res2row($res,0); 86 return (int) $row['val']; 87 } 88 /** 89 * Update the database if needed 90 * 91 * @param bool $init - true if this is a new database to initialize 92 * @param string $updatedir - Database update infos 93 */ 94 function _updatedb($init,$updatedir){ 95 if($init){ 96 $current = 0; 97 }else{ 98 $current = $this->_currentDBversion(); 99 if(!$current){ 100 msg('SQLite: no DB version found. '.$this->dbname.' DB probably broken.',-1); 101 return false; 102 } 103 } 104 105 // in case of init, add versioning table 106 if($init){ 107 if(!$this->_runupdatefile(dirname(__FILE__).'/db.sql',0)){ 108 msg('SQLite: '.$this->dbname.' database upgrade failed for version '.$i, -1); 109 return false; 110 } 111 } 112 113 $latest = (int) trim(io_readFile($updatedir.'/latest.version')); 114 115 // all up to date? 116 if($current >= $latest) return true; 117 for($i=$current+1; $i<=$latest; $i++){ 118 $file = sprintf($updatedir.'/update%04d.sql',$i); 119 if(file_exists($file)){ 120 if(!$this->_runupdatefile($file,$i)){ 121 msg('SQLite: '.$this->dbname.' database upgrade failed for version '.$i, -1); 122 123 124 return false; 125 } 126 } 127 } 128 return true; 129 } 130 131 /** 132 * Updates the database structure using the given file to 133 * the given version. 134 */ 135 function _runupdatefile($file,$version){ 136 $sql = io_readFile($file,false); 137 138 $sql = explode(";",$sql); 139 array_unshift($sql,'BEGIN TRANSACTION'); 140 array_push($sql,"INSERT OR REPLACE INTO opts (val,opt) VALUES ($version,'dbversion')"); 141 array_push($sql,"COMMIT TRANSACTION"); 142 143 foreach($sql as $s){ 144 $s = preg_replace('!^\s*--.*$!m', '', $s); 145 $s = trim($s); 146 if(!$s) continue; 147 148 149 $res = $this->query("$s;"); 150 if ($res === false) { 151 sqlite_query($this->db, 'ROLLBACK TRANSACTION'); 152 return false; 153 } 154 } 155 156 return ($version == $this->_currentDBversion()); 157 } 158 159 /** 160 * Emulate ALTER TABLE 161 * 162 * The ALTER TABLE syntax is parsed and then emulated using a 163 * temporary table 164 * 165 * @author <jon@jenseng.com> 166 * @link http://code.jenseng.com/db/ 167 */ 168 function _altertable($table,$alterdefs){ 169 $result = $this->query("SELECT sql,name,type 170 FROM sqlite_master 171 WHERE tbl_name = '$table' 172 ORDER BY type DESC"); 173 if(!$result || sqlite_num_rows($result)<=0){ 174 msg("ALTER TABLE failed, no such table '".hsc($table)."'",-1); 175 return false; 176 } 177 178 $row = sqlite_fetch_array($result); //table sql 179 $tmpname = 't'.time(); 180 181 $origsql = trim(preg_replace("/[\s]+/"," ", 182 str_replace(",",", ", 183 preg_replace('/\)$/',' )', 184 preg_replace("/[\(]/","( ",$row['sql'],1))))); 185 $createtemptableSQL = 'CREATE TEMPORARY '.substr(trim(preg_replace("'".$table."'",$tmpname,$origsql,1)),6); 186 $createindexsql = array(); 187 $i = 0; 188 $defs = preg_split("/[,]+/",$alterdefs,-1,PREG_SPLIT_NO_EMPTY); 189 $prevword = $table; 190 $oldcols = preg_split("/[,]+/",substr(trim($createtemptableSQL),strpos(trim($createtemptableSQL),'(')+1),-1,PREG_SPLIT_NO_EMPTY); 191 $newcols = array(); 192 193 for($i=0;$i<sizeof($oldcols);$i++){ 194 $colparts = preg_split("/[\s]+/",$oldcols[$i],-1,PREG_SPLIT_NO_EMPTY); 195 $oldcols[$i] = $colparts[0]; 196 $newcols[$colparts[0]] = $colparts[0]; 197 } 198 $newcolumns = ''; 199 $oldcolumns = ''; 200 reset($newcols); 201 while(list($key,$val) = each($newcols)){ 202 $newcolumns .= ($newcolumns?', ':'').$val; 203 $oldcolumns .= ($oldcolumns?', ':'').$key; 204 } 205 $copytotempsql = 'INSERT INTO '.$tmpname.'('.$newcolumns.') SELECT '.$oldcolumns.' FROM '.$table; 206 $dropoldsql = 'DROP TABLE '.$table; 207 $createtesttableSQL = $createtemptableSQL; 208 209 foreach($defs as $def){ 210 $defparts = preg_split("/[\s]+/",$def,-1,PREG_SPLIT_NO_EMPTY); 211 $action = strtolower($defparts[0]); 212 switch($action){ 213 case 'add': 214 if(sizeof($defparts) < 2){ 215 msg('ALTER TABLE: not enough arguments for ADD statement',-1); 216 return false; 217 } 218 $createtesttableSQL = substr($createtesttableSQL,0,strlen($createtesttableSQL)-1).','; 219 for($i=1;$i<sizeof($defparts);$i++) 220 $createtesttableSQL.=' '.$defparts[$i]; 221 $createtesttableSQL.=')'; 222 break; 223 224 case 'change': 225 if(sizeof($defparts) <= 3){ 226 msg('ALTER TABLE: near "'.$defparts[0].($defparts[1]?' '.$defparts[1]:'').($defparts[2]?' '.$defparts[2]:'').'": syntax error',-1); 227 return false; 228 } 229 230 if($severpos = strpos($createtesttableSQL,' '.$defparts[1].' ')){ 231 if($newcols[$defparts[1]] != $defparts[1]){ 232 msg('ALTER TABLE: unknown column "'.$defparts[1].'" in "'.$table.'"',-1); 233 return false; 234 } 235 $newcols[$defparts[1]] = $defparts[2]; 236 $nextcommapos = strpos($createtesttableSQL,',',$severpos); 237 $insertval = ''; 238 for($i=2;$i<sizeof($defparts);$i++) 239 $insertval.=' '.$defparts[$i]; 240 if($nextcommapos) 241 $createtesttableSQL = substr($createtesttableSQL,0,$severpos).$insertval.substr($createtesttableSQL,$nextcommapos); 242 else 243 $createtesttableSQL = substr($createtesttableSQL,0,$severpos-(strpos($createtesttableSQL,',')?0:1)).$insertval.')'; 244 } else { 245 msg('ALTER TABLE: unknown column "'.$defparts[1].'" in "'.$table.'"',-1); 246 return false; 247 } 248 break; 249 case 'drop': 250 if(sizeof($defparts) < 2){ 251 msg('ALTER TABLE: near "'.$defparts[0].($defparts[1]?' '.$defparts[1]:'').'": syntax error',-1); 252 return false; 253 } 254 if($severpos = strpos($createtesttableSQL,' '.$defparts[1].' ')){ 255 $nextcommapos = strpos($createtesttableSQL,',',$severpos); 256 if($nextcommapos) 257 $createtesttableSQL = substr($createtesttableSQL,0,$severpos).substr($createtesttableSQL,$nextcommapos + 1); 258 else 259 $createtesttableSQL = substr($createtesttableSQL,0,$severpos-(strpos($createtesttableSQL,',')?0:1) - 1).')'; 260 unset($newcols[$defparts[1]]); 261 }else{ 262 msg('ALTER TABLE: unknown column "'.$defparts[1].'" in "'.$table.'"',-1); 263 return false; 264 } 265 break; 266 default: 267 msg('ALTER TABLE: near "'.$prevword.'": syntax error',-1); 268 return false; 269 } 270 $prevword = $defparts[sizeof($defparts)-1]; 271 } 272 273 // this block of code generates a test table simply to verify that the 274 // columns specifed are valid in an sql statement 275 // this ensures that no reserved words are used as columns, for example 276 $res = $this->query($createtesttableSQL); 277 if($res === false) return false; 278 279 $droptempsql = 'DROP TABLE '.$tmpname; 280 $res = $this->query($droptempsql); 281 if($res === false) return false; 282 283 284 $createnewtableSQL = 'CREATE '.substr(trim(preg_replace("'".$tmpname."'",$table,$createtesttableSQL,1)),17); 285 $newcolumns = ''; 286 $oldcolumns = ''; 287 reset($newcols); 288 while(list($key,$val) = each($newcols)){ 289 $newcolumns .= ($newcolumns?', ':'').$val; 290 $oldcolumns .= ($oldcolumns?', ':'').$key; 291 } 292 293 $copytonewsql = 'INSERT INTO '.$table.'('.$newcolumns.') SELECT '.$oldcolumns.' FROM '.$tmpname; 294 295 $res = $this->query($createtemptableSQL); //create temp table 296 if($res === false) return false; 297 $res = $this->query($copytotempsql); //copy to table 298 if($res === false) return false; 299 $res = $this->query($dropoldsql); //drop old table 300 if($res === false) return false; 301 302 $res = $this->query($createnewtableSQL); //recreate original table 303 if($res === false) return false; 304 $res = $this->query($copytonewsql); //copy back to original table 305 if($res === false) return false; 306 $res = $this->query($droptempsql); //drop temp table 307 if($res === false) return false; 308 309 return $res; // return a valid resource 310 } 311 312 /** 313 * Execute a query with the given parameters. 314 * 315 * Takes care of escaping 316 * 317 * @param string $sql - the statement 318 * @param arguments... 319 */ 320 function query(){ 321 if(!$this->db) return false; 322 323 // get function arguments 324 $args = func_get_args(); 325 $sql = trim(array_shift($args)); 326 $sql = rtrim($sql,';'); 327 328 if(!$sql){ 329 msg('No SQL statement given',-1); 330 return false; 331 } 332 333 if(is_array($args[0])) $args = $args[0]; 334 $argc = count($args); 335 336 // check number of arguments 337 if($argc < substr_count($sql,'?')){ 338 msg('Not enough arguments passed for statement. '. 339 'Expected '.substr_count($sql,'?').' got '. 340 $argc.' - '.hsc($sql),-1); 341 return false; 342 } 343 344 // explode at wildcard, then join again 345 $parts = explode('?',$sql,$argc+1); 346 $args = array_map(array($this,'quote_string'),$args); 347 $sql = ''; 348 349 while( ($part = array_shift($parts)) !== null ){ 350 $sql .= $part; 351 $sql .= array_shift($args); 352 } 353 354 // intercept ALTER TABLE statements 355 if(preg_match('/^ALTER\s+TABLE\s+([\w\.]+)\s+(.*)/i',$sql,$match)){ 356 return $this->_altertable($match[1],$match[2]); 357 } 358 359 // execute query 360 $err = ''; 361 $res = @sqlite_query($this->db,$sql,SQLITE_ASSOC,$err); 362 if($err){ 363 msg($err.':<br /><pre>'.hsc($sql).'</pre>',-1); 364 return false; 365 }elseif(!$res){ 366 msg(sqlite_error_string(sqlite_last_error($this->db)). 367 ':<br /><pre>'.hsc($sql).'</pre>',-1); 368 return false; 369 } 370 371 return $res; 372 } 373 374 /** 375 * Returns a complete result set as array 376 */ 377 function res2arr($res){ 378 $data = array(); 379 if(!sqlite_num_rows($res)) return $data; 380 sqlite_rewind($res); 381 while(($row = sqlite_fetch_array($res)) !== false){ 382 $data[] = $row; 383 } 384 return $data; 385 } 386 387 /** 388 * Return the wanted row from a given result set as 389 * associative array 390 */ 391 function res2row($res,$rownum=0){ 392 if(!@sqlite_seek($res,$rownum)){ 393 return false; 394 } 395 return sqlite_fetch_array($res); 396 } 397 398 399 /** 400 * Join the given values and quote them for SQL insertion 401 */ 402 function quote_and_join($vals,$sep=',') { 403 $vals = array_map(array('helper_plugin_sqlite','quote_string'),$vals); 404 return join($sep,$vals); 405 } 406 407 /** 408 * Run sqlite_escape_string() on the given string and surround it 409 * with quotes 410 */ 411 function quote_string($string){ 412 return "'".sqlite_escape_string($string)."'"; 413 } 414 415 416 /** 417 * Aggregation function for SQLite 418 * 419 * @link http://devzone.zend.com/article/863-SQLite-Lean-Mean-DB-Machine 420 */ 421 function _sqlite_group_concat_step(&$context, $string, $separator = ',') { 422 $context['sep'] = $separator; 423 $context['data'][] = $string; 424 } 425 426 /** 427 * Aggregation function for SQLite 428 * 429 * @link http://devzone.zend.com/article/863-SQLite-Lean-Mean-DB-Machine 430 */ 431 function _sqlite_group_concat_finalize(&$context) { 432 $context['data'] = array_unique($context['data']); 433 return join($context['sep'],$context['data']); 434 } 435 436 437} 438 439// vim:ts=4:sw=4:et:enc=utf-8: 440