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