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