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