From 1f01f37d4b7e80b30d8f5d8f9479080353352fe8 Mon Sep 17 00:00:00 2001
From: chris <chris@teacherscpd.co.uk>
Date: Thu, 25 Aug 2005 01:22:55 +0200
Subject: [PATCH] plugin manager, an admin plugin

darcs-hash:20050824232255-50fdc-b5eb608833470cea87d457e029f530b916431b61.gz
---
 lib/plugins/plugin/admin.php                | 686 ++++++++++++++++++++
 lib/plugins/plugin/inc/tarlib.class.php     | 677 +++++++++++++++++++
 lib/plugins/plugin/inc/zip.lib.php          | 353 ++++++++++
 lib/plugins/plugin/lang/en/admin_plugin.txt |   7 +
 lib/plugins/plugin/lang/en/lang.php         |  58 ++
 lib/plugins/plugin/style.css                |  49 ++
 6 files changed, 1830 insertions(+)
 create mode 100644 lib/plugins/plugin/admin.php
 create mode 100644 lib/plugins/plugin/inc/tarlib.class.php
 create mode 100644 lib/plugins/plugin/inc/zip.lib.php
 create mode 100644 lib/plugins/plugin/lang/en/admin_plugin.txt
 create mode 100644 lib/plugins/plugin/lang/en/lang.php
 create mode 100644 lib/plugins/plugin/style.css

diff --git a/lib/plugins/plugin/admin.php b/lib/plugins/plugin/admin.php
new file mode 100644
index 000000000..536dc525b
--- /dev/null
+++ b/lib/plugins/plugin/admin.php
@@ -0,0 +1,686 @@
+<?php
+/**
+ * Plugin management functions
+ *
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author     Christopher Smith <chris@jalakai.co.uk>
+ */
+      
+// todo
+// - maintain a history of file modified
+// - allow a plugin to contain extras to be copied to the current template (extra/tpl/)
+// - to images (lib/images/) [ not needed, should go in lib/plugin/images/ ]
+     
+if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
+if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
+require_once(DOKU_PLUGIN.'admin.php');
+     
+    // language stuff here for now ... move to language files when complete
+    
+//    global $lang;
+    
+    //--------------------------[ GLOBALS ]------------------------------------------------
+    // note: probably should be dokuwiki wide globals, where it can be access by pluginutils.php
+    global $common_plugin_files, $common_plugin_types;
+    $common_plugin_types = array('syntax', 'admin');
+    $common_plugin_files = array("style.css", "screen.css", "print.css", "script.js");
+
+/**
+ * All DokuWiki plugins to extend the admin function
+ * need to inherit from this class
+ */
+class admin_plugin_plugin extends DokuWiki_Admin_Plugin {
+
+    var $disabled = 0;
+    var $plugin = '';
+    var $cmd = '';
+    var $handler = NULL;
+    
+    var $functions = array('delete','update','settings','info');  // require a plugin name
+    var $commands = array('manage','refresh','download');         // don't require a plugin name
+    var $plugin_list = array();
+    
+    var $msg = '';
+    var $error = '';
+    
+    function admin_plugin_plugin() {
+      global $conf;
+      $this->disabled = (!isset($conf['pluginmanager']) || ($conf['pluginmanager'] == 0));
+    }
+ 
+    /**
+     * return some info
+     */
+    function getInfo(){
+      $disabled = ($this->disabled) ? '(disabled)' : '';
+      
+      return array(
+        'author' => 'Christopher Smith',
+        'email'  => 'chris@jalakai.co.uk',
+        'date'   => '2005-08-10',
+        'name'   => 'Plugin Manager',
+        'desc'   => "Manage Plugins, including automated plugin installer $disabled",
+        'url'    => 'http://wiki.splitbrain.org/plugin:adminplugin',
+      );
+    }
+ 
+    /**
+     * return prompt for admin menu
+     */
+    function getMenuText($language) {
+        if (!$this->disabled) 
+          return parent::getMenuText($language);
+
+        return '';
+    }
+ 
+    /**
+     * return sort order for position in admin menu
+     */
+    function getMenuSort() {
+      return 20;
+    }
+ 
+    /**
+     * handle user request
+     */
+    function handle() {
+         global $ID, $lang;
+      
+      if ($this->disabled) return;
+      
+      $this->plugin = $_REQUEST['plugin'];
+      $this->cmd = $_REQUEST['fn'];
+      if (is_array($this->cmd)) $this->cmd = key($this->cmd);
+      
+      sort($this->plugin_list = plugin_list());
+      
+      // verify $_REQUEST vars
+      if (in_array($this->cmd, $this->commands)) {
+        $this->plugin = '';
+      } else if (!in_array($this->cmd, $this->functions) || !in_array($this->plugin, $this->plugin_list)) {
+        $this->cmd = 'manage';
+        $this->plugin = '';
+      }
+      
+      // create object to handle the command
+      $class = "admin_plugin_".$this->cmd;
+      if (!class_exists($class)) $class = 'admin_plugin_manage';
+      
+      $this->handler = & new $class($this, $plugin);
+      $this->msg = $this->handler->process();
+    }
+ 
+    /**
+     * output appropriate html
+     */
+    function html() {
+      
+      if ($this->disabled) return;
+
+      // enable direct access to language strings
+//      if (!$this->localised) $this->setupLocale();
+      $this->setupLocale();
+      
+      if ($this->handler === NULL) $this->handler = & new admin_plugin_manage();
+      if (!$this->plugin_list) sort($this->plugin_list = plugin_list());
+      
+      ptln('<div id="plugin_manager">');
+      $this->handler->html();  
+      ptln('</div><!-- #plugin_manager -->');
+    }
+    
+}
+
+class admin_plugin_manage {
+    
+        var $manager = NULL;
+        var $lang = array();
+        var $plugin = '';
+        var $downloaded = array();
+        
+        function admin_plugin_manage(&$manager, $plugin) {
+            $this->manager = & $manager;
+            $this->plugin = $plugin;
+            $this->lang = & $manager->lang;            
+        }
+
+        function process() {
+            return '';
+        }
+        
+        function html() {
+          
+          print $this->manager->plugin_locale_xhtml('admin_plugin');
+          
+          // FIXME, these probably shouldn't be here any more!
+          if (!$this->manager->msg) $this->manager->msg = '&nbsp;';
+          ptln("<p>{$manager->msg}</p>");
+          
+          if ($this->manager->error) {
+            ptln("<p class='error'>".str_replace("\n","<br>",$this->manager->error)."</p>");
+          }
+          
+          $this->html_menu();          
+        }
+        
+        // build our standard menu
+        function html_menu($listPlugins = true) {
+          global $ID;
+        
+          ptln('<div class="pm_menu">');
+          
+          ptln('<div class="common">');
+          ptln('  <form action="'.wl($ID).'" method="post">');
+          ptln('    <fieldset class="hidden">',4);
+          ptln('      <input type="hidden" name="do"   value="admin" />');
+          ptln('      <input type="hidden" name="page" value="plugin" />');
+          ptln('    </fieldset>');
+          ptln('    <fieldset>');
+          ptln('      <legend>'.$this->lang['refresh'].'</legend>');
+          ptln('      <h3 class="legend">'.$this->lang['refresh'].'</h3>');
+          ptln('      <input type="submit" class="button" name="fn[refresh]" value="'.$this->lang['btn_refresh'].'" />');
+          ptln('      <p>'.$this->lang['refresh_x'].'</p>');
+          ptln('    </fieldset>');
+          ptln('    <fieldset>');
+          ptln('      <legend>'.$this->lang['download'].'</legend>');
+          ptln('      <h3 class="legend">'.$this->lang['download'].'</h3>');
+          ptln('      <input type="submit" class="button" name="fn[download]" value="'.$this->lang['btn_download'].'" />');
+          ptln('      <label for="url">'.$this->lang['url'].'<input name="url" id="url" class="field" type="text" maxlength="200" /></label>');
+          ptln('    </fieldset>');
+          ptln('  </form>');
+          ptln('</div>');
+          
+          if ($listPlugins) {
+            ptln('<h2>'.$this->lang['manage'].'</h2>');
+            ptln('<div class="plugins">');
+            $this->html_pluginlist();
+            ptln('</div>');
+          }
+                              
+          ptln('</div>');
+        }
+            
+        function html_pluginlist() {
+
+          foreach ($this->manager->plugin_list as $plugin) {
+          
+            $new = (in_array($plugin, $this->downloaded)) ? ' class="new"' : '';
+            
+            ptln('  <form action="'.wl($ID).'" method="post" '.$new.'>');
+            ptln('    <fieldset>');
+            ptln('      <legend>'.$plugin.'</legend>');
+            ptln('      <h3 class="legend">'.$plugin.'</h3>');
+            ptln('      <input type="hidden" name="do"     value="admin" />');
+            ptln('      <input type="hidden" name="page"   value="plugin" />');
+            ptln('      <input type="hidden" name="plugin" value="'.$plugin.'" />');
+            
+            $this->html_button('delete', false, 6);
+            $this->html_button('update', !$this->plugin_readlog($plugin, 'url'), 6);
+            $this->html_button('settings', !@file_exists(DOKU_PLUGIN.$plugin.'/settings.php'), 6);
+            $this->html_button('info', false, 6);
+            
+            ptln('    </fieldset>');
+            ptln('  </form>');
+            }
+        }
+        
+        function html_button($btn, $disabled=false, $indent=0) {
+            $disabled = ($disabled) ? 'disabled="disabled"' : '';
+            ptln('<input type="submit" class="button" '.$disabled.' name="fn['.$btn.']" value="'.$this->lang['btn_'.$btn].'" />',$indent);
+        }
+        
+        /**
+         *  Rebuild aggregated files & update latest plugin date
+         */
+        function refresh() {
+            global $lang;
+            global $common_plugin_files;
+            
+            sort($this->manager->plugin_list = plugin_list());
+            
+            foreach ($common_plugin_files as $file) {
+              $aggregate = '';
+              
+              // could replace with an class/object based aggregator, 
+              // that way special files could have their own aggregator
+              foreach ($this->manager->plugin_list as $plugin) {
+                if (@file_exists(DOKU_PLUGIN."$plugin/$file")) {
+                    $contents = @file_get_contents(DOKU_PLUGIN."$plugin/$file")."\n";
+                    
+                    // url conversion for css files
+                    if (is_css($file)) {
+                      $contents = preg_replace('/(url\([\'\"]?)([^\/](?![a-zA-Z0-9]+:\/\/).*?)([\'\"]?\))/','$1'.$plugin.'/$2$3',$contents); 
+                    }
+                    
+                    $aggregate .= $contents;
+                }
+              }
+              
+              if (trim($aggregate)) {
+                if (!io_savefile(DOKU_PLUGIN."plugin_$file", $aggregate)) {
+                  $this->manager->error .= sprintf($this->lang['error_write'],$file);
+                }
+              }
+            }
+            
+            // update latest plugin date - FIXME            
+            return (!$this->manager->error);
+        }
+        
+        // log 
+        function plugin_writelog($plugin, $cmd, $data) {
+        
+            $file = DOKU_PLUGIN.$plugin.'/manager.dat';
+            
+            switch ($cmd) {
+              case 'install' :
+                $url = $data[0];
+                $date = date('r');
+                if (!$fp = @fopen($file, 'w')) return;
+                fwrite($fp, "installed=$date\nurl=$url\n");
+                fclose($fp);
+                break;
+                
+              case 'update' :
+                $date = date('r');
+                if (!$fp = @fopen($file, 'w+')) return;
+                fwrite($fp, "updated=$date\n");
+                fclose($fp);
+                break;
+            }
+        }
+        
+        function plugin_readlog($plugin, $field) {
+            static $log = array();
+            $file = DOKU_PLUGIN.$plugin.'/manager.dat';
+            
+            if (!isset($log[$plugin])) {
+                $tmp = @file_get_contents($file);
+                if (!$tmp) return '';
+                $log[$plugin] = & $tmp;
+            }
+            
+            if ($field == 'ALL') {
+                return $log[$plugin];
+            }
+            
+            if (preg_match_all('/'.$field.'=(.*)$/m',$log[$plugin], $match=array()))
+                return implode("\n", $match[1]);
+            
+            return '';
+        }
+    }
+    
+    class admin_plugin_refresh extends admin_plugin_manage {
+    
+        function process() {
+            $this->refresh();
+            
+            if (!$this->manager->error) return $this->lang['refreshed'];
+        }
+        
+        function html() {
+        
+            parent::html();
+            
+            ptln('<div class="pm_info">');
+            ptln('<h2>'.$this->lang['refreshing'].'</h2>');
+            ptln('<p>'.$this->lang['refreshed'].'</p>');
+            ptln('</div>');
+        }
+        
+    }
+    
+    class admin_plugin_download extends admin_plugin_manage {
+    
+        var $overwrite = false;
+        
+        function process() {
+          global $lang, $conf;
+          
+          $plugin_url = $_REQUEST['url'];
+          if (!preg_match("/[^\/]*$/", $plugin_url, $matches = array()) || !$matches[0]) {
+            $this->manager->error = $this->lang['error_badurl'].'\n';
+            return '';
+          }
+          
+          $file = $matches[0];
+          $folder = "p".md5($file.date('r'));     // tmp folder name - will be empty (should really make sure it doesn't already exist)
+          $tmp = DOKU_PLUGIN."tmp/$folder";
+          
+          if (!$this->manager->error && !ap_mkdir($tmp)) {
+            $this->manager->error = $this->lang['error_dir_create'].'\n';
+            $folder = '';
+          }
+          
+          if (!$this->manager->error && !io_download($plugin_url, "$tmp/$file")) {
+            $this->manager->error = sprintf($this->lang['error_download'],$url)."\n";
+          }
+    
+          ap_decompress("$tmp/$file", $tmp);
+          
+          // search tmp/$folder for the directory that has been created
+          // move that directory to lib/plugins/
+          if ($dh = @opendir("$tmp/")) {
+              while (false !== ($f = readdir($dh))) {
+                if ($f == '.' || $f == '..' || $f == 'tmp') continue;
+                if (!is_dir("$tmp/$f")) continue;
+                
+                // check to make sure we aren't overwriting anything
+                if (file_exists(DOKU_PLUGIN."/$f")) {
+                   // remember our settings, ask the user to confirm overwrite, FIXME
+                   continue;
+                } 
+                
+                ap_copy("$tmp/$f", DOKU_PLUGIN.$f);
+                $this->downloaded[] = $f;
+                $this->plugin_writelog($f, 'install', array($plugin_url));
+              }        
+            closedir($dh);
+          }
+          
+          // cleanup
+          if ($folder && is_dir(DOKU_PLUGIN."tmp/$folder")) ap_delete(DOKU_PLUGIN."tmp/$folder");
+          
+          if (!$this->manager->error) {
+              $this->refresh();
+          }
+
+          return '';
+        }
+        
+        function html() {
+            parent::html();
+            
+            ptln('<div class="pm_info">');
+            ptln('<h2>'.$this->lang['downloading'].'</h2>');
+            
+            if ($this->manager->error) {
+                ptln('<p class="error">'.$this->manager->error.'</p>');
+            } else if (count($this->downloaded) == 1) {
+                ptln('<p>'.sprintf($this->lang['downloaded'],$this->downloaded[0]).'</p>');
+            } else if (count($this->downloaded)) {   // more than one plugin in the download
+                ptln('<p>'.$this->lang['downloads'].'</p>');
+                ptln('<ul>');
+                foreach ($this->downloaded as $plugin) {
+                    ptln('<li>'.$plugin.'</li>',2);
+                }
+                ptln('</ul>');
+            } else {        // none found in download
+                ptln('<p>'.$this->lang['download_none'].'</p>');
+            }
+            ptln('</div>');
+        }
+        
+    }
+    
+    class admin_plugin_delete extends admin_plugin_manage {
+    
+        function process() {    
+        
+            $deleted = $this->manager->plugin;
+            ap_delete(DOKU_PLUGIN.$deleted);
+            $this->plugin = '';
+            
+            $this->refresh();
+            return "Plugin $deleted deleted";
+        }
+    }
+    
+    class admin_plugin_info extends admin_plugin_manage {
+    
+        var $plugin_info = array();        // the plugin itself
+        var $details = array();            // any component plugins
+
+        function process() { 
+        
+          // sanity check
+          if (!$this->manager->plugin) { return; }
+          
+          $component_list = ap_plugin_components($this->manager->plugin);
+          usort($component_list, ap_component_sort);
+          
+          foreach ($component_list as $component) {
+              if ($obj = & plugin_load($component['type'],$component['name']) === NULL) continue;
+            
+            $this->details[] = array_merge($obj->getInfo(), array('type' => $component['type']));
+            unset($obj);
+          }
+          
+          // review details to simplify things
+          foreach($this->details as $info) {
+            foreach($info as $item => $value) {
+              if (!isset($this->plugin_info[$item])) { $this->plugin_info[$item] = $value; continue; }
+              if ($this->plugin_info[$item] != $value) $this->plugin_info[$item] = '';
+            }    
+          }        
+        } 
+        
+        function html() {
+          
+          // output the standard menu stuff
+          parent::html();
+          
+          // sanity check
+          if (!$this->manager->plugin) { return; }
+                              
+          ptln('<div class="pm_info">');
+          ptln("<h2>Plugin: {$this->manager->plugin}</h2>");
+
+          // collect pertinent information from the log
+          $installed = $this->plugin_readlog($this->manager->plugin, 'installed');
+          $source = $this->plugin_readlog($this->manager->plugin, 'url');          
+          $updated = substr(strrchr("\n".$this->plugin_readlog($this->manager->plugin, 'updated'), '\n'), 1);
+          
+          ptln("<dl>",2);
+          ptln("<dt>".$this->manager->getLang('installed').'</dt><dd>'.($installed ? $installed : $this->manager->getLang('unknown'))."</dd>",4);
+          if ($updated) ptln("<dt>".$this->manager->getLang('lastupdate').'</dt><dd>'.$updated."</dd>",4);
+          ptln("<dt>".$this->manager->getLang('source').'</dt><dd>'.($source ? $source : $this->manager->getLang('unknown'))."</dd>",4);
+          ptln("</dl>",2);
+                    
+          if (count($this->details) == 0) {
+              ptln("<p>This plugin returned no information, it may be invalid.</p>",2);
+          } else {
+          
+            ptln("<dl>",2);
+            if ($this->plugin_info['name']) ptln("<dt>Name</dt><dd>".$this->out($this->plugin_info['name'])."</dd>",4);
+            if ($this->plugin_info['type']) ptln("<dt>Type</dt><dd>".$this->out($this->plugin_info['type'])."</dd>",4);
+            if ($this->plugin_info['desc']) ptln("<dt>Description</dt><dd>".$this->out($this->plugin_info['desc'])."</dd>",4);
+            if ($this->plugin_info['author']) ptln("<dt>Author</dt><dd>".$this->manager->plugin_email($this->plugin_info['email'], $this->plugin_info['author'])."</dd>",4);
+            if ($this->plugin_info['url']) ptln("<dt>Web</dt><dd>".$this->manager->plugin_link($this->plugin_info['url'], '', 'urlextern')."</dd>",4);
+            ptln("</dl>",2);
+          
+            if (count($this->details) > 1) {
+              ptln("<h3>Components</h3>",2);
+              ptln("<div>",2);
+          
+              foreach ($this->details as $info) {
+            
+                  ptln("<dl>",4);
+                if (!$this->plugin_info['name']) ptln("<dt>Name</dt><dd>".$this->out($info['name'])."</dd>",6);            
+                if (!$this->plugin_info['type']) ptln("<dt>Type</dt><dd>".$this->out($info['type'])."</dd>",6);
+                if (!$this->plugin_info['desc']) ptln("<dt>Description</dt><dd>".$this->out($info['desc'])."</dd>",6);
+                if (!$this->plugin_info['author']) ptln("<dt>Author</dt><dd>".$this->manager->plugin_email($info['email'], $info['author'])."</dd>",6);
+                if (!$this->plugin_info['url']) ptln("<dt>Web</dt><dd>".$this->manager->plugin_link($info['url'], '', 'urlextern')."</dd>",6);
+                ptln("</dl>",4);
+          
+              }
+                ptln("</div>",2);
+            }
+          }
+          ptln("</div>");
+        }
+        
+        // simple output filter, make html entities safe and convert new lines to <br />
+        function out($text) {
+            return str_replace("\n",'<br />',htmlentities($text));
+        }
+        
+    }
+    
+    //--------------[ to do ]---------------------------------------
+    class admin_plugin_update extends admin_plugin_manage {
+    
+        function html() {
+            parent::html();
+            
+            ptln('<div class="pm_info">');
+            ptln('<h2>'.$this->lang['updating'].'</h2>');
+            
+            if ($this->manager->error) {
+                ptln('<p class="error">'.$this->manager->error.'</p>');
+            } else if (count($this->downloaded) == 1) {
+                ptln('<p>'.sprintf($this->lang['downloaded'],$this->downloaded[0]).'</p>');
+            } else if (count($this->downloaded)) {   // more than one plugin in the download
+                ptln('<p>'.$this->lang['downloads'].'</p>');
+                ptln('<ul>');
+                foreach ($this->downloaded as $plugin) {
+                    ptln('<li>'.$plugin.'</li>',2);
+                }
+                ptln('</ul>');
+            } else {        // none found in download
+                ptln('<p>'.$this->lang['download_none'].'</p>');
+            }
+            ptln('<p>Under Construction</p>');
+            ptln('</div>');
+        }
+    }
+    class admin_plugin_settings extends admin_plugin_manage {}
+    
+    //--------------[ utilities ]-----------------------------------
+    
+    function is_css($f) { return (substr($f, -4) == '.css'); }
+    
+    // generate an admin plugin href 
+    function apl($pl, $fn) { return wl($ID,"do=admin&amp;page=plugin".($pl?"&amp;plugin=$pl":"").($fn?"&amp;fn=$fn":"")); }
+    
+    // generate a complete admin plugin link (may change to button)
+    function ap_link($pl, $fn, $txt) {
+      return '<a href="'.apl($pl, $fn).'">['.$txt.']</a>';
+    }
+    
+    // decompress wrapper
+    function ap_decompress($file, $target) {
+    
+        // decompression library doesn't like target folders ending in "/"
+        if (substr($target, -1) == "/") $target = substr($target, 0, -1);
+        
+        // .tar, .tar.bz, .tar.gz
+        if (preg_match("/\.tar(\.bz2?|\.gz)?$/", $file)) {
+          
+          require_once(DOKU_PLUGIN."plugin/inc/tarlib.class.php");
+          
+          $tar = new CompTar($file, COMPRESS_DETECT);
+          $ok = $tar->Extract(FULL_ARCHIVE, $target, '', 0777);
+        
+          // sort something out for handling tar error messages meaningfully  
+          if ($ok<0) ptln("<p>tar error:".$tar->TarErrorStr($ok)."</p>");
+          return ($ok<0?false:true);
+        }
+        
+        if (substr($file, -4) == ".zip") {    
+    
+          require_once(DOKU_PLUGIN."plugin/inc/zip.lib.php");
+          
+          $zip = new zip();
+          $ok = $zip->Extract($file, $target);
+          
+          // sort something out for handling zip error messages meaningfully  
+          if ($ok==-1) ptln("<p>zip error:</p>");            
+          return ($ok==-1?false:true);
+        }
+        
+        if (substr($file, -4) == ".rar") {
+          // not yet supported -- fix me
+          return false;
+        }
+        
+        // unsupported file type
+        return false;
+    }
+    
+    // possibly should use io_MakeFileDir, not sure about using its method of error handling
+    function ap_mkdir($d) {
+        global $conf;
+        
+        umask($conf['dmask']);
+        $ok = io_mkdir_p($d);
+        umask($conf['umask']);
+        return $ok;
+    }
+    
+    // copy with recursive sub-directory support
+    function ap_copy($src, $dst) {
+    
+        if (is_dir($src)) {
+          if (!$dh = @opendir($src)) return false;
+    
+          if ($ok = ap_mkdir($dst)) {      
+            while ($ok && $f = readdir($dh)) {
+              if ($f == '..' || $f == '.') continue;
+              $ok = ap_copy("$src/$f", "$dst/$f");
+            }
+          }        
+          
+          closedir($dh);
+          return $ok;
+          
+        } else {
+            if (!@copy($src,$dst)) return false;
+            touch($dst,filemtime($src));
+        }
+        
+        return true;
+    }
+    
+    // delete, with recursive sub-directory support
+    function ap_delete($path) {
+    
+        if (!is_string($path) || $path == "") return;
+    
+        if (is_dir($path)) {
+          if (!$dh = @opendir($path)) return;
+    
+          while ($f = readdir($dh)) {
+            if ($f == '..' || $f == '.') continue;
+            ap_delete("$path/$f");
+          }
+          
+          closedir($dh);
+          rmdir($path);
+          return;
+          
+        } else {
+          unlink($path);
+        }
+    } 
+    
+    // return a list (name & type) of all the component plugins that make up this plugin
+    // can this move to pluginutils?
+    function ap_plugin_components($plugin) {
+
+      global $common_plugin_types;
+      
+      $components = array();
+      $path = DOKU_PLUGIN.$plugin.'/';
+      
+      foreach ($common_plugin_types as $type) {
+          if (file_exists($path.$type.'.php')) { $components[] = array('name'=>$plugin, 'type'=>$type); continue; }
+        
+        if ($dh = @opendir($path.$type.'/')) {
+          while (false !== ($cp = readdir($dh))) {
+            if ($cp == '.' || $cp == '..' || strtolower(substr($cp,-4)) != '.php') continue;
+            $components[] = array('name'=>$plugin.'_'.substr($cp, 0, -4), 'type'=>$type);
+          }
+          closedir($dh);
+        }        
+      }
+      return $components;
+    }
+    
+    function ap_component_sort($a, $b) {
+        if ($a['name'] == $b['name']) return 0;
+        return ($a['name'] < $b['name']) ? -1 : 1;
+    }
+
diff --git a/lib/plugins/plugin/inc/tarlib.class.php b/lib/plugins/plugin/inc/tarlib.class.php
new file mode 100644
index 000000000..cda8254d5
--- /dev/null
+++ b/lib/plugins/plugin/inc/tarlib.class.php
@@ -0,0 +1,677 @@
+<?php
+
+/*
+ +---------------------------------------------+
+ |   TAR format class - Creates TAR archives   |
+ +---------------------------------------------+
+ |   This class is part or the MaxgComp suite  |
+ +---------------------------------------------+
+ |   Created by the Maxg Network (maxg.info)   |
+ |  http://docs.maxg.info for help & license.  |
+ +---------------------------------------------+
+ | Author: Bouchon <tarlib@bouchon.org> (Maxg) |
+ +---------------------------------------------+
+*  Modified for Dokuwiki
+*  @author    Christopher Smith <chris@jalakai.co.uk>
+*/
+
+define('COMPRESS_GZIP',1);
+define('COMPRESS_BZIP',2);
+define('COMPRESS_AUTO',3);
+define('COMPRESS_NONE',0);
+
+define('TARLIB_VERSION','1.2');
+define('FULL_ARCHIVE',-1);
+
+define('ARCHIVE_DYNAMIC',0);
+define('ARCHIVE_RENAMECOMP',5);
+define('COMPRESS_DETECT',-1);
+
+class CompTar
+{
+  var $_comptype;
+  var $_compzlevel;
+  var $_fp;
+  var $_memdat;
+  var $_nomf;
+  var $_result;
+
+  function CompTar($p_filen = ARCHIVE_DYNAMIC , $p_comptype = COMPRESS_AUTO, $p_complevel = 9)
+  {
+    $this->_nomf = $p_filen; $flag=0;
+    if($p_comptype && $p_comptype % 5 == 0){$p_comptype /= ARCHIVE_RENAMECOMP; $flag=1;}
+
+    if($p_complevel > 0 && $p_complevel <= 9) $this->_compzlevel = $p_complevel;
+    else $p_complevel = 9;
+
+    if($p_comptype == COMPRESS_DETECT)
+    {
+      if(strtolower(substr($p_filen,-3)) == '.gz') $p_comptype = COMPRESS_GZIP;
+//    elseif(strtolower(substr($p_filen,-4)) == '.bz2') $p_comptype = COMPRESS_BZIP;  -- CS
+      elseif(preg_match( "/\.bz2?$/", $p_filen)) $p_comptype = COMPRESS_BZIP;  //--CS  bz or bz2
+      else $p_comptype = COMPRESS_NONE;
+    }
+
+    switch($p_comptype)
+    {
+      case COMPRESS_GZIP:
+        if(!extension_loaded('zlib')) $this->_result = -1;
+        $this->_comptype = COMPRESS_GZIP;
+      break;
+
+      case COMPRESS_BZIP:
+        if(!extension_loaded('bz2')) $this->_result = -2;
+        $this->_comptype = COMPRESS_BZIP;
+      break;
+
+      case COMPRESS_AUTO:
+        if(extension_loaded('zlib'))
+          $this->_comptype = COMPRESS_GZIP;
+        elseif(extension_loaded('bz2'))
+          $this->_comptype = COMPRESS_BZIP;
+        else
+          $this->_comptype = COMPRESS_NONE;
+      break;
+
+      default:
+        $this->_comptype = COMPRESS_NONE;
+    }
+
+    if($this->_result < 0) $this->_comptype = COMPRESS_NONE;
+
+    if($flag) $this->_nomf.= '.'.$this->getCompression(1);
+    $this->_result = true;
+  }
+
+  function setArchive($p_name='', $p_comp = COMPRESS_AUTO, $p_level=9)
+  {
+    $this->_CompTar();
+    $this->CompTar($p_name, $p_comp, $p_level);
+    return $this->_result;
+  }
+
+  function getCompression($ext = false)
+  {
+    $exts = Array('tar','tar.gz','tar.bz2');
+    if($ext) return $exts[$this->_comptype];
+    return $this->_comptype;
+  }
+
+  function setCompression($p_comp = COMPRESS_AUTO)
+  {
+    $this->setArchive($this->_nomf, $p_comp, $this->_compzlevel);
+    return $this->_compzlevel;
+  }
+
+  function getDynamicArchive()
+  {
+    return $this->_encode($this->_memdat);
+  }
+
+  function writeArchive($p_archive)
+  {
+    if(!$this->_memdat) return -7;
+    $fp = @fopen($p_archive, 'wb');
+    if(!$fp) return -6;
+
+    fwrite($fp, $this->_memdat);
+    fclose($fp);
+
+    return true;
+  }
+
+  function sendClient($name = '', $archive = '', $headers = TRUE)
+  {
+    if(!$name && !$this->_nomf) return -9;
+    if(!$archive && !$this->_memdat) return -10;
+    if(!$name) $name = basename($this->_nomf);
+
+    if($archive){ if(!file_exists($archive)) return -11; }
+    else $decoded = $this->getDynamicArchive();
+
+    if($headers)
+    {
+      header('Content-Type: application/x-gtar');
+      header('Content-Disposition: attachment; filename='.basename($name));
+      header('Accept-Ranges: bytes');
+      header('Content-Length: '.($archive ? filesize($archive) : strlen($decoded)));
+    }
+
+    if($archive)
+    {
+      $fp = @fopen($archive,'rb');
+      if(!$fp) return -4;
+
+      while(!foef($fp)) echo fread($fp,2048);
+    }
+    else
+    {
+      echo $decoded;
+    }
+
+    return true;
+  }
+
+  function Extract($p_what = FULL_ARCHIVE, $p_to = '.', $p_remdir='', $p_mode = 0755)
+  {
+    if(!$this->_OpenRead()) return -4;
+//  if(!@is_dir($p_to)) if(!@mkdir($p_to, 0777)) return -8;   --CS
+    if(!@is_dir($p_to)) if(!$this->_dirApp($p_to)) return -8;   //--CS (route through correct dir fn)
+
+    $ok = $this->_extractList($p_to, $p_what, $p_remdir, $p_mode);
+    $this->_CompTar();
+
+    return $ok;
+  }
+
+  function Create($p_filelist,$p_add='',$p_rem='')
+  {
+    if(!$fl = $this->_fetchFilelist($p_filelist)) return -5;
+    if(!$this->_OpenWrite()) return -6;
+
+    $ok = $this->_addFileList($fl,$p_add,$p_rem);
+
+    if($ok) $this->_writeFooter();
+    else{ $this->_CompTar(); @unlink($this->_nomf); }
+
+    return $ok;
+  }
+
+  function Add($p_filelist, $p_add = '', $p_rem = '')
+  {
+    if (($this->_nomf != ARCHIVE_DYNAMIC && @is_file($this->_nomf)) || ($this->_nomf == ARCHIVE_DYNAMIC && !$this->_memdat))
+      return $this->Create($p_filelist, $p_add, $p_rem);
+
+    if(!$fl = $this->_fetchFilelist($p_filelist)) return -5;
+    return $this->_append($fl, $p_add, $p_rem);
+  }
+
+  function ListContents()
+  {
+    if(!$this->_nomf) return -3;
+    if(!$this->_OpenRead()) return -4;
+
+    $result = Array();
+
+    while ($dat = $this->_read(512))
+    {
+      $dat = $this->_readHeader($dat);
+      if(!is_array($dat)) continue;
+
+      $this->_seek(ceil($dat['size']/512)*512,1);
+      $result[] = $dat;
+    }
+
+    return  $result;
+  }
+
+  function TarErrorStr($i)
+  {
+    $ecodes = Array(
+         1 => TRUE,
+         0 => "Undocumented error",
+        -1 => "Can't use COMPRESS_GZIP compression : ZLIB extensions are not loaded !",
+        -2 => "Can't use COMPRESS_BZIP compression : BZ2 extensions are not loaded !",
+        -3 => "You must set a archive file to read the contents !",
+        -4 => "Can't open the archive file for read !",
+        -5 => "Invalide file list !",
+        -6 => "Can't open the archive in write mode !",
+        -7 => "There is no ARCHIVE_DYNAMIC to write !",
+        -8 => "Can't create the directory to extract files !",
+        -9 => "Please pass a archive name to send if you made created an ARCHIVE_DYNAMIC !",
+       -10 => "You didn't pass an archive filename and there is no stored ARCHIVE_DYNAMIC !",
+       -11 => "Given archive doesn't exist !"
+    );
+
+    return isset($ecodes[$i]) ? $ecodes[$i] : $ecodes[0];
+  }
+
+  function TarInfo($headers = true)
+  {
+    if($headers)
+    {
+    ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+
+<head>
+  <title>MaxgComp TAR</title>
+  <style type="text/css">
+   body{margin: 20px;}
+   body,td{font-size:10pt;font-family: arial;}
+  </style>
+  <meta name="Author" content="The Maxg Network, http://maxg.info" />
+  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
+</head>
+
+<body bgcolor="#EFEFEF">
+<?php
+    }
+?>
+<table border="0" align="center" width="500" cellspacing="4" cellpadding="5" style="border:1px dotted black;">
+<tr>
+  <td align="center" bgcolor="#DFDFEF" colspan="3" style="font-size:15pt;font-color:#330000;border:1px solid black;">MaxgComp TAR</td>
+</tr>
+<tr>
+  <td colspan="2" bgcolor="#EFEFFE" style="border:1px solid black;">This software was created by the Maxg Network, <a href="http://maxg.info" target="_blank" style="text-decoration:none;color:#333366;">http://maxg.info</a>
+   <br />It is distributed under the GNU <a href="http://www.gnu.org/copyleft/lesser.html" target="_blank" style="text-decoration:none;color:#333366;">Lesser General Public License</a>
+   <br />You can find the documentation of this class <a href="http://docs.maxg.info" target="_blank" style="text-decoration:none;color:#333366;">here</a></td>
+   <td width="60" bgcolor="#EFEFFE" style="border:1px solid black;" align="center"><img src="http://img.maxg.info/menu/tar.gif" border="0" alt="MaxgComp TAR" /></td>
+</tr>
+<tr>
+  <td width="50%" align="center" style="border:1px solid black;" bgcolor="#DFDFEF">MaxgComp TAR version</td>
+  <td colspan="2" align="center" bgcolor="#EFEFFE" style="border:1px solid black;"><?=TARLIB_VERSION?></td>
+</tr>
+<tr>
+  <td width="50%" align="center" style="border:1px solid black;" bgcolor="#DFDFEF">ZLIB extensions</td>
+  <td colspan="2" align="center" bgcolor="#EFEFFE" style="border:1px solid black;"><?=(extension_loaded('zlib') ? '<b>Yes</b>' : '<i>No</i>')?></td>
+</tr>
+<tr>
+  <td width="50%" align="center" style="border:1px solid black;" bgcolor="#DFDFEF">BZ2 extensions</td>
+  <td colspan="2" align="center" bgcolor="#EFEFFE" style="border:1px solid black;"><?=(extension_loaded('bz2') ? '<b>Yes</b>' : '<i>No</i>')?></td>
+</tr>
+<tr>
+  <td width="50%" align="center" style="border:1px solid black;" bgcolor="#DFDFEF">Allow URL fopen</td>
+  <td colspan="2" align="center" bgcolor="#EFEFFE" style="border:1px solid black;"><?=(ini_get('allow_url_fopen') ? '<b>Yes</b>' : '<i>No</i>')?></td>
+</tr>
+<tr>
+  <td width="50%" align="center" style="border:1px solid black;" bgcolor="#DFDFEF">Time limit</td>
+  <td colspan="2" align="center" bgcolor="#EFEFFE" style="border:1px solid black;"><?=ini_get('max_execution_time')?></td>
+</tr>
+<tr>
+  <td width="50%" align="center" style="border:1px solid black;" bgcolor="#DFDFEF">PHP Version</td>
+  <td colspan="2" align="center" bgcolor="#EFEFFE" style="border:1px solid black;"><?=phpversion()?></td>
+</tr>
+<tr>
+  <td colspan="3" align="center" bgcolor="#EFEFFE" style="border:1px solid black;">
+    <i>Special thanks to &laquo; Vincent Blavet &raquo; for his PEAR::Archive_Tar class</i>
+  </td>
+</tr>
+</table>
+<?php
+    if($headers) echo '</body></html>';
+  }
+
+  function _seek($p_flen, $tell=0)
+  {
+    if($this->_nomf === ARCHIVE_DYNAMIC)
+      $this->_memdat=substr($this->_memdat,0,($tell ? strlen($this->_memdat) : 0) + $p_flen);
+    elseif($this->_comptype == COMPRESS_GZIP)
+      @gzseek($this->_fp, ($tell ? @gztell($this->_fp) : 0)+$p_flen);
+    elseif($this->_comptype == COMPRESS_BZIP)
+      @fseek($this->_fp, ($tell ? @ftell($this->_fp) : 0)+$p_flen);
+    else
+      @fseek($this->_fp, ($tell ? @ftell($this->_fp) : 0)+$p_flen);
+  }
+
+  function _OpenRead()
+  {
+    if($this->_comptype == COMPRESS_GZIP)
+      $this->_fp = @gzopen($this->_nomf, 'rb');
+    elseif($this->_comptype == COMPRESS_BZIP)
+      $this->_fp = @bzopen($this->_nomf, 'rb');
+    else
+      $this->_fp = @fopen($this->_nomf, 'rb');
+
+    return ($this->_fp ? true : false);
+  }
+
+  function _OpenWrite($add = 'w')
+  {
+    if($this->_nomf === ARCHIVE_DYNAMIC) return true;
+
+    if($this->_comptype == COMPRESS_GZIP)
+      $this->_fp = @gzopen($this->_nomf, $add.'b'.$this->_compzlevel);
+    elseif($this->_comptype == COMPRESS_BZIP)
+      $this->_fp = @bzopen($this->_nomf, $add.'b');
+    else
+      $this->_fp = @fopen($this->_nomf, $add.'b');
+
+    return ($this->_fp ? true : false);
+  }
+
+  function _CompTar()
+  {
+    if($this->_nomf === ARCHIVE_DYNAMIC || !$this->_fp) return;
+
+    if($this->_comptype == COMPRESS_GZIP) @gzclose($this->_fp);
+    elseif($this->_comptype == COMPRESS_BZIP) @bzclose($this->_fp);
+    else @fclose($this->_fp);
+  }
+
+  function _read($p_len)
+  {
+    if($this->_comptype == COMPRESS_GZIP)
+      return @gzread($this->_fp,$p_len);
+    elseif($this->_comptype == COMPRESS_BZIP)
+      return @bzread($this->_fp,$p_len);
+    else
+      return @fread($this->_fp,$p_len);
+  }
+
+  function _write($p_data)
+  {
+    if($this->_nomf === ARCHIVE_DYNAMIC) $this->_memdat .= $p_data;
+    elseif($this->_comptype == COMPRESS_GZIP)
+      return @gzwrite($this->_fp,$p_data);
+
+    elseif($this->_comptype == COMPRESS_BZIP)
+      return @bzwrite($this->_fp,$p_data);
+
+    else
+      return @fwrite($this->_fp,$p_data);
+  }
+
+  function _encode($p_dat)
+  {
+    if($this->_comptype == COMPRESS_GZIP)
+      return gzencode($p_dat, $this->_compzlevel);
+    elseif($this->_comptype == COMPRESS_BZIP)
+      return bzcompress($p_dat, $this->_compzlevel);
+    else return $p_dat;
+  }
+
+  function _readHeader($p_dat)
+  {
+    if (!$p_dat || strlen($p_dat) != 512) return false;
+
+    for ($i=0, $chks=0; $i<148; $i++)
+      $chks += ord($p_dat[$i]);
+
+    for ($i=156,$chks+=256; $i<512; $i++)
+      $chks += ord($p_dat[$i]);
+
+    $headers = @unpack("a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor", $p_dat);
+    if(!$headers) return false;
+
+    $return['checksum'] = OctDec(trim($headers['checksum']));
+    if ($return['checksum'] != $chks) return false;
+
+    $return['filename'] = trim($headers['filename']);
+    $return['mode'] = OctDec(trim($headers['mode']));
+    $return['uid'] = OctDec(trim($headers['uid']));
+    $return['gid'] = OctDec(trim($headers['gid']));
+    $return['size'] = OctDec(trim($headers['size']));
+    $return['mtime'] = OctDec(trim($headers['mtime']));
+    $return['typeflag'] = $headers['typeflag'];
+    $return['link'] = trim($headers['link']);
+    $return['uname'] = trim($headers['uname']);
+    $return['gname'] = trim($headers['gname']);
+
+    return $return;
+  }
+
+  function _fetchFilelist($p_filelist)
+  {
+    if(!$p_filelist || (is_array($p_filelist) && !@count($p_filelist))) return false;
+
+    if(is_string($p_filelist))
+    {
+        $p_filelist = explode('|',$p_filelist);
+        if(!is_array($p_filelist)) $p_filelist = Array($p_filelist);
+    }
+
+    return $p_filelist;
+  }
+
+  function _addFileList($p_fl, $p_addir, $p_remdir)
+  {
+    foreach($p_fl as $file)
+    {
+      if(($file == $this->_nomf && $this->_nomf != ARCHIVE_DYNAMIC) || !$file || (!file_exists($file) && !is_array($file)))
+        continue;
+
+      if (!$this->_addFile($file, $p_addir, $p_remdir))
+        continue;
+
+      if (@is_dir($file))
+      {
+        $d = @opendir($file);
+
+        if(!$d) continue;
+        readdir($d); readdir($d);
+
+        while($f = readdir($d))
+        {
+          if($file != ".") $tmplist[0] = "$file/$f";
+          else $tmplist[0] = $d;
+
+          $this->_addFileList($tmplist, $p_addir, $p_remdir);
+        }
+
+        closedir($d); unset($tmplist,$f);
+      }
+    }
+    return true;
+  }
+
+  function _addFile($p_fn, $p_addir = '', $p_remdir = '')
+  {
+    if(is_array($p_fn)) list($p_fn, $data) = $p_fn;
+    $sname = $p_fn;
+
+    if($p_remdir)
+    {
+        if(substr($p_remdir,-1) != '/') $p_remdir .= "/";
+
+        if(substr($sname, 0, strlen($p_remdir)) == $p_remdir)
+          $sname = substr($sname, strlen($p_remdir));
+    }
+
+    if($p_addir) $sname = $p_addir.(substr($p_addir,-1) == '/' ? '' : "/").$sname;
+
+    if(strlen($sname) > 99) return;
+
+    if(@is_dir($p_fn))
+    {
+      if(!$this->_writeFileHeader($p_fn, $sname)) return false;
+    }
+    else
+    {
+     if(!$data)
+     {
+      $fp = fopen($p_fn, 'rb');
+      if(!$fp) return false;
+     }
+
+     if(!$this->_writeFileHeader($p_fn, $sname, ($data ? strlen($data) : FALSE))) return false;
+
+     if(!$data)
+     {
+      while(!feof($fp))
+      {
+        $packed = pack("a512", fread($fp,512));
+        $this->_write($packed);
+      }
+      fclose($fp);
+     }
+     else
+     {
+      for($s = 0; $s < strlen($data); $s += 512)
+        $this->_write(pack("a512",substr($data,$s,512)));
+     }
+    }
+
+    return true;
+  }
+
+  function _writeFileHeader($p_file, $p_sname, $p_data=false)
+  {
+   if(!$p_data)
+   {
+    if (!$p_sname) $p_sname = $p_file;
+    $p_sname = $this->_pathTrans($p_sname);
+
+    $h_info = stat($p_file);
+    $h[0] = sprintf("%6s ", DecOct($h_info[4]));
+    $h[] = sprintf("%6s ", DecOct($h_info[5]));
+    $h[] = sprintf("%6s ", DecOct(fileperms($p_file)));
+    clearstatcache();
+    $h[] = sprintf("%11s ", DecOct(filesize($p_file)));
+    $h[] = sprintf("%11s", DecOct(filemtime($p_file)));
+
+    $dir = @is_dir($p_file) ? '5' : '';
+   }
+   else
+   {
+    $dir = '';
+    $p_data = sprintf("%11s ", DecOct($p_data));
+    $time = sprintf("%11s ", DecOct(time()));
+    $h = Array("     0 ","     0 "," 40777 ",$p_data,$time);
+   }
+
+    $data_first = pack("a100a8a8a8a12A12", $p_sname, $h[2], $h[0], $h[1], $h[3], $h[4]);
+    $data_last = pack("a1a100a6a2a32a32a8a8a155a12", $dir, '', '', '', '', '', '', '', '', "");
+
+     for ($i=0,$chks=0; $i<148; $i++)
+       $chks += ord($data_first[$i]);
+
+     for ($i=156, $chks+=256, $j=0; $i<512; $i++, $j++)
+       $chks += ord($data_last[$j]);
+
+     $this->_write($data_first);
+
+     $chks = pack("a8",sprintf("%6s ", DecOct($chks)));
+     $this->_write($chks.$data_last);
+
+     return true;
+  }
+
+  function _append($p_filelist, $p_addir="", $p_remdir="")
+  {
+    if(!$this->_fp) if(!$this->_OpenWrite('a')) return -6;
+
+    if($this->_nomf == ARCHIVE_DYNAMIC)
+    {
+      $s = strlen($this->_memdat);
+      $this->_memdat = substr($this->_memdat,0,-512);
+    }
+    else
+    {
+      $s = filesize($this->_nomf);
+      $this->_seek($s-512);
+    }
+
+    $ok = $this->_addFileList($p_filelist, $p_addir, $p_remdir);
+    $this->_writeFooter();
+
+    return $ok;
+  }
+
+  function _pathTrans($p_dir)
+  {
+    if ($p_dir)
+    {
+      $subf = explode("/", $p_dir); $r='';
+
+      for ($i=count($subf)-1; $i>=0; $i--)
+      {
+        if ($subf[$i] == ".") {}
+        else if ($subf[$i] == "..") $i--;
+        else if (!$subf[$i] && $i!=count($subf)-1 && $i) {}
+        else $r = $subf[$i].($i!=(count($subf)-1) ? "/".$r : "");
+      }
+    }
+    return $r;
+  }
+
+  function _writeFooter()
+  {
+    $this->_write(pack("a512", ""));
+  }
+
+  function _extractList($p_to, $p_files, $p_remdir, $p_mode = 0755)
+  {
+    if (!$p_to || ($p_to[0]!="/"&&substr($p_to,0,3)!="../"&&substr($p_to,1,3)!=":\\")) /*" // <- PHP Coder bug */
+      $p_to = "./$p_to";
+
+    if ($p_remdir && substr($p_remdir,-1)!='/') $p_remdir .= '/';
+    $p_remdirs = strlen($p_remdir);
+    while($dat = $this->_read(512))
+    {
+      $headers = $this->_readHeader($dat);
+      if(!$headers['filename']) continue;
+
+      if($p_files == -1 || $p_files[0] == -1) $extract = true;
+      else
+      {
+        $extract = false;
+
+        foreach($p_files as $f)
+        {
+          if(substr($f,-1) == "/") {
+            if((strlen($headers['filename']) > strlen($f)) && (substr($headers['filename'],0,strlen($f))==$f)) {
+              $extract = true; break;
+            }
+          }
+          elseif($f == $headers['filename']) {
+            $extract = true; break;
+          }
+        }
+      }
+
+      if ($extract)
+      {
+        $det[] = $headers;
+        if ($p_remdir && substr($headers['filename'],0,$p_remdirs)==$p_remdir)
+          $headers['filename'] = substr($headers['filename'],$p_remdirs);
+
+        if($headers['filename'].'/' == $p_remdir && $headers['typeflag']=='5') continue;
+
+        if ($p_to != "./" && $p_to != "/")
+        {
+          while($p_to{-1}=="/") $p_to = substr($p_to,0,-1);
+
+          if($headers['filename']{0} == "/")
+            $headers['filename'] = $p_to.$headers['filename'];
+          else
+            $headers['filename'] = $p_to."/".$headers['filename'];
+        }
+
+        $ok = $this->_dirApp($headers['typeflag']=="5" ? $headers['filename'] : dirname($headers['filename']));
+        if($ok < 0) return $ok;
+
+        if (!$headers['typeflag'])
+        {
+          if (!$fp = @fopen($headers['filename'], "wb")) return -6;
+          $n = floor($headers['size']/512);
+
+          for ($i=0; $i<$n; $i++) fwrite($fp, $this->_read(512),512);
+          if (($headers['size'] % 512) != 0) fwrite($fp, $this->_read(512), $headers['size'] % 512);
+
+          fclose($fp);
+          touch($headers['filename'], $headers['mtime']);
+          chmod($headers['filename'], $p_mode);
+        }
+       else
+       {
+         $this->_seek(ceil($headers['size']/512)*512,1);
+       }
+      }else $this->_seek(ceil($headers['size']/512)*512,1);
+    }
+    return $det;
+  }
+
+function _dirApp($d)
+  {
+//  map to dokuwiki function (its more robust)
+    return ap_mkdir($d);  
+/*
+    $d = explode('/', $d);
+    $base = '';
+
+    foreach($d as $f)
+    {
+      if(!is_dir($base.$f))
+      {
+        $ok = @mkdir($base.$f, 0777);
+        if(!$ok) return false;
+      }
+      $base .= "$f/";
+    }
+*/    
+  }
+
+}
+
diff --git a/lib/plugins/plugin/inc/zip.lib.php b/lib/plugins/plugin/inc/zip.lib.php
new file mode 100644
index 000000000..351b68e70
--- /dev/null
+++ b/lib/plugins/plugin/inc/zip.lib.php
@@ -0,0 +1,353 @@
+<?php
+
+/**
+ * @author     bouchon
+ * @link       http://dev.maxg.info
+ * @link       http://forum.maxg.info
+ *
+ *  Modified for Dokuwiki
+ *  @author    Christopher Smith <chris@jalakai.co.uk>
+ */
+
+class zip
+{
+
+ var $datasec, $ctrl_dir = array();
+ var $eof_ctrl_dir = "\x50\x4b\x05\x06\x00\x00\x00\x00";
+ var $old_offset = 0; var $dirs = Array(".");
+
+ function get_List($zip_name)
+ {
+   $zip = @fopen($zip_name, 'rb');
+   if(!$zip) return(0);
+   $centd = $this->ReadCentralDir($zip,$zip_name);
+
+    @rewind($zip);
+    @fseek($zip, $centd['offset']);
+
+   for ($i=0; $i<$centd['entries']; $i++)
+   {
+    $header = $this->ReadCentralFileHeaders($zip);
+    $header['index'] = $i;$info['filename'] = $header['filename'];
+    $info['stored_filename'] = $header['stored_filename'];
+    $info['size'] = $header['size'];$info['compressed_size']=$header['compressed_size'];
+    $info['crc'] = strtoupper(dechex( $header['crc'] ));
+    $info['mtime'] = $header['mtime']; $info['comment'] = $header['comment'];
+    $info['folder'] = ($header['external']==0x41FF0010||$header['external']==16)?1:0;
+    $info['index'] = $header['index'];$info['status'] = $header['status'];
+    $ret[]=$info; unset($header);
+   }
+  return $ret;
+ }
+
+ function Add($files,$compact)
+ {
+  if(!is_array($files[0])) $files=Array($files);
+
+  for($i=0;$files[$i];$i++){
+    $fn = $files[$i];
+    if(!in_Array(dirname($fn[0]),$this->dirs))
+     $this->add_Dir(dirname($fn[0]));
+    if(basename($fn[0]))
+     $ret[basename($fn[0])]=$this->add_File($fn[1],$fn[0],$compact);
+  }
+  return $ret;
+ }
+
+ function get_file()
+ {
+   $data = implode('', $this -> datasec);
+   $ctrldir = implode('', $this -> ctrl_dir);
+
+   return $data . $ctrldir . $this -> eof_ctrl_dir .
+    pack('v', sizeof($this -> ctrl_dir)).pack('v', sizeof($this -> ctrl_dir)).
+    pack('V', strlen($ctrldir)) . pack('V', strlen($data)) . "\x00\x00";
+ }
+
+ function add_dir($name) 
+ { 
+   $name = str_replace("\\", "/", $name); 
+   $fr = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00"; 
+
+   $fr .= pack("V",0).pack("V",0).pack("V",0).pack("v", strlen($name) ); 
+   $fr .= pack("v", 0 ).$name.pack("V", 0).pack("V", 0).pack("V", 0); 
+   $this -> datasec[] = $fr;
+
+   $new_offset = strlen(implode("", $this->datasec)); 
+
+   $cdrec = "\x50\x4b\x01\x02\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00"; 
+   $cdrec .= pack("V",0).pack("V",0).pack("V",0).pack("v", strlen($name) ); 
+   $cdrec .= pack("v", 0 ).pack("v", 0 ).pack("v", 0 ).pack("v", 0 ); 
+   $ext = "\xff\xff\xff\xff"; 
+   $cdrec .= pack("V", 16 ).pack("V", $this -> old_offset ).$name; 
+
+   $this -> ctrl_dir[] = $cdrec; 
+   $this -> old_offset = $new_offset; 
+   $this -> dirs[] = $name;
+ }
+
+ function add_File($data, $name, $compact = 1)
+ {
+   $name     = str_replace('\\', '/', $name);
+   $dtime    = dechex($this->DosTime());
+
+   $hexdtime = '\x' . $dtime[6] . $dtime[7].'\x'.$dtime[4] . $dtime[5]
+     . '\x' . $dtime[2] . $dtime[3].'\x'.$dtime[0].$dtime[1];
+   eval('$hexdtime = "' . $hexdtime . '";');
+
+   if($compact)
+   $fr = "\x50\x4b\x03\x04\x14\x00\x00\x00\x08\x00".$hexdtime;
+   else $fr = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00".$hexdtime;
+   $unc_len = strlen($data); $crc = crc32($data);
+
+   if($compact){
+     $zdata = gzcompress($data); $c_len = strlen($zdata);
+     $zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2);
+   }else{
+     $zdata = $data;
+   }
+   $c_len=strlen($zdata);
+   $fr .= pack('V', $crc).pack('V', $c_len).pack('V', $unc_len);
+   $fr .= pack('v', strlen($name)).pack('v', 0).$name.$zdata;
+
+   $fr .= pack('V', $crc).pack('V', $c_len).pack('V', $unc_len);
+
+   $this -> datasec[] = $fr;
+   $new_offset        = strlen(implode('', $this->datasec));
+   if($compact)
+        $cdrec = "\x50\x4b\x01\x02\x00\x00\x14\x00\x00\x00\x08\x00";
+   else $cdrec = "\x50\x4b\x01\x02\x14\x00\x0a\x00\x00\x00\x00\x00";
+   $cdrec .= $hexdtime.pack('V', $crc).pack('V', $c_len).pack('V', $unc_len);
+   $cdrec .= pack('v', strlen($name) ).pack('v', 0 ).pack('v', 0 );
+   $cdrec .= pack('v', 0 ).pack('v', 0 ).pack('V', 32 );
+   $cdrec .= pack('V', $this -> old_offset );
+
+   $this -> old_offset = $new_offset;
+   $cdrec .= $name;
+   $this -> ctrl_dir[] = $cdrec;
+   return true;
+ }
+
+ function DosTime() {
+   $timearray = getdate();
+   if ($timearray['year'] < 1980) {
+     $timearray['year'] = 1980; $timearray['mon'] = 1;
+     $timearray['mday'] = 1; $timearray['hours'] = 0;
+     $timearray['minutes'] = 0; $timearray['seconds'] = 0;
+   }
+   return (($timearray['year'] - 1980) << 25) | ($timearray['mon'] << 21) |     ($timearray['mday'] << 16) | ($timearray['hours'] << 11) | 
+    ($timearray['minutes'] << 5) | ($timearray['seconds'] >> 1);
+ }
+
+ function Extract ( $zn, $to, $index = Array(-1) )
+ {
+   if(!@is_dir($to)) @mkdir($to,0777);
+   $ok = 0; $zip = @fopen($zn,'rb');
+   if(!$zip) return(-1);
+   $cdir = $this->ReadCentralDir($zip,$zn);
+   $pos_entry = $cdir['offset'];
+
+   if(!is_array($index)){ $index = array($index);  }
+   for($i=0; $index[$i];$i++){
+     if(intval($index[$i])!=$index[$i]||$index[$i]>$cdir['entries'])
+      return(-1);
+   }
+
+   for ($i=0; $i<$cdir['entries']; $i++)
+   {
+     @fseek($zip, $pos_entry);
+     $header = $this->ReadCentralFileHeaders($zip);
+     $header['index'] = $i; $pos_entry = ftell($zip);
+     @rewind($zip); fseek($zip, $header['offset']);
+     if(in_array("-1",$index)||in_array($i,$index))
+      $stat[$header['filename']]=$this->ExtractFile($header, $to, $zip);
+      
+   }
+   fclose($zip);
+   return $stat;
+ }
+
+  function ReadFileHeader($zip)
+  {
+    $binary_data = fread($zip, 30);
+    $data = unpack('vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', $binary_data);
+
+    $header['filename'] = fread($zip, $data['filename_len']);
+    if ($data['extra_len'] != 0) {
+      $header['extra'] = fread($zip, $data['extra_len']);
+    } else { $header['extra'] = ''; }
+
+    $header['compression'] = $data['compression'];$header['size'] = $data['size'];
+    $header['compressed_size'] = $data['compressed_size'];
+    $header['crc'] = $data['crc']; $header['flag'] = $data['flag'];
+    $header['mdate'] = $data['mdate'];$header['mtime'] = $data['mtime'];
+
+    if ($header['mdate'] && $header['mtime']){
+     $hour=($header['mtime']&0xF800)>>11;$minute=($header['mtime']&0x07E0)>>5;
+     $seconde=($header['mtime']&0x001F)*2;$year=(($header['mdate']&0xFE00)>>9)+1980;
+     $month=($header['mdate']&0x01E0)>>5;$day=$header['mdate']&0x001F;
+     $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year);
+    }else{$header['mtime'] = time();}
+
+    $header['stored_filename'] = $header['filename'];
+    $header['status'] = "ok";
+    return $header;
+  }
+
+ function ReadCentralFileHeaders($zip){
+    $binary_data = fread($zip, 46);
+    $header = unpack('vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', $binary_data);
+
+    if ($header['filename_len'] != 0)
+      $header['filename'] = fread($zip,$header['filename_len']);
+    else $header['filename'] = '';
+
+    if ($header['extra_len'] != 0)
+      $header['extra'] = fread($zip, $header['extra_len']);
+    else $header['extra'] = '';
+
+    if ($header['comment_len'] != 0)
+      $header['comment'] = fread($zip, $header['comment_len']);
+    else $header['comment'] = '';
+
+    if ($header['mdate'] && $header['mtime'])
+    {
+      $hour = ($header['mtime'] & 0xF800) >> 11;
+      $minute = ($header['mtime'] & 0x07E0) >> 5;
+      $seconde = ($header['mtime'] & 0x001F)*2;
+      $year = (($header['mdate'] & 0xFE00) >> 9) + 1980;
+      $month = ($header['mdate'] & 0x01E0) >> 5;
+      $day = $header['mdate'] & 0x001F;
+      $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year);
+    } else {
+      $header['mtime'] = time();
+    }
+    $header['stored_filename'] = $header['filename'];
+    $header['status'] = 'ok';
+    if (substr($header['filename'], -1) == '/')
+      $header['external'] = 0x41FF0010;
+    return $header;
+ }
+
+ function ReadCentralDir($zip,$zip_name)
+ {
+  $size = filesize($zip_name);
+  if ($size < 277) $maximum_size = $size;
+  else $maximum_size=277;
+
+  @fseek($zip, $size-$maximum_size);
+  $pos = ftell($zip); $bytes = 0x00000000;
+
+  while ($pos < $size)
+  {
+    $byte = @fread($zip, 1); $bytes=($bytes << 8) | Ord($byte);
+    if ($bytes == 0x504b0506){ $pos++; break; } $pos++;
+  }
+
+ $data=unpack('vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size',
+        fread($zip, 18));
+
+  if ($data['comment_size'] != 0)
+    $centd['comment'] = fread($zip, $data['comment_size']);
+    else $centd['comment'] = ''; $centd['entries'] = $data['entries'];
+  $centd['disk_entries'] = $data['disk_entries'];
+  $centd['offset'] = $data['offset'];$centd['disk_start'] = $data['disk_start'];
+  $centd['size'] = $data['size'];  $centd['disk'] = $data['disk'];
+  return $centd;
+ }
+
+ function ExtractFile($header,$to,$zip)
+ {
+   $header = $this->readfileheader($zip);
+
+   if(substr($to,-1)!="/") $to.="/";
+   if(substr($header['filename'],-1)=="/")
+   {
+//    @mkdir($to.$header['filename']);   --CS
+    $this->_mkdir($to.$header['filename']);  //-- CS
+    return +2;
+   }
+
+//   $pth = explode("/",dirname($header['filename']));
+//   for($i=0,$tmp="";isset($pth[$i]);$i++){
+//     if(!$pth[$i]) continue;
+//     if(!is_dir($to.$tmp.$pth[$i])) @mkdir($to.$pth[$i],0777);
+//     $tmp.=$pth[$i]."/";
+//   }
+  if (!$this->_mkdir($to.dirname($header['filename']))) return (-1);   //--CS
+
+  if (!($header['external']==0x41FF0010)&&!($header['external']==16))
+  {
+   if ($header['compression']==0)
+   {
+    $fp = @fopen($to.$header['filename'], 'wb');
+    if(!$fp) return(-1);
+    $size = $header['compressed_size'];
+
+    while ($size != 0)
+    {
+      $read_size = ($size < 2048 ? $size : 2048);
+      $buffer = fread($zip, $read_size);
+      $binary_data = pack('a'.$read_size, $buffer);
+      @fwrite($fp, $binary_data, $read_size);
+      $size -= $read_size;
+    }
+    fclose($fp);
+    touch($to.$header['filename'], $header['mtime']);
+
+  }else{
+   if (!is_dir(dirname($to.$header['filename']))) $this->_mkdir(dirname($to.$header['filename']));  //-CS
+   $fp = fopen($to.$header['filename'].'.gz','wb');
+   if(!$fp) return(-1);
+   $binary_data = pack('va1a1Va1a1', 0x8b1f, Chr($header['compression']),
+     Chr(0x00), time(), Chr(0x00), Chr(3));
+
+   fwrite($fp, $binary_data, 10);
+   $size = $header['compressed_size'];
+
+   while ($size != 0)
+   {
+     $read_size = ($size < 1024 ? $size : 1024);
+     $buffer = fread($zip, $read_size);
+     $binary_data = pack('a'.$read_size, $buffer);
+     @fwrite($fp, $binary_data, $read_size);
+     $size -= $read_size;
+   }
+
+   $binary_data = pack('VV', $header['crc'], $header['size']);
+   fwrite($fp, $binary_data,8); fclose($fp);
+
+   $gzp = @gzopen($to.$header['filename'].'.gz','rb');
+    if(!$gzp){
+       @gzclose($gzp); @unlink($to.$header['filename']);
+       die("Archive is compressed whereas ZLIB is not enabled.");
+    }
+   $fp = @fopen($to.$header['filename'],'wb');
+   if(!$fp) return(-1);
+   $size = $header['size'];
+
+   while ($size != 0)
+   {
+     $read_size = ($size < 2048 ? $size : 2048);
+     $buffer = gzread($gzp, $read_size);
+     $binary_data = pack('a'.$read_size, $buffer);
+     @fwrite($fp, $binary_data, $read_size);
+     $size -= $read_size;
+   }
+   fclose($fp); gzclose($gzp);
+
+   touch($to.$header['filename'], $header['mtime']);
+   @unlink($to.$header['filename'].'.gz');
+
+  }}
+  return true;
+ }
+ 
+ //--CS start
+ // centralize mkdir calls and use dokuwiki io functions  
+ function _mkdir($d) {
+    return ap_mkdir($d);  
+ }
+ //--CS end
+}
+
diff --git a/lib/plugins/plugin/lang/en/admin_plugin.txt b/lib/plugins/plugin/lang/en/admin_plugin.txt
new file mode 100644
index 000000000..45a93ca45
--- /dev/null
+++ b/lib/plugins/plugin/lang/en/admin_plugin.txt
@@ -0,0 +1,7 @@
+====== Plugin Management ======
+
+On this page you can manage everything to do with Dokuwiki [[doku>wiki:plugins|plugins]].
+To be able to download and install a plugin your plugin folder must
+be writeable by the webserver.
+
+
diff --git a/lib/plugins/plugin/lang/en/lang.php b/lib/plugins/plugin/lang/en/lang.php
new file mode 100644
index 000000000..fb9934688
--- /dev/null
+++ b/lib/plugins/plugin/lang/en/lang.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * english language file
+ *
+ * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ * @author     Christopher Smith <chris@jalakai.co.uk>
+ */
+
+// settings must be present and set appropriately for the language
+$lang['encoding']   = 'utf-8';
+$lang['direction']  = 'ltr';
+
+// for admin plugins, the menu prompt to be displayed in the admin menu
+// if set here, the plugin doesn't need to override the getMenuText() method
+$lang['menu'] = 'Manage Plugins...'; 
+
+// custom language strings for the plugin
+$lang['refresh'] = "Refresh list of installed plugins";
+$lang['refresh_x'] = "Use this option if you have altered any of your plugins manually"; 
+$lang['download'] = "Download and install a new plugin";
+$lang['manage'] = "Installed Plugins";
+$lang['btn_info'] = 'info';
+$lang['btn_update'] = 'update';
+$lang['btn_delete'] = 'delete';
+$lang['btn_settings'] = 'settings';
+$lang['btn_refresh'] = 'Refresh';
+$lang['btn_download'] = 'Download';
+$lang['url'] = 'URL';
+//$lang[''] = '';
+
+$lang['installed'] = 'Installed:';
+$lang['lastupdate'] = 'Last updated:';
+$lang['source'] = 'Source:';
+$lang['unknown'] = 'unknown';
+
+// ..ing = header message
+// ..ed = success message
+
+$lang['refreshing'] = 'Refreshing ...';
+$lang['refreshed'] = 'Plugin refresh completed.';
+
+$lang['updating'] = 'Updating ...';
+$lang['updated'] = 'Plugin %s updated successfully';
+
+$lang['downloading'] = 'Downloading ...';
+$lang['downloaded'] = 'Plugin %s installed successfully';
+$lang['downloads'] = 'The following plugins have been installed successfully:';
+$lang['download_none'] = 'No plugins found, or there has been an unknown problem during downloading and installing.';
+	
+// error messages
+$lang['error_download'] = 'Unable to download the plugin file: %s';
+$lang['error_write'] = 'Unable to write create aggregate file %s';
+$lang['error_badurl'] = 'Suspect bad url - unable to determine file name from the url';
+$lang['error_dircreate'] = 'Unable to create temporary folder to receive download';
+//$lang['error_'] = '';	
+	
+
+//Setup VIM: ex: et ts=2 enc=utf-8 :
diff --git a/lib/plugins/plugin/style.css b/lib/plugins/plugin/style.css
new file mode 100644
index 000000000..9bc5bc38e
--- /dev/null
+++ b/lib/plugins/plugin/style.css
@@ -0,0 +1,49 @@
+/*
+ * admin plugin extension - style additions
+ *
+ * @author  Christopher Smith  chris@jalakai.co.uk
+ * @link    http://wiki.jalakai.co.uk/dokuwiki/doku.php/tutorials/adminplugin
+ */
+
+/* overall containing div */
+#plugin_manager {}
+
+#plugin_manager h2 {margin-left: 0;}
+#plugin_manager form { display: block; margin: 0; padding: 0;}
+#plugin_manager form:after {content:'.'; display: block; clear: both; visibility: hidden; height: 0;}
+#plugin_manager legend {display: none;}
+#plugin_manager .legend {color: black; display: block; margin: 0; padding: 0; font-size: 1em; line-height: 1.4em; font-weight: normal; text-align: left;}
+#plugin_manager fieldset {border: 0; width: auto;}
+#plugin_manager .button {float: right; margin: 0 0.3em 2px 0;}
+#plugin_manager p, #plugin_manager label {text-align: left;}
+#plugin_manager .hidden {display: none;}
+#plugin_manager .new {background: #dee7ec;}
+#plugin_manager input[disabled] {color: #ccc; border-color: #ccc;}  /* IE won't understand but doesn't require it */
+
+#plugin_manager .pm_menu, #plugin_manager .pm_info {margin-left: 0; text-align: left; overflow: hidden;}
+#plugin_manager .pm_menu {float: left; width: 48%;}
+#plugin_manager .pm_info {float: right; width: 50%;}
+
+#plugin_manager .common {border-bottom: 1px solid #8cacbb;}
+#plugin_manager .common form { border: 1px solid #8cacbb; border-bottom: 0;}
+#plugin_manager .common fieldset { margin: 0; padding: 0;}
+#plugin_manager .common .legend { background: #dee7ec; margin-bottom: 0.3em; padding-left: 0.5em;}
+#plugin_manager .common label { padding: 0 0 0.5em 0.5em; }
+#plugin_manager .common input {width: auto; margin: 0 1em;}
+#plugin_manager .common .button { width: 6em; }
+#plugin_manager .common p {border-bottom: 1px solid #8cacbb; padding: 0 0 0.5em 0.5em; margin-bottom: 0;}
+
+#plugin_manager .plugins {border-bottom: 1px solid #8cacbb;}
+#plugin_manager .plugins form { border: 1px solid #8cacbb; border-bottom: 0;}
+#plugin_manager .plugins fieldset { }
+#plugin_manager .plugins .legend { float: left; }
+#plugin_manager .plugins .button { }
+
+#plugin_manager .pm_info h3 {margin-left: 0; }
+#plugin_manager .pm_info dl { overflow: hidden; margin: 1em 0; padding: 0;}
+#plugin_manager .pm_info dt { width: 6em; float: left; clear: left; margin:0; padding: 0;}
+#plugin_manager .pm_info dd { margin:0 0 0 7em; padding: 0;}
+
+
+/* end admin plugin styles */
+
-- 
GitLab