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