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