xref: /plugin/upgrade/admin.php (revision d04a99cd62a3baf7e73fd5b9907fc37f616ae0d2)
1<?php
2/**
3 * DokuWiki Plugin upgrade (Admin Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Andreas Gohr <andi@splitbrain.org>
7 */
8
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
12require_once DOKU_PLUGIN.'admin.php';
13require_once DOKU_PLUGIN.'upgrade/VerboseTarLib.class.php';
14
15class admin_plugin_upgrade extends DokuWiki_Admin_Plugin {
16    private $tgzurl;
17    private $tgzfile;
18    private $tgzdir;
19    private $tgzversion;
20    private $pluginversion;
21
22    public function __construct() {
23        global $conf;
24
25        $branch = 'stable';
26
27        $this->tgzurl        = "https://github.com/splitbrain/dokuwiki/archive/$branch.tar.gz";
28        $this->tgzfile       = $conf['tmpdir'].'/dokuwiki-upgrade.tgz';
29        $this->tgzdir        = $conf['tmpdir'].'/dokuwiki-upgrade/';
30        $this->tgzversion    = "https://raw.githubusercontent.com/splitbrain/dokuwiki/$branch/VERSION";
31        $this->pluginversion = "https://raw.githubusercontent.com/splitbrain/dokuwiki-plugin-upgrade/master/plugin.info.txt";
32    }
33
34    public function getMenuSort() {
35        return 555;
36    }
37
38    public function handle() {
39        if($_REQUEST['step'] && !checkSecurityToken()) {
40            unset($_REQUEST['step']);
41        }
42    }
43
44    public function html() {
45        $abrt = false;
46        $next = false;
47
48        echo '<h1>'.$this->getLang('menu').'</h1>';
49
50        $this->_say('<div id="plugin__upgrade">');
51        // enable auto scroll
52        ?>
53        <script language="javascript" type="text/javascript">
54            var plugin_upgrade = window.setInterval(function () {
55                var obj = document.getElementById('plugin__upgrade');
56                if (obj) obj.scrollTop = obj.scrollHeight;
57            }, 25);
58        </script>
59        <?php
60
61        // handle current step
62        $this->_stepit($abrt, $next);
63
64        // disable auto scroll
65        ?>
66        <script language="javascript" type="text/javascript">
67            window.setTimeout(function () {
68                window.clearInterval(plugin_upgrade);
69            }, 50);
70        </script>
71        <?php
72        $this->_say('</div>');
73
74        echo '<form action="" method="get" id="plugin__upgrade_form">';
75        echo '<input type="hidden" name="do" value="admin" />';
76        echo '<input type="hidden" name="page" value="upgrade" />';
77        echo '<input type="hidden" name="sectok" value="'.getSecurityToken().'" />';
78        if($next) echo '<input type="submit" name="step['.$next.']" value="'.$this->getLang('btn_continue').' ➡" class="button continue" />';
79        if($abrt) echo '<input type="submit" name="step[cancel]" value="✖ '.$this->getLang('btn_abort').'" class="button abort" />';
80        echo '</form>';
81
82        $this->_progress($next);
83    }
84
85    /**
86     * Display a progress bar of all steps
87     *
88     * @param string $next the next step
89     */
90    private function _progress($next) {
91        $steps  = array('version', 'download', 'unpack', 'check', 'upgrade');
92        $active = true;
93        $count = 0;
94
95        echo '<div id="plugin__upgrade_meter"><ol>';
96        foreach($steps as $step) {
97            $count++;
98            if($step == $next) $active = false;
99            if($active) {
100                echo '<li class="active">';
101                echo '<span class="step">✔</span>';
102            } else {
103                echo '<li>';
104                echo '<span class="step">'.$count.'</span>';
105            }
106
107            echo '<span class="stage">'.$this->getLang('step_'.$step).'</span>';
108            echo '</li>';
109        }
110        echo '</ol></div>';
111    }
112
113    /**
114     * Decides the current step and executes it
115     *
116     * @param bool $abrt
117     * @param bool $next
118     */
119    private function _stepit(&$abrt, &$next) {
120        global $conf;
121        if($conf['safemodehack']) {
122            $abrt = false;
123            $next = false;
124            echo $this->locale_xhtml('safemode');
125        }
126
127        if(isset($_REQUEST['step']) && is_array($_REQUEST['step'])) {
128            $step = array_shift(array_keys($_REQUEST['step']));
129        } else {
130            $step = '';
131        }
132
133        if($step == 'cancel') {
134            # cleanup
135            @unlink($this->tgzfile);
136            $this->_rdel($this->tgzdir);
137            $step = '';
138        }
139
140        if($step) {
141            $abrt = true;
142            $next = false;
143            if($step == 'version') {
144                $this->_step_version();
145                $next = 'download';
146            } elseif(!file_exists($this->tgzfile)) {
147                if($this->_step_download()) $next = 'unpack';
148            } elseif(!is_dir($this->tgzdir)) {
149                if($this->_step_unpack()) $next = 'check';
150            } elseif($step != 'upgrade') {
151                if($this->_step_check()) $next = 'upgrade';
152            } elseif($step == 'upgrade') {
153                if($this->_step_copy()) $next = 'cancel';
154            } else {
155                echo 'uhm. what happened? where am I? This should not happen';
156            }
157        } else {
158            # first time run, show intro
159            echo $this->locale_xhtml('step0');
160            $abrt = false;
161            $next = 'version';
162        }
163    }
164
165    /**
166     * Output the given arguments using vsprintf and flush buffers
167     */
168    public static function _say() {
169        $args = func_get_args();
170        echo '<img src="'.DOKU_BASE.'lib/images/blank.gif" width="16" height="16" alt="" /> ';
171        echo vsprintf(array_shift($args)."<br />\n", $args);
172        flush();
173        ob_flush();
174    }
175
176    /**
177     * Print a warning using the given arguments with vsprintf and flush buffers
178     */
179    public static function _warn() {
180        $args = func_get_args();
181        echo '<img src="'.DOKU_BASE.'lib/images/error.png" width="16" height="16" alt="!" /> ';
182        echo vsprintf(array_shift($args)."<br />\n", $args);
183        flush();
184        ob_flush();
185    }
186
187    /**
188     * Recursive delete
189     *
190     * @author Jon Hassall
191     * @link   http://de.php.net/manual/en/function.unlink.php#87045
192     */
193    private function _rdel($dir) {
194        if(!$dh = @opendir($dir)) {
195            return false;
196        }
197        while(false !== ($obj = readdir($dh))) {
198            if($obj == '.' || $obj == '..') continue;
199
200            if(!@unlink($dir.'/'.$obj)) {
201                $this->_rdel($dir.'/'.$obj);
202            }
203        }
204        closedir($dh);
205        return @rmdir($dir);
206    }
207
208    /**
209     * Check various versions
210     *
211     * @return bool
212     */
213    private function _step_version() {
214        $ok = true;
215
216        // check if PHP is up to date
217        if(version_compare(phpversion(), '5.2.0', '<')) {
218            $this->_warn($this->getLang('vs_php'));
219            $ok = false;
220        }
221
222        // get the available version
223        $http       = new DokuHTTPClient();
224        $tgzversion = $http->get($this->tgzversion);
225        if(!$tgzversion) {
226            $this->_warn($this->getLang('vs_tgzno').' '.hsc($http->error));
227            $ok = false;
228        }
229        if(!preg_match('/(^| )(\d\d\d\d-\d\d-\d\d[a-z]*)( |$)/i', $tgzversion, $m)) {
230            $this->_warn($this->getLang('vs_tgzno'));
231            $ok            = false;
232            $tgzversionnum = 0;
233        } else {
234            $tgzversionnum = $m[2];
235            $this->_say($this->getLang('vs_tgz'), $tgzversion);
236        }
237
238        // get the current version
239        $version = getVersion();
240        if(!preg_match('/(^| )(\d\d\d\d-\d\d-\d\d[a-z]*)( |$)/i', $version, $m)) {
241            $versionnum = 0;
242        } else {
243            $versionnum = $m[2];
244        }
245        $this->_say($this->getLang('vs_local'), $version);
246
247        // compare versions
248        if(!$versionnum) {
249            $this->_warn($this->getLang('vs_localno'));
250            $ok = false;
251        } else if($tgzversionnum) {
252            if($tgzversionnum < $versionnum) {
253                $this->_warn($this->getLang('vs_newer'));
254                $ok = false;
255            } elseif($tgzversionnum == $versionnum) {
256                $this->_warn($this->getLang('vs_same'));
257                $ok = false;
258            }
259        }
260
261        // check plugin version
262        $pluginversion = $http->get($this->pluginversion);
263        if($pluginversion) {
264            $plugininfo = linesToHash(explode("\n", $pluginversion));
265            $myinfo     = $this->getInfo();
266            if($plugininfo['date'] > $myinfo['date']) {
267                $this->_warn($this->getLang('vs_plugin'));
268                $ok = false;
269            }
270        }
271
272        return $ok;
273    }
274
275    /**
276     * Download the tarball
277     *
278     * @return bool
279     */
280    private function _step_download() {
281        $this->_say($this->getLang('dl_from'), $this->tgzurl);
282
283        @set_time_limit(120);
284        @ignore_user_abort();
285
286        $http          = new DokuHTTPClient();
287        $http->timeout = 120;
288        $data          = $http->get($this->tgzurl);
289
290        if(!$data) {
291            $this->_warn($http->error);
292            $this->_warn($this->getLang('dl_fail'));
293            return false;
294        }
295
296        if(!io_saveFile($this->tgzfile, $data)) {
297            $this->_warn($this->getLang('dl_fail'));
298            return false;
299        }
300
301        $this->_say($this->getLang('dl_done'), filesize_h(strlen($data)));
302
303        return true;
304    }
305
306    /**
307     * Unpack the tarball
308     *
309     * @return bool
310     */
311    private function _step_unpack() {
312        $this->_say('<b>'.$this->getLang('pk_extract').'</b>');
313
314        @set_time_limit(120);
315        @ignore_user_abort();
316
317        try {
318            $tar = new VerboseTar();
319            $tar->open($this->tgzfile);
320            $tar->extract($this->tgzdir, 1);
321            $tar->close();
322        } catch (Exception $e) {
323            $this->_warn($e->getMessage());
324            $this->_warn($this->getLang('pk_fail'));
325            return false;
326        }
327
328        $this->_say($this->getLang('pk_done'));
329
330        $this->_say(
331            $this->getLang('pk_version'),
332            hsc(file_get_contents($this->tgzdir.'/VERSION')),
333            getVersion()
334        );
335        return true;
336    }
337
338    /**
339     * Check permissions of files to change
340     *
341     * @return bool
342     */
343    private function _step_check() {
344        $this->_say($this->getLang('ck_start'));
345        $ok = $this->_traverse('', true);
346        if($ok) {
347            $this->_say('<b>'.$this->getLang('ck_done').'</b>');
348        } else {
349            $this->_warn('<b>'.$this->getLang('ck_fail').'</b>');
350        }
351        return $ok;
352    }
353
354    /**
355     * Copy over new files
356     *
357     * @return bool
358     */
359    private function _step_copy() {
360        $this->_say($this->getLang('cp_start'));
361        $ok = $this->_traverse('', false);
362        if($ok) {
363            $this->_say('<b>'.$this->getLang('cp_done').'</b>');
364            $this->_rmold();
365            $this->_say('<b>'.$this->getLang('finish').'</b>');
366        } else {
367            $this->_warn('<b>'.$this->getLang('cp_fail').'</b>');
368        }
369        return $ok;
370    }
371
372    /**
373     * Delete outdated files
374     */
375    private function _rmold() {
376        $list = file($this->tgzdir.'data/deleted.files');
377        foreach($list as $line) {
378            $line = trim(preg_replace('/#.*$/', '', $line));
379            if(!$line) continue;
380            $file = DOKU_INC.$line;
381            if(!file_exists($file)) continue;
382            if((is_dir($file) && $this->_rdel($file)) ||
383                @unlink($file)
384            ) {
385                $this->_say($this->getLang('rm_done'), hsc($line));
386            } else {
387                $this->_warn($this->getLang('rm_fail'), hsc($line));
388            }
389        }
390    }
391
392    /**
393     * Traverse over the given dir and compare it to the DokuWiki dir
394     *
395     * Checks what files need an update, tests for writability and copies
396     *
397     * @param string $dir
398     * @param bool   $dryrun do not copy but only check permissions
399     * @return bool
400     */
401    private function _traverse($dir, $dryrun) {
402        $base = $this->tgzdir;
403        $ok   = true;
404
405        $dh = @opendir($base.'/'.$dir);
406        if(!$dh) return false;
407        while(($file = readdir($dh)) !== false) {
408            if($file == '.' || $file == '..') continue;
409            $from = "$base/$dir/$file";
410            $to   = DOKU_INC."$dir/$file";
411
412            if(is_dir($from)) {
413                if($dryrun) {
414                    // just check for writability
415                    if(!is_dir($to)) {
416                        if(is_dir(dirname($to)) && !is_writable(dirname($to))) {
417                            $this->_warn('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file"));
418                            $ok = false;
419                        }
420                    }
421                }
422
423                // recursion
424                if(!$this->_traverse("$dir/$file", $dryrun)) {
425                    $ok = false;
426                }
427            } else {
428                $fmd5 = md5(@file_get_contents($from));
429                $tmd5 = md5(@file_get_contents($to));
430                if($fmd5 != $tmd5 || !file_exists($to)) {
431                    if($dryrun) {
432                        // just check for writability
433                        if((file_exists($to) && !is_writable($to)) ||
434                            (!file_exists($to) && is_dir(dirname($to)) && !is_writable(dirname($to)))
435                        ) {
436
437                            $this->_warn('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file"));
438                            $ok = false;
439                        } else {
440                            $this->_say($this->getLang('tv_upd'), hsc("$dir/$file"));
441                        }
442                    } else {
443                        // check dir
444                        if(io_mkdir_p(dirname($to))) {
445                            // copy
446                            if(!copy($from, $to)) {
447                                $this->_warn('<b>'.$this->getLang('tv_nocopy').'</b>', hsc("$dir/$file"));
448                                $ok = false;
449                            } else {
450                                $this->_say($this->getLang('tv_done'), hsc("$dir/$file"));
451                            }
452                        } else {
453                            $this->_warn('<b>'.$this->getLang('tv_nodir').'</b>', hsc("$dir"));
454                            $ok = false;
455                        }
456                    }
457                }
458            }
459        }
460        closedir($dh);
461        return $ok;
462    }
463}
464
465// vim:ts=4:sw=4:et:enc=utf-8:
466