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