1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Brend Wanders <b.wanders@utwente.nl> 5 */ 6// must be run within Dokuwiki 7if(!defined('DOKU_INC')) die('Meh.'); 8 9// Define the location of the local credentials file. 10if(!defined('STRATA_CREDENTIALS')) define('STRATA_CREDENTIALS', DOKU_PLUGIN.'strata/credentials.local.php'); 11 12/** 13 * The base class for database drivers. 14 */ 15abstract class plugin_strata_driver { 16 17 /** 18 * Whether the driver should generate debug output. 19 */ 20 var $_debug; 21 22 /** 23 * The dsn. 24 */ 25 var $_dsn; 26 27 /** 28 * The PDO database object. 29 */ 30 var $_db; 31 32 /** 33 * Create a new database driver. 34 * 35 * @param debug boolean whether the created driver should generate debug output. 36 */ 37 function __construct($debug=false) { 38 $this->_debug = $debug; 39 $this->util =& plugin_load('helper', 'strata_util'); 40 } 41 42 /** 43 * Produces the syntax to cast something to a number. 44 * 45 * @param val string the thing to cast 46 */ 47 public function castToNumber($val) { 48 return "CAST($val AS NUMERIC)"; 49 } 50 51 /** 52 * Casts the given value to a case insensitive variant. 53 * 54 * This cast can, for example, be using a case insensitive collation or using the function 'lower' (default). 55 * @param val string the thing make case insensitive 56 */ 57 public function ci($val='?') { 58 return "lower($val)"; 59 } 60 61 /** 62 * Returns the syntax for case-insensitive string comparison. 63 * 64 * Preferably, this syntax should allow % as a wildcard (e.g. as done by LIKE). 65 */ 66 public function stringCompare() { 67 return 'LIKE'; 68 } 69 70 /** 71 * Returns the terms on which we should order. 72 * 73 * Ideally, the ordering should be natural, that is '2 apples' is sorted before '10 pears'. 74 * However, depending on the supported database, ordering can vary between string and natural ordering, including any compromises. 75 * @param val string the thing to sort on 76 * @return an array of terms to sort on 77 */ 78 public function orderBy($val) { 79 return array( 80 $this->castToNumber($val), 81 $this->ci($val) 82 ); 83 } 84 85 /** 86 * Open the database connection. 87 * 88 * @param dsn string the dsn to use for connecting 89 * @return boolean true when connecting was successful 90 */ 91 public function connect($dsn) { 92 $this->_dsn = $dsn; 93 try { 94 $this->_db = $this->initializePDO($dsn); 95 } catch(PDOException $e) { 96 if ($this->_debug) { 97 msg(sprintf($this->util->getLang('driver_failed_detail'), hsc($dsn), hsc($e->getMessage())), -1); 98 } else { 99 msg($this->util->getLang('driver_failed'), -1); 100 } 101 return false; 102 } 103 $this->initializeConnection(); 104 return true; 105 } 106 107 /** 108 * Initialize the PDO object. 109 * 110 * @param dsn string the dsn to use for construction 111 * @return the PDO object 112 */ 113 protected function initializePDO($dsn) { 114 $credentials = array(null,null); 115 if(@file_exists(STRATA_CREDENTIALS)) { 116 $credentials = include(STRATA_CREDENTIALS); 117 } 118 return new PDO($dsn, $credentials[0], $credentials[1]); 119 } 120 121 122 /** 123 * Initialises a connection directly after the connection was made (e.g. by setting the character set of the connection). 124 */ 125 protected function initializeConnection() {} 126 127 /** 128 * Determines whether the database is initialised. 129 * 130 * @return boolean true if the database is initialised 131 */ 132 public abstract function isInitialized(); 133 134 /** 135 * Initialises the database by setting up all tables. 136 * 137 * This implementation looks for a file called 'setup-@driver@.sql' and executes all SQL statements in that file. 138 * Here, '@driver@' represents the database driver, such as 'sqlite'. 139 * 140 * @return boolean true if the database was initialised successfully 141 */ 142 public function initializeDatabase() { 143 if($this->_db == false) return false; 144 145 // determine driver 146 list($driver, $connection) = explode(':', $this->_dsn, 2); 147 if ($this->_debug) msg(sprintf($this->util->getLang('driver_setup_start'), hsc($driver))); 148 149 // load SQL script 150 $sqlfile = DOKU_PLUGIN . "strata/sql/setup-$driver.sql"; 151 152 $sql = io_readFile($sqlfile, false); 153 $lines = explode("\n",$sql); 154 155 // remove empty lines and comment lines 156 // (this makes sure that a semicolon in the comment doesn't break the script) 157 $sql = ''; 158 foreach($lines as $line) { 159 $line = preg_replace('/--.*$/','',$line); 160 if(trim($line," \t\n\r") == '') continue; 161 $sql .= $line; 162 } 163 164 // split the script into distinct statements 165 $sql = explode(';', $sql); 166 167 // execute the database initialisation script in a transaction 168 // (doesn't work in all databases, but provides some failsafe where it works) 169 $this->beginTransaction(); 170 foreach($sql as $s) { 171 // skip empty lines (usually the last line is empty, due to the final semicolon) 172 if(trim($s) == '') continue; 173 174 if ($this->_debug) msg(sprintf($this->util->getLang('driver_setup_statement'),hsc($s))); 175 if(!$this->query($s, $this->util->getLang('driver_setup_failed'))) { 176 $this->rollBack(); 177 return false; 178 } 179 } 180 $this->commit(); 181 182 if($this->_debug) msg($this->util->getLang('driver_setup_succes'), 1); 183 184 return true; 185 } 186 187 /** 188 * Removes a database that was initialized before. 189 * 190 * @return whether the database was removed successfully 191 */ 192 public function removeDatabase() { 193 return $this->query('DROP TABLE data', $this->util->getLang('driver_remove_failed')); 194 } 195 196 /** 197 * Prepares a query and reports any problems to Dokuwiki. 198 * 199 * @param query string the query to prepare 200 * @return the prepared statement 201 */ 202 public function prepare($query) { 203 if($this->_db == false) return false; 204 205 $result = $this->_db->prepare($query); 206 if ($result === false) { 207 $error = $this->_db->errorInfo(); 208 msg(sprintf($this->util->getLang('driver_prepare_failed'),hsc($query), hsc($error[2])),-1); 209 return false; 210 } 211 212 return $result; 213 } 214 215 /** 216 * Executes a query and reports any problems to Dokuwiki. 217 * 218 * @param query string the query to execute 219 * @param message string message to report when executing the query fails 220 * @return whether querying succeeded 221 */ 222 public function query($query, $message=false) { 223 if($this->_db == false) return false; 224 225 if($message === false) { 226 $message = $this->util->getLang('driver_query_failed_default'); 227 } 228 229 $res = $this->_db->query($query); 230 if ($res === false) { 231 $error = $this->_db->errorInfo(); 232 msg(sprintf($this->utiutil->getLang('driver_query_failed'), $message, hsc($query), hsc($error[2])),-1); 233 return false; 234 } 235 return true; 236 } 237 238 private $transactions = array(); 239 private $transactionCount = 0; 240 241 private function _transactionId() { 242 return "t".$this->transactionCount++; 243 } 244 245 /** 246 * Begins a transaction. 247 */ 248 public function beginTransaction() { 249 if($this->_db == false) return false; 250 251 if(count($this->transactions)) { 252 $t = $this->_transactionId(); 253 array_push($this->transactions, $t); 254 $this->_db->query('SAVEPOINT '.$t.';'); 255 return true; 256 } else { 257 array_push($this->transactions, 'work'); 258 return $this->_db->beginTransaction(); 259 } 260 } 261 262 /** 263 * Commits the current transaction. 264 */ 265 public function commit() { 266 if($this->_db == false) return false; 267 268 array_pop($this->transactions); 269 if(count($this->transactions)) { 270 return true; 271 } else { 272 return $this->_db->commit(); 273 } 274 } 275 276 /** 277 * Rolls back the current transaction. 278 */ 279 public function rollBack() { 280 if($this->_db == false) return false; 281 282 $t = array_pop($this->transactions); 283 if(count($this->transactions)) { 284 $this->_db->query('ROLLBACK TO '.$t.';'); 285 return true; 286 } else { 287 return $this->_db->rollBack(); 288 } 289 } 290} 291