1<?php
2/**
3 * Indexmenu Admin Plugin:   Indexmenu Component.
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Samuele Tognini <samuele@samuele.netsons.org>
7 */
8
9if(!defined('DOKU_INC')) die();
10
11require_once(DOKU_PLUGIN."indexmenu/inc/pclzip.lib.php");
12
13if(!defined('INDEXMENU_IMG_ABSDIR')) define('INDEXMENU_IMG_ABSDIR', DOKU_PLUGIN."indexmenu/images");
14define('INDEXMENU_ICOS', 'base,folder,folderopen,folderh,folderhopen,page,plus,minus,nolines_plus,nolines_minus,minusbottom,plusbottom,join,joinbottom,line,empty');
15
16class admin_plugin_indexmenu extends DokuWiki_Admin_Plugin {
17    var $req = 'fetch';
18    var $repos = array(
19        "url"    => array(DOKU_URL),
20        "status" => array(""),
21    );
22
23    var $selected = -1;
24
25    /**
26     * return sort order for position in admin menu
27     */
28    function getMenuSort() {
29        return 999;
30    }
31
32    /**
33     * handle user request
34     */
35    function handle() {
36        $url = "http://samuele.netsons.org/dokuwiki";
37        if(empty($url)) {
38            $this->repos['url'][]     = $this->getLang('no_repos');
39            $this->repos['status'][]  = "disabled";
40            $this->repos['install'][] = -1;
41        } else {
42            $this->repos['url'] = array_merge($this->repos['url'], explode(',', $url));
43        }
44
45        if(!isset($_REQUEST['req'])) return; // first time - nothing to do
46        $this->req = $_REQUEST['req'];
47
48        if(is_numeric($_REQUEST['repo'])) $this->selected = $_REQUEST['repo'];
49    }
50
51    /**
52     * output appropriate html
53     */
54    function html() {
55        global $conf;
56        ptln('<div id="config__manager">');
57        ptln(' <h1>'.$this->getLang('menu').'</h1>');
58        ptln($this->_donate());
59        ptln(' <fieldset>');
60        ptln('   <legend>'.$this->getLang('checkupdates').'</legend>');
61        $this->_form_open("checkupdates");
62        $this->_form_close('check');
63        if($this->req == 'checkupdates') {
64            $this->_checkupdates();
65        }
66        ptln(' </fieldset>');
67        ptln(' <fieldset>');
68        ptln('   <legend>Themes</legend>');
69        ptln('   <table class="inline">');
70        ptln('   <tr class="default"><td class="label" colspan="2">');
71        ptln('   <span class="outkey">'.$this->getLang('infos').'</span>');
72        ptln('   </td></tr>');
73        $n = 0;
74        //cycles thru repositories urls
75        foreach($this->repos['url'] as $url) {
76            ptln('    <tr class="search_hit"><td>');
77            $legend = ($n == 0) ? $conf['title'] : $this->repos['url'][$n];
78            ptln('     <span><label><strong>'.$legend.'</strong></label></span>');
79            ptln('    </td>');
80            ptln('    <td class="value">');
81            $this->_form_open("fetch", $n);
82            $this->_form_close("fetch");
83            ptln('    </td></tr>');
84            //list requested theme
85            if($n == $this->selected) {
86                ptln('    <tr class="default"><td colspan="2">');
87                if($this->req == 'install') $this->install($this->selected, $_REQUEST['name']);
88                if($this->req == 'upload' && $_REQUEST['name']) {
89                    $info = "";
90                    if(isset($_REQUEST['author_info'])) {
91                        $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
92                        $info .= "author=".strtr($_REQUEST['author_info'], $obfuscate)."\n";
93                    }
94                    if(isset($_REQUEST['url_info'])) $info .= "url=".$_REQUEST['url_info']."\n";
95                    if(isset($_REQUEST['author_info'])) $info .= "description=".$_REQUEST['author_info'];
96                    if(!$this->upload($_REQUEST['name'], $info)) msg($this->getLang('install_no'), -1);
97                }
98                if($this->req == 'delete' && $_REQUEST['name']) $this->_delete($_REQUEST['name']);
99                ptln('    </td></tr><tr><td colspan="2">');
100                $this->dolist($n);
101                ptln('    </td></tr>');
102            }
103            $n++;
104        }
105        ptln('   </table>');
106        ptln('  </fieldset>');
107        ptln('</div>');
108    }
109
110    /**
111     * Connect to theme repository and list themes
112     *
113     * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
114     * @author     Samuele Tognini <samuele@samuele.netsons.org>
115     */
116    function dolist($n) {
117        global $INFO;
118        if($n === false) return;
119        //info.txt keys to parse
120        $keys = array('author', 'url', 'description');
121        $icos = explode(',', INDEXMENU_ICOS);
122        $turl = "";
123        $info = "";
124        //get list
125        $data = $this->_remotequery($this->repos['url'][$n]."/lib/plugins/indexmenu/ajax.php?req=local");
126        $data = explode(",", $data);
127        //print themes
128        for($i = 3; $i < count($data); $i++) {
129            $theme = $data[$i];
130            $turl  = $data[1].$data[2]."/".$theme;
131            ptln('     <em>'.$theme.'</em>');
132            ptln('    <div class="indexmenu_list_themes">');
133            ptln('     <div>');
134            //print images
135            foreach(array_slice($icos, 0, 8) as $ico) {
136                $ext = explode(".", $theme);
137                $ext = array_pop($ext);
138                $ext = ($ext == $theme) ? '.gif' : ".$ext";
139                ptln('      <img src="'.$turl."/".$ico.$ext.'" title="'.$ico.'" alt="'.$ico.'" />');
140            }
141            ptln('      </div>');
142            //get theme info.txt
143            if($info = $this->_remotequery($turl."/info.txt", false)) {
144                foreach($keys as $key) {
145                    if(!preg_match('/'.$key.'=(.*)/', $info, $out)) continue;
146                    ptln("       <div>");
147                    ptln("       <strong>".hsc($key).": </strong>".hsc($out[1]));
148                    ptln("       </div>");
149                }
150            }
151            if($n == 0) {
152                $act = "upload";
153                if($theme != "default") {
154                    $this->_form_open("delete", $n);
155                    ptln('       <input type="hidden" name="name" value="'.$theme.'" />');
156                    $this->_form_close("delete");
157                }
158            } else {
159                $act = "install";
160                ptln('      <a href="'.$this->repos['url'][$n]."/lib/plugins/indexmenu/ajax.php?req=send&amp;t=".$theme.'">Download</a>');
161            }
162            $this->_form_open($act, $n);
163            if($n == 0 && !is_file(INDEXMENU_IMG_ABSDIR."/".$theme."/info.txt")) {
164                ptln('       <div><strong>author:</strong><input type="text" name="author_info" value="'.$INFO["userinfo"]["name"].hsc(" <".$INFO["userinfo"]["mail"].">").'" size="50" maxlength="100" /><br />');
165                ptln('       <strong>url:</strong><input type="text" name="url_info" value="'.$this->repos['url'][$n].'" size="50" maxlength="200" /><br />');
166                ptln('       <strong>description:</strong><input type="text" name="description_info" value="" size="50" maxlength="200" /></div>');
167            }
168            ptln('       <input type="hidden" name="name" value="'.$theme.'" />');
169            $this->_form_close($act);
170            ptln('     <br /><br /></div>');
171        }
172    }
173
174    /**
175     * Download and install themes
176     *
177     * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
178     * @author     Samuele Tognini <samuele@samuele.netsons.org>
179     */
180    function install($n, $name) {
181        $repo = $this->repos['url'][$n];
182        if(!isset($name)) return false;
183        $return = true;
184        if(!$absdir = $this->checktmpsubdir()) return false;
185        $tmp = $absdir."/tmp";
186
187        //send theme list request
188        if(!$zipfile = io_download($repo."/lib/plugins/indexmenu/ajax.php?req=send&t=".$name, "$tmp/", true)) {
189            msg($this->getLang('down_err').": $name", -1);
190            $return = false;
191        } else {
192            //create zip
193            $zip    = new PclZip("$tmp/$zipfile");
194            $regexp = "/^".$name."\/(info.txt)|(style.css)|(".str_replace(",", "|", INDEXMENU_ICOS).")\.(gif|png|jpg)$/i";
195            $status = $zip->extract(PCLZIP_OPT_PATH, $absdir."/", PCLZIP_OPT_BY_PREG, $regexp);
196            //error
197            if($status == 0) {
198                msg($this->getLang('zip_err')." $tmp/$zipfile: ".$zip->errorName(true), -1);
199                $return = false;
200            } else {
201                msg("<strong>$name</strong> ".$this->getLang('install_ok'), 1);
202            }
203        }
204        //clean tmp
205        $this->_rm_dir($tmp);
206        return $return;
207    }
208
209    /**
210     * Remove a directory
211     *
212     */
213    function _rm_dir($path) {
214        if(!is_string($path) || $path == "") return false;
215
216        if(is_dir($path)) {
217            if(!$dh = @opendir($path)) return false;
218
219            while($f = readdir($dh)) {
220                if($f == '..' || $f == '.') continue;
221                $this->_rm_dir("$path/$f");
222            }
223
224            closedir($dh);
225            return @rmdir($path);
226        } else {
227            return @unlink($path);
228        }
229    }
230
231    /**
232     * Retrive and create themes tmp directory
233     *
234     * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
235     * @author     Samuele Tognini <samuele@samuele.netsons.org>
236     */
237    function checktmpsubdir() {
238        $tmp = INDEXMENU_IMG_ABSDIR."/tmp";
239        if(!io_mkdir_p($tmp)) {
240            msg($this->getLang('dir_err').": $tmp", -1);
241            return false;
242        }
243        return INDEXMENU_IMG_ABSDIR;
244    }
245
246    /**
247     * Upload a theme into my site
248     *
249     * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
250     * @author     Samuele Tognini <samuele@samuele.netsons.org>
251     */
252    function upload($theme, $info) {
253        $return = true;
254        $host   = 'samuele.netsons.org';
255        $path   = '/dokuwiki/lib/plugins/indexmenu/upload/index.php';
256        //TODO: merge zip creation with that in ajax.php (create a class?)
257        if(!$absdir = $this->checktmpsubdir()) return false;
258        $tmp      = $absdir."/tmp";
259        $zipfile  = "$theme.zip";
260        $filelist = "$absdir/$theme";
261        //create info
262        if(!empty($info)) {
263            io_savefile("$tmp/$theme/info.txt", $info);
264            $filelist .= ",$tmp/$theme";
265        }
266        //create zip
267        $zip    = new PclZip("$tmp/$zipfile");
268        $status = $zip->create($filelist, PCLZIP_OPT_REMOVE_ALL_PATH);
269        if($status == 0) {
270            //error
271            msg($this->getLang('zip_err').": ".$zip->errorName(true), -1);
272            $return = false;
273        } else {
274            //prepare POST headers.
275            $boundary = "---------------------------".uniqid("");
276            $data     = join("", file("$tmp/$zipfile"));
277            $header   = "POST $path HTTP/1.0\r\n";
278            $header .= "Host: $host\r\n";
279            $header .= "User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061024 Iceweasel/2.0 (Debian-2.0+dfsg-1)\r\n";
280            $header .= "Content-type: multipart/form-data, boundary=$boundary\r\n";
281            $body = "--".$boundary."\r\n";
282            $body .= "Content-Disposition: form-data; name=\"userfile\"; filename=\"$zipfile\"\r\n";
283            $body .= "Content-Type: application/x-zip-compressed\r\n\r\n";
284            $body .= $data."\r\n";
285            $body .= "--".$boundary."\r\n";
286            $body .= "Content-Disposition: form-data; name=\"upload\"\r\n\r\n";
287            $body .= "Upload\r\n";
288            $body .= "--".$boundary."--\r\n";
289            $header .= "Content-Length: ".strlen($body)."\r\n\r\n";
290
291            //connect and send zip
292            if($fp = fsockopen($host, 80)) {
293                fwrite($fp, $header.$body);
294                //reply
295                $buf = "";
296                while(!feof($fp)) {
297                    $buf .= fgets($fp, 3200);
298                }
299                fclose($fp);
300                //parse resply
301                if(preg_match("/<!--indexmenu-->(.*)<!--\/indexmenu-->/s", $buf, $match)) {
302                    $str = substr($match[1], 4, 7);
303                    switch($str) {
304                        case "ERROR  ":
305                            $mesg_type = -1;
306                            break;
307                        case "SUCCESS":
308                            $mesg_type = 1;
309                            break;
310                        default:
311                            $mesg_type = 2;
312                    }
313                    msg($match[1], $mesg_type);
314                } else {
315                    $return = false;
316                }
317            } else {
318                $return = false;
319            }
320        }
321
322        $this->_rm_dir($tmp);
323        return $return;
324    }
325
326    /**
327     * Check for new messages from upstream
328     *
329     * @author Samuele Tognini <samuele@samuele.netsons.org>
330     */
331    function _checkupdates() {
332        require_once (DOKU_INC.'inc/HTTPClient.php');
333        global $conf;
334        global $INFO;
335        $w    = -1;
336        $date = $this->getInfo('date');
337        $date = $date['date'];
338        $data = $this->_remotequery("http://samuele.netsons.org/dokuwiki/lib/plugins/indexmenu/remote.php?check=$date");
339        if($data === "") {
340            msg($this->getLang('noupdates'), 1);
341            $data .= @preg_replace('/\n\n.*$/s', '', @io_readFile(DOKU_PLUGIN.'indexmenu/changelog'))."\n%\n";
342            $w = 1;
343        } else {
344            $data = preg_replace('/\<br(\s*)?\/?\>/i', "", $data);
345            $data = preg_replace('/\t/', " ", $data);
346        }
347        $data = preg_replace('/\[\[(?!(http|https))(.:)(.*?)\]\]/s', "[[plugin:$3]]", $data);
348        $data = preg_replace('/\[\[(?!(http|https))(.*?)\]\]/s', "[[http://www.dokuwiki.org/$2]]", $data);
349        $msgs = explode("\n%\n", $data);
350        foreach($msgs as $msg) {
351            if($msg) {
352                $msg = p_render('xhtml', p_get_instructions($msg), $info);
353                msg($msg, $w);
354            }
355        }
356    }
357
358    /**
359     * Get url response and check it
360     *
361     * @author Samuele Tognini <samuele@samuele.netsons.org>
362     */
363    function _remotequery($url, $tag = true) {
364        require_once (DOKU_INC.'inc/HTTPClient.php');
365        $http          = new DokuHTTPClient();
366        $http->timeout = 8;
367        $data          = $http->get($url);
368        if($tag) {
369            if($data === false) {
370                msg($this->getLang('conn_err'), -1);
371            } else {
372                (substr($data, 0, 9) === "indexmenu") ? $data = substr($data, 9) : $data = "";
373            }
374        }
375        return $data;
376    }
377
378    /**
379     * Open an html form
380     *
381     * @author Samuele Tognini <samuele@samuele.netsons.org>
382     */
383    function _form_open($act, $n = -1) {
384        global $ID;
385        ptln('     <form action="'.wl($ID).'" method="post">');
386        ptln('      <input type="hidden" name="do" value="admin" />');
387        ptln('      <input type="hidden" name="page" value="'.$this->getPluginName().'" />');
388        ptln('      <input type="hidden" name="req" value="'.$act.'" />');
389        ptln('      <input type="hidden" name="repo" value="'.$n.'" />');
390    }
391
392    /**
393     * Close the html form
394     *
395     * @author Samuele Tognini <samuele@samuele.netsons.org>
396     */
397    function _form_close($act) {
398        ptln('      <input type="submit" name="btn" '.$this->repos['status'][$n].' value="'.$this->getLang($act).'" />');
399        ptln('     </form>');
400    }
401
402    /**
403     * Remove an installed theme
404     *
405     * @author Samuele Tognini <samuele@samuele.netsons.org>
406     */
407    function _delete($theme) {
408        if($theme == "default") return;
409        if($this->_rm_dir(INDEXMENU_IMG_ABSDIR."/".utf8_encodeFN(basename($theme)))) {
410            msg($this->getLang('delete_ok').": $theme.", 1);
411        } else {
412            msg($this->getLang('delete_no').": $theme.", -1);
413        }
414    }
415
416    /**
417     * Print the donate button.
418     *
419     * @author Samuele Tognini <samuele@samuele.netsons.org>
420     */
421    function _donate() {
422        $out = "<fieldset>\n";
423        $out .= '<p>'.$this->getLang('donation_text').'</p>';
424        $out .= '<form action="https://www.paypal.com/cgi-bin/webscr" method="post">'."\n";
425        $out .= '<input type="hidden" name="cmd" value="_s-xclick" />'."\n";
426        $out .= '<input type="hidden" name="hosted_button_id" value="102873" />'."\n";
427        $out .= '<input type="image" src="https://www.paypal.com/en_US/i/btn/btn_donateCC_LG.gif" name="submit" alt="" />'."\n";
428        $out .= '<img alt="" src="https://www.paypal.com/it_IT/i/scr/pixel.gif" width="1" height="1" />'."\n";
429        $out .= "</form></fieldset>\n";
430        return $out;
431    }
432
433}
434