xref: /plugin/upgrade/admin.php (revision bd08ebd1656ec632c41a18142010f16ccf0d3b40)
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
20    public function __construct() {
21        global $conf;
22
23        $branch = 'stable';
24
25        $this->tgzurl  = "https://github.com/splitbrain/dokuwiki/archive/$branch.tar.gz";
26        $this->tgzfile = $conf['tmpdir'].'/dokuwiki-upgrade.tgz';
27        $this->tgzdir  = $conf['tmpdir'].'/dokuwiki-upgrade/';
28    }
29
30    public function getMenuSort() {
31        return 555;
32    }
33
34    public function handle() {
35        if($_REQUEST['step'] && !checkSecurityToken()) {
36            unset($_REQUEST['step']);
37        }
38    }
39
40    public function html() {
41        $abrt = false;
42        $next = false;
43
44        echo '<h1>'.$this->getLang('menu').'</h1>';
45
46        $this->_say('<div id="plugin__upgrade">');
47        // enable auto scroll
48        ?>
49        <script language="javascript" type="text/javascript">
50            var plugin_upgrade = window.setInterval(function () {
51                var obj = document.getElementById('plugin__upgrade');
52                if (obj) obj.scrollTop = obj.scrollHeight;
53            }, 25);
54        </script>
55        <?php
56
57        // handle current step
58        $this->_stepit($abrt, $next);
59
60        // disable auto scroll
61        ?>
62        <script language="javascript" type="text/javascript">
63            window.setTimeout(function () {
64                window.clearInterval(plugin_upgrade);
65            }, 50);
66        </script>
67        <?php
68        $this->_say('</div>');
69
70        echo '<form action="" method="get" id="plugin__upgrade_form">';
71        echo '<input type="hidden" name="do" value="admin" />';
72        echo '<input type="hidden" name="page" value="upgrade" />';
73        echo '<input type="hidden" name="sectok" value="'.getSecurityToken().'" />';
74        if($next) echo '<input type="submit" name="step['.$next.']" value="Continue" class="button continue" />';
75        if($abrt) echo '<input type="submit" name="step[cancel]" value="Abort" class="button abort" />';
76        echo '</form>';
77    }
78
79    /**
80     * Decides the current step and executes it
81     *
82     * @param bool $abrt
83     * @param bool $next
84     */
85    private function _stepit(&$abrt, &$next) {
86        global $conf;
87        if($conf['safemodehack']) {
88            $abrt = false;
89            $next = false;
90            echo $this->locale_xhtml('safemode');
91        }
92
93        if(isset($_REQUEST['step']) && is_array($_REQUEST['step'])) {
94            $step = array_shift(array_keys($_REQUEST['step']));
95        } else {
96            $step = '';
97        }
98
99        if($step == 'cancel') {
100            # cleanup
101            @unlink($this->tgzfile);
102            $this->_rdel($this->tgzdir);
103            $step = '';
104        }
105
106        if($step) {
107            $abrt = true;
108            $next = false;
109            if(!file_exists($this->tgzfile)) {
110                if($this->_step_download()) $next = 'unpack';
111            } elseif(!is_dir($this->tgzdir)) {
112                if($this->_step_unpack()) $next = 'check';
113            } elseif($step != 'upgrade') {
114                if($this->_step_check()) $next = 'upgrade';
115            } elseif($step == 'upgrade') {
116                if($this->_step_copy()) $next = 'cancel';
117            } else {
118                echo 'uhm. what happened? where am I? This should not happen';
119            }
120        } else {
121            # first time run, show intro
122            echo $this->locale_xhtml('step0');
123            $abrt = false;
124            $next = 'download';
125        }
126    }
127
128    /**
129     * Output the given arguments using vsprintf and flush buffers
130     */
131    private function _say() {
132        $args = func_get_args();
133        echo vsprintf(array_shift($args)."<br />\n", $args);
134        flush();
135        ob_flush();
136    }
137
138    /**
139     * Recursive delete
140     *
141     * @author Jon Hassall
142     * @link   http://de.php.net/manual/en/function.unlink.php#87045
143     */
144    private function _rdel($dir) {
145        if(!$dh = @opendir($dir)) {
146            return false;
147        }
148        while(false !== ($obj = readdir($dh))) {
149            if($obj == '.' || $obj == '..') continue;
150
151            if(!@unlink($dir.'/'.$obj)) {
152                $this->_rdel($dir.'/'.$obj);
153            }
154        }
155        closedir($dh);
156        return @rmdir($dir);
157    }
158
159    /**
160     * Download the tarball
161     *
162     * @return bool
163     */
164    private function _step_download() {
165        $this->_say($this->getLang('dl_from'), $this->tgzurl);
166
167        @set_time_limit(120);
168        @ignore_user_abort();
169
170        $http          = new DokuHTTPClient();
171        $http->timeout = 120;
172        $data          = $http->get($this->tgzurl);
173
174        if(!$data) {
175            $this->_say($http->error);
176            $this->_say($this->getLang('dl_fail'));
177            return false;
178        }
179
180        if(!io_saveFile($this->tgzfile, $data)) {
181            $this->_say($this->getLang('dl_fail'));
182            return false;
183        }
184
185        $this->_say($this->getLang('dl_done'), filesize_h(strlen($data)));
186
187        return true;
188    }
189
190    /**
191     * Unpack the tarball
192     *
193     * @return bool
194     */
195    private function _step_unpack() {
196        global $conf;
197        $this->_say('<b>'.$this->getLang('pk_extract').'</b>');
198
199        @set_time_limit(120);
200        @ignore_user_abort();
201
202        $tar = new VerboseTarLib($this->tgzfile);
203        if($tar->_initerror < 0) {
204            $this->_say($tar->TarErrorStr($tar->_initerror));
205            $this->_say($this->getLang('pk_fail'));
206            return false;
207        }
208
209        $ok = $tar->Extract(VerboseTarLib::FULL_ARCHIVE, $this->tgzdir, 1, $conf['fmode'], '/^(_cs|_test|\.gitignore)/');
210        if($ok < 1) {
211            $this->_say($tar->TarErrorStr($ok));
212            $this->_say($this->getLang('pk_fail'));
213            return false;
214        }
215
216        $this->_say($this->getLang('pk_done'));
217
218        $this->_say(
219            $this->getLang('pk_version'),
220            hsc(file_get_contents($this->tgzdir.'/VERSION')),
221            getVersion()
222        );
223        return true;
224    }
225
226    /**
227     * Check permissions of files to change
228     *
229     * @return bool
230     */
231    private function _step_check() {
232        $this->_say($this->getLang('ck_start'));
233        $ok = $this->_traverse('', true);
234        if($ok) {
235            $this->_say('<b>'.$this->getLang('ck_done').'</b>');
236        } else {
237            $this->_say('<b>'.$this->getLang('ck_fail').'</b>');
238        }
239        return $ok;
240    }
241
242    /**
243     * Copy over new files
244     *
245     * @return bool
246     */
247    private function _step_copy() {
248        $this->_say($this->getLang('cp_start'));
249        $ok = $this->_traverse('', false);
250        if($ok) {
251            $this->_say('<b>'.$this->getLang('cp_done').'</b>');
252            $this->_rmold();
253            $this->_say('<b>'.$this->getLang('finish').'</b>');
254        } else {
255            $this->_say('<b>'.$this->getLang('cp_fail').'</b>');
256        }
257        return $ok;
258    }
259
260    /**
261     * Delete outdated files
262     */
263    private function _rmold() {
264        $list = file($this->tgzdir.'data/deleted.files');
265        foreach($list as $line) {
266            $line = trim(preg_replace('/#.*$/', '', $line));
267            if(!$line) continue;
268            $file = DOKU_INC.$line;
269            if(!file_exists($file)) continue;
270            if((is_dir($file) && $this->_rdel($file)) ||
271                @unlink($file)
272            ) {
273                $this->_say($this->getLang('rm_done'), hsc($line));
274            } else {
275                $this->_say($this->getLang('rm_fail'), hsc($line));
276            }
277        }
278    }
279
280    /**
281     * Traverse over the given dir and compare it to the DokuWiki dir
282     *
283     * Checks what files need an update, tests for writability and copies
284     *
285     * @param string $dir
286     * @param bool $dryrun do not copy but only check permissions
287     * @return bool
288     */
289    private function _traverse($dir, $dryrun) {
290        $base = $this->tgzdir;
291        $ok   = true;
292
293        $dh = @opendir($base.'/'.$dir);
294        if(!$dh) return false;
295        while(($file = readdir($dh)) !== false) {
296            if($file == '.' || $file == '..') continue;
297            $from = "$base/$dir/$file";
298            $to   = DOKU_INC."$dir/$file";
299
300            if(is_dir($from)) {
301                if($dryrun) {
302                    // just check for writability
303                    if(!is_dir($to)) {
304                        if(is_dir(dirname($to)) && !is_writable(dirname($to))) {
305                            $this->_say('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file"));
306                            $ok = false;
307                        }
308                    }
309                }
310
311                // recursion
312                if(!$this->_traverse("$dir/$file", $dryrun)) {
313                    $ok = false;
314                }
315            } else {
316                $fmd5 = md5(@file_get_contents($from));
317                $tmd5 = md5(@file_get_contents($to));
318                if($fmd5 != $tmd5 || !file_exists($to)) {
319                    if($dryrun) {
320                        // just check for writability
321                        if((file_exists($to) && !is_writable($to)) ||
322                            (!file_exists($to) && is_dir(dirname($to)) && !is_writable(dirname($to)))
323                        ) {
324
325                            $this->_say('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file"));
326                            $ok = false;
327                        } else {
328                            $this->_say($this->getLang('tv_upd'), hsc("$dir/$file"));
329                        }
330                    } else {
331                        // check dir
332                        if(io_mkdir_p(dirname($to))) {
333                            // copy
334                            if(!copy($from, $to)) {
335                                $this->_say('<b>'.$this->getLang('tv_nocopy').'</b>', hsc("$dir/$file"));
336                                $ok = false;
337                            } else {
338                                $this->_say($this->getLang('tv_done'), hsc("$dir/$file"));
339                            }
340                        } else {
341                            $this->_say('<b>'.$this->getLang('tv_nodir').'</b>', hsc("$dir"));
342                            $ok = false;
343                        }
344                    }
345                }
346            }
347        }
348        closedir($dh);
349        return $ok;
350    }
351}
352
353// vim:ts=4:sw=4:et:enc=utf-8:
354