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