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