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