xref: /plugin/upgrade/admin.php (revision eb52c46a343434f52d0105c529efb816add8ca38)
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="Continue" class="button continue" />';
79        if($abrt) echo '<input type="submit" name="step[cancel]" value="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 vsprintf(array_shift($args)."<br />\n", $args);
171        flush();
172        ob_flush();
173    }
174
175    /**
176     * Recursive delete
177     *
178     * @author Jon Hassall
179     * @link   http://de.php.net/manual/en/function.unlink.php#87045
180     */
181    private function _rdel($dir) {
182        if(!$dh = @opendir($dir)) {
183            return false;
184        }
185        while(false !== ($obj = readdir($dh))) {
186            if($obj == '.' || $obj == '..') continue;
187
188            if(!@unlink($dir.'/'.$obj)) {
189                $this->_rdel($dir.'/'.$obj);
190            }
191        }
192        closedir($dh);
193        return @rmdir($dir);
194    }
195
196    /**
197     * Check various versions
198     *
199     * @return bool
200     */
201    private function _step_version() {
202        $ok = true;
203
204        // check if PHP is up to date
205        if(version_compare(phpversion(), '5.2.0', '<')) {
206            $this->_say('<div class="error">'.$this->getLang('vs_php').'</div>');
207            $ok = false;
208        }
209
210        // get the available version
211        $http       = new DokuHTTPClient();
212        $tgzversion = $http->get($this->tgzversion);
213        if(!$tgzversion) {
214            $this->_say('<div class="error">'.$this->getLang('vs_tgzno').' '.hsc($http->error).'</div>');
215            $ok = false;
216        }
217        if(!preg_match('/(^| )(\d\d\d\d-\d\d-\d\d[a-z]*)( |$)/i', $tgzversion, $m)) {
218            $this->_say('<div class="error">'.$this->getLang('vs_tgzno').'</div>');
219            $ok            = false;
220            $tgzversionnum = 0;
221        } else {
222            $tgzversionnum = $m[2];
223            $this->_say($this->getLang('vs_tgz'), $tgzversion);
224        }
225
226        // get the current version
227        $version = getVersion();
228        if(!preg_match('/(^| )(\d\d\d\d-\d\d-\d\d[a-z]*)( |$)/i', $version, $m)) {
229            $versionnum = 0;
230        } else {
231            $versionnum = $m[2];
232        }
233        $this->_say($this->getLang('vs_local'), $version);
234
235        // compare versions
236        if(!$versionnum) {
237            $this->_say('<div class="error">'.$this->getLang('vs_localno').'</div>');
238            $ok = false;
239        } else if($tgzversionnum) {
240            if($tgzversionnum < $versionnum) {
241                $this->_say('<div class="error">'.$this->getLang('vs_newer').'</div>');
242                $ok = false;
243            } elseif($tgzversionnum == $versionnum) {
244                $this->_say('<div class="error">'.$this->getLang('vs_same').'</div>');
245                $ok = false;
246            }
247        }
248
249        // check plugin version
250        $pluginversion = $http->get($this->pluginversion);
251        if($pluginversion) {
252            $plugininfo = linesToHash(explode("\n", $pluginversion));
253            $myinfo     = $this->getInfo();
254            if($plugininfo['date'] > $myinfo['date']) {
255                $this->_say('<div class="error">'.$this->getLang('vs_plugin').'</div>');
256                $ok = false;
257            }
258        }
259
260        return $ok;
261    }
262
263    /**
264     * Download the tarball
265     *
266     * @return bool
267     */
268    private function _step_download() {
269        $this->_say($this->getLang('dl_from'), $this->tgzurl);
270
271        @set_time_limit(120);
272        @ignore_user_abort();
273
274        $http          = new DokuHTTPClient();
275        $http->timeout = 120;
276        $data          = $http->get($this->tgzurl);
277
278        if(!$data) {
279            $this->_say($http->error);
280            $this->_say($this->getLang('dl_fail'));
281            return false;
282        }
283
284        if(!io_saveFile($this->tgzfile, $data)) {
285            $this->_say($this->getLang('dl_fail'));
286            return false;
287        }
288
289        $this->_say($this->getLang('dl_done'), filesize_h(strlen($data)));
290
291        return true;
292    }
293
294    /**
295     * Unpack the tarball
296     *
297     * @return bool
298     */
299    private function _step_unpack() {
300        global $conf;
301        $this->_say('<b>'.$this->getLang('pk_extract').'</b>');
302
303        @set_time_limit(120);
304        @ignore_user_abort();
305
306        $tar = new VerboseTarLib($this->tgzfile);
307        if($tar->_initerror < 0) {
308            $this->_say($tar->TarErrorStr($tar->_initerror));
309            $this->_say($this->getLang('pk_fail'));
310            return false;
311        }
312
313        $ok = $tar->Extract(VerboseTarLib::FULL_ARCHIVE, $this->tgzdir, 1, $conf['fmode'], '/^(_cs|_test|\.gitignore)/');
314        if($ok < 1) {
315            $this->_say($tar->TarErrorStr($ok));
316            $this->_say($this->getLang('pk_fail'));
317            return false;
318        }
319
320        $this->_say($this->getLang('pk_done'));
321
322        $this->_say(
323            $this->getLang('pk_version'),
324            hsc(file_get_contents($this->tgzdir.'/VERSION')),
325            getVersion()
326        );
327        return true;
328    }
329
330    /**
331     * Check permissions of files to change
332     *
333     * @return bool
334     */
335    private function _step_check() {
336        $this->_say($this->getLang('ck_start'));
337        $ok = $this->_traverse('', true);
338        if($ok) {
339            $this->_say('<b>'.$this->getLang('ck_done').'</b>');
340        } else {
341            $this->_say('<b>'.$this->getLang('ck_fail').'</b>');
342        }
343        return $ok;
344    }
345
346    /**
347     * Copy over new files
348     *
349     * @return bool
350     */
351    private function _step_copy() {
352        $this->_say($this->getLang('cp_start'));
353        $ok = $this->_traverse('', false);
354        if($ok) {
355            $this->_say('<b>'.$this->getLang('cp_done').'</b>');
356            $this->_rmold();
357            $this->_say('<b>'.$this->getLang('finish').'</b>');
358        } else {
359            $this->_say('<b>'.$this->getLang('cp_fail').'</b>');
360        }
361        return $ok;
362    }
363
364    /**
365     * Delete outdated files
366     */
367    private function _rmold() {
368        $list = file($this->tgzdir.'data/deleted.files');
369        foreach($list as $line) {
370            $line = trim(preg_replace('/#.*$/', '', $line));
371            if(!$line) continue;
372            $file = DOKU_INC.$line;
373            if(!file_exists($file)) continue;
374            if((is_dir($file) && $this->_rdel($file)) ||
375                @unlink($file)
376            ) {
377                $this->_say($this->getLang('rm_done'), hsc($line));
378            } else {
379                $this->_say($this->getLang('rm_fail'), hsc($line));
380            }
381        }
382    }
383
384    /**
385     * Traverse over the given dir and compare it to the DokuWiki dir
386     *
387     * Checks what files need an update, tests for writability and copies
388     *
389     * @param string $dir
390     * @param bool   $dryrun do not copy but only check permissions
391     * @return bool
392     */
393    private function _traverse($dir, $dryrun) {
394        $base = $this->tgzdir;
395        $ok   = true;
396
397        $dh = @opendir($base.'/'.$dir);
398        if(!$dh) return false;
399        while(($file = readdir($dh)) !== false) {
400            if($file == '.' || $file == '..') continue;
401            $from = "$base/$dir/$file";
402            $to   = DOKU_INC."$dir/$file";
403
404            if(is_dir($from)) {
405                if($dryrun) {
406                    // just check for writability
407                    if(!is_dir($to)) {
408                        if(is_dir(dirname($to)) && !is_writable(dirname($to))) {
409                            $this->_say('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file"));
410                            $ok = false;
411                        }
412                    }
413                }
414
415                // recursion
416                if(!$this->_traverse("$dir/$file", $dryrun)) {
417                    $ok = false;
418                }
419            } else {
420                $fmd5 = md5(@file_get_contents($from));
421                $tmd5 = md5(@file_get_contents($to));
422                if($fmd5 != $tmd5 || !file_exists($to)) {
423                    if($dryrun) {
424                        // just check for writability
425                        if((file_exists($to) && !is_writable($to)) ||
426                            (!file_exists($to) && is_dir(dirname($to)) && !is_writable(dirname($to)))
427                        ) {
428
429                            $this->_say('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file"));
430                            $ok = false;
431                        } else {
432                            $this->_say($this->getLang('tv_upd'), hsc("$dir/$file"));
433                        }
434                    } else {
435                        // check dir
436                        if(io_mkdir_p(dirname($to))) {
437                            // copy
438                            if(!copy($from, $to)) {
439                                $this->_say('<b>'.$this->getLang('tv_nocopy').'</b>', hsc("$dir/$file"));
440                                $ok = false;
441                            } else {
442                                $this->_say($this->getLang('tv_done'), hsc("$dir/$file"));
443                            }
444                        } else {
445                            $this->_say('<b>'.$this->getLang('tv_nodir').'</b>', hsc("$dir"));
446                            $ok = false;
447                        }
448                    }
449                }
450            }
451        }
452        closedir($dh);
453        return $ok;
454    }
455}
456
457// vim:ts=4:sw=4:et:enc=utf-8:
458