*/ // must be run within Dokuwiki if(!defined('DOKU_INC')) die(); // 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_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); require_once(DOKU_PLUGIN.'admin.php'); //--------------------------[ GLOBALS ]------------------------------------------------ // note: probably should be dokuwiki wide globals, where they can be accessed by pluginutils.php // global $plugin_types; // $plugin_types = array('syntax', 'admin'); // plugins that are an integral part of dokuwiki, they shouldn't be disabled or deleted global $plugin_protected; $plugin_protected = array('acl','plugin','config','info','usermanager'); /** * 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','download','enable'); // 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() { if ($this->disabled) return; // enable direct access to language strings $this->setupLocale(); // $this->plugin = $_REQUEST['plugin']; // $this->cmd = $_REQUEST['fn']; // if (is_array($this->cmd)) $this->cmd = key($this->cmd); $fn = $_REQUEST['fn']; if (is_array($fn)) { $this->cmd = key($fn); $this->plugin = is_array($fn[$this->cmd]) ? key($fn[$this->cmd]) : null; } else { $this->cmd = $fn; $this->plugin = null; } $this->plugin_list = plugin_list('', true); sort($this->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 = "ap_".$this->cmd; if (!class_exists($class)) $class = 'ap_manage'; $this->handler = & new $class($this, $this->plugin); $this->msg = $this->handler->process(); } /** * output appropriate html */ function html() { if ($this->disabled) return; // enable direct access to language strings $this->setupLocale(); if ($this->handler === NULL) $this->handler = & new ap_manage($this, $this->plugin); if (!$this->plugin_list) { $this->plugin_list = plugin_list('',true); sort($this->plugin_list); } ptln('
'); $this->handler->html(); ptln('
'); } } class ap_manage { var $manager = NULL; var $lang = array(); var $plugin = ''; var $downloaded = array(); function ap_manage(&$manager, $plugin) { $this->manager = & $manager; $this->plugin = $plugin; $this->lang = & $manager->lang; } function process() { return ''; } function html() { print $this->manager->locale_xhtml('admin_plugin'); $this->html_menu(); } // build our standard menu function html_menu($listPlugins = true) { global $ID; ptln('
'); ptln('
'); ptln('

'.$this->lang['download'].'

'); ptln('
'); ptln(' '); ptln('
'); ptln(' '.$this->lang['download'].''); ptln(' '); ptln(' '); ptln('
'); ptln('
'); ptln('
'); if ($listPlugins) { ptln('

'.$this->lang['manage'].'

'); ptln('
'); // ptln('
'); ptln(' '); $this->html_pluginlist(); ptln('
'); ptln(' '); ptln('
'); // ptln('
'); ptln('
'); } ptln('
'); } function html_pluginlist() { global $ID; global $plugin_protected; foreach ($this->manager->plugin_list as $plugin) { $disabled = plugin_isdisabled($plugin); $protected = in_array($plugin,$plugin_protected); $checked = ($disabled) ? '' : ' checked="checked"'; $check_disabled = ($protected) ? ' disabled="disabled"' : ''; // determine display class(es) $class = array(); if (in_array($plugin, $this->downloaded)) $class[] = 'new'; if ($disabled) $class[] = 'disabled'; if ($protected) $class[] = 'protected'; $class = count($class) ? ' class="'.join(' ', $class).'"' : ''; ptln(' '); ptln(' '.$plugin.''); ptln(' '); ptln('

'.$plugin.'

'); $this->html_button($plugin, 'info', false, 6); if (in_array('settings', $this->manager->functions)) { $this->html_button($plugin, 'settings', !@file_exists(DOKU_PLUGIN.$plugin.'/settings.php'), 6); } $this->html_button($plugin, 'update', !$this->plugin_readlog($plugin, 'url'), 6); $this->html_button($plugin, 'delete', $protected, 6); ptln(' '); } } function html_button($plugin, $btn, $disabled=false, $indent=0) { $disabled = ($disabled) ? 'disabled="disabled"' : ''; ptln('',$indent); } /** * Refresh plugin list */ function refresh() { $this->manager->plugin_list = plugin_list('',true); sort($this->manager->plugin_list); // expire dokuwiki caches // touching local.php expires wiki page, JS and CSS caches @touch(DOKU_CONF.'local.php'); // update latest plugin date - FIXME return (!$this->manager->error); } function download($url, $overwrite=false) { global $lang; // check the url $matches = array(); if (!preg_match("/[^\/]*$/", $url, $matches) || !$matches[0]) { $this->manager->error = $this->lang['error_badurl']."\n"; return false; } $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 (!ap_mkdir($tmp)) { $this->manager->error = $this->lang['error_dir_create']."\n"; return false; } if (!$file = io_download($url, "$tmp/", true, $file)) { $this->manager->error = sprintf($this->lang['error_download'],$url)."\n"; } if (!$this->manager->error && !ap_decompress("$tmp/$file", $tmp)) { $this->manager->error = sprintf($this->lang['error_decompress'],$file)."\n"; } // search tmp/$folder for the folder(s) that has been created // move that folder(s) to lib/plugins/ if (!$this->manager->error) { 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 (!$overwrite && @file_exists(DOKU_PLUGIN."/$f")) { // remember our settings, ask the user to confirm overwrite, FIXME continue; } $instruction = @file_exists(DOKU_PLUGIN."/$f") ? 'update' : 'install'; if (ap_copy("$tmp/$f", DOKU_PLUGIN.$f)) { $this->downloaded[] = $f; $this->plugin_writelog($f, $instruction, array($url)); } else { $this->manager->error .= sprintf($lang['error_copy']."\n", $f); } } closedir($dh); } else { $this->manager->error = $this->lang['error']."\n"; } } // cleanup if ($folder && is_dir(DOKU_PLUGIN."tmp/$folder")) ap_delete(DOKU_PLUGIN."tmp/$folder"); if (!$this->manager->error) { $this->refresh(); return true; } return false; } // 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, 'a')) 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]; } $match = array(); if (preg_match_all('/'.$field.'=(.*)$/m',$log[$plugin], $match)) return implode("\n", $match[1]); return ''; } } class ap_download extends ap_manage { var $overwrite = false; function process() { global $lang; $plugin_url = $_REQUEST['url']; $this->download($plugin_url, $this->overwrite); return ''; } function html() { parent::html(); ptln('
'); ptln('

'.$this->lang['downloading'].'

'); if ($this->manager->error) { ptln('
'.str_replace("\n","
",$this->manager->error).'
'); } else if (count($this->downloaded) == 1) { ptln('

'.sprintf($this->lang['downloaded'],$this->downloaded[0]).'

'); } else if (count($this->downloaded)) { // more than one plugin in the download ptln('

'.$this->lang['downloads'].'

'); ptln(''); } else { // none found in download ptln('

'.$this->lang['download_none'].'

'); } ptln('
'); } } class ap_delete extends ap_manage { function process() { if (!ap_delete(DOKU_PLUGIN.$this->manager->plugin)) { $this->manager->error = sprintf($this->lang['error_delete'],$this->manager->plugin); } else { $this->refresh(); } } function html() { parent::html(); ptln('
'); ptln('

'.$this->lang['deleting'].'

'); if ($this->manager->error) { ptln('
'.str_replace("\n","
",$this->manager->error).'
'); } else { ptln('

'.sprintf($this->lang['deleted'],$this->plugin).'

'); } ptln('
'); } } class ap_info extends ap_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('
'); ptln("

".$this->manager->getLang('plugin')." {$this->manager->plugin}

"); // collect pertinent information from the log $installed = $this->plugin_readlog($this->manager->plugin, 'installed'); $source = $this->plugin_readlog($this->manager->plugin, 'url'); $updated = $this->plugin_readlog($this->manager->plugin, 'updated'); if (strrpos($updated, "\n") !== false) $updated = substr($updated, strrpos($updated, "\n")+1); ptln("
",2); ptln("
".$this->manager->getLang('source').'
'.($source ? $source : $this->manager->getLang('unknown'))."
",4); ptln("
".$this->manager->getLang('installed').'
'.($installed ? $installed : $this->manager->getLang('unknown'))."
",4); if ($updated) ptln("
".$this->manager->getLang('lastupdate').'
'.$updated."
",4); ptln("
",2); if (count($this->details) == 0) { ptln("

".$this->manager->getLang('noinfo')."

",2); } else { ptln("
",2); if ($this->plugin_info['name']) ptln("
".$this->manager->getLang('name')."
".$this->out($this->plugin_info['name'])."
",4); if ($this->plugin_info['date']) ptln("
".$this->manager->getLang('date')."
".$this->out($this->plugin_info['date'])."
",4); if ($this->plugin_info['type']) ptln("
".$this->manager->getLang('type')."
".$this->out($this->plugin_info['type'])."
",4); if ($this->plugin_info['desc']) ptln("
".$this->manager->getLang('desc')."
".$this->out($this->plugin_info['desc'])."
",4); if ($this->plugin_info['author']) ptln("
".$this->manager->getLang('author')."
".$this->manager->email($this->plugin_info['email'], $this->plugin_info['author'])."
",4); if ($this->plugin_info['url']) ptln("
".$this->manager->getLang('www')."
".$this->manager->external_link($this->plugin_info['url'], '', 'urlextern')."
",4); ptln("
",2); if (count($this->details) > 1) { ptln("

".$this->manager->getLang('components')."

",2); ptln("
",2); foreach ($this->details as $info) { ptln("
",4); ptln("
".$this->manager->getLang('name')."
".$this->out($info['name'])."
",6); if (!$this->plugin_info['date']) ptln("
".$this->manager->getLang('date')."
".$this->out($info['date'])."
",6); if (!$this->plugin_info['type']) ptln("
".$this->manager->getLang('type')."
".$this->out($info['type'])."
",6); if (!$this->plugin_info['desc']) ptln("
".$this->manager->getLang('desc')."
".$this->out($info['desc'])."
",6); if (!$this->plugin_info['author']) ptln("
".$this->manager->getLang('author')."
".$this->manager->email($info['email'], $info['author'])."
",6); if (!$this->plugin_info['url']) ptln("
".$this->manager->getLang('www')."
".$this->manager->external_link($info['url'], '', 'urlextern')."
",6); ptln("
",4); } ptln("
",2); } } ptln("
"); } // simple output filter, make html entities safe and convert new lines to
function out($text) { return str_replace("\n",'
',htmlentities($text)); } } //--------------[ to do ]--------------------------------------- class ap_update extends ap_manage { var $overwrite = true; function process() { global $lang; $plugin_url = $this->plugin_readlog($this->plugin, 'url'); $this->download($plugin_url, $this->overwrite); return ''; } function html() { parent::html(); ptln('
'); ptln('

'.$this->lang['updating'].'

'); if ($this->manager->error) { ptln('
'.str_replace("\n","
", $this->manager->error).'
'); } else if (count($this->downloaded) == 1) { ptln('

'.sprintf($this->lang['updated'],$this->downloaded[0]).'

'); } else if (count($this->downloaded)) { // more than one plugin in the download ptln('

'.$this->lang['updates'].'

'); ptln(''); } else { // none found in download ptln('

'.$this->lang['update_none'].'

'); } ptln('
'); } } class ap_settings extends ap_manage {} class ap_enable extends ap_manage { var $enabled = array(); function process() { global $plugin_protected; $this->enabled = isset($_REQUEST['enabled']) ? $_REQUEST['enabled'] : array(); foreach ($this->manager->plugin_list as $plugin) { if (in_array($plugin, $plugin_protected)) continue; $new = in_array($plugin, $this->enabled); $old = !plugin_isdisabled($plugin); if ($new != $old) { switch ($new) { // enable plugin case true : plugin_enable($plugin); break; case false: plugin_disable($plugin); break; } } } // refresh plugins, including expiring any dokuwiki cache(s) $this->refresh(); } } //--------------[ utilities ]----------------------------------- function is_css($f) { return (substr($f, -4) == '.css'); } // generate an admin plugin href function apl($pl, $fn) { global $ID; return wl($ID,"do=admin&page=plugin".($pl?"&plugin=$pl":"").($fn?"&fn=$fn":"")); } // 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); $ext = substr($file, strrpos($file,'.')+1); // .tar, .tar.bz, .tar.gz, .tgz if (in_array($ext, array('tar','bz','bz2','gz','tgz'))) { require_once(DOKU_INC."inc/TarLib.class.php"); if (strpos($ext, 'bz') !== false) $compress_type = COMPRESS_BZIP; else if (strpos($ext,'gz') !== false) $compress_type = COMPRESS_GZIP; else $compress_type = COMPRESS_NONE; $tar = new TarLib($file, $compress_type); $ok = $tar->Extract(FULL_ARCHIVE, $target, '', 0777); // FIXME sort something out for handling tar error messages meaningfully return ($ok<0?false:true); } else if ($ext == 'zip') { require_once(DOKU_INC."inc/ZipLib.class.php"); $zip = new ZipLib(); $ok = $zip->Extract($file, $target); // FIXME sort something out for handling zip error messages meaningfully return ($ok==-1?false:true); } else if ($ext == "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; $ok = io_mkdir_p($d); return $ok; } // copy with recursive sub-directory support function ap_copy($src, $dst) { global $conf; if (is_dir($src)) { if (!$dh = @opendir($src)) return false; if ($ok = ap_mkdir($dst)) { while ($ok && (false !== ($f = readdir($dh)))) { if ($f == '..' || $f == '.') continue; $ok = ap_copy("$src/$f", "$dst/$f"); } } closedir($dh); return $ok; } else { $exists = @file_exists($dst); if (!@copy($src,$dst)) return false; if (!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']); @touch($dst,filemtime($src)); } return true; } // delete, with recursive sub-directory support function ap_delete($path) { if (!is_string($path) || $path == "") return false; if (is_dir($path)) { if (!$dh = @opendir($path)) return false; while ($f = readdir($dh)) { if ($f == '..' || $f == '.') continue; ap_delete("$path/$f"); } closedir($dh); return @rmdir($path); } else { return @unlink($path); } return false; } // 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 $plugin_types; $components = array(); $path = DOKU_PLUGIN.$plugin.'/'; foreach ($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; }