diff --git a/actions/plugindisable.php b/actions/plugindisable.php new file mode 100644 index 0000000000..7f107b3335 --- /dev/null +++ b/actions/plugindisable.php @@ -0,0 +1,78 @@ +. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Brion Vibber + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Plugin enable action. + * + * (Re)-enables a plugin from the default plugins list. + * + * Takes parameters: + * + * - plugin: plugin name + * - token: session token to prevent CSRF attacks + * - ajax: boolean; whether to return Ajax or full-browser results + * + * Only works if the current user is logged in. + * + * @category Action + * @package StatusNet + * @author Brion Vibber + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class PluginDisableAction extends PluginEnableAction +{ + /** + * Value to save into $config['plugins']['disable-'] + */ + protected function overrideValue() + { + return 1; + } + + protected function successShortTitle() + { + // TRANS: Page title for AJAX form return when a disabling a plugin. + return _m('plugin', 'Disabled'); + } + + protected function successNextForm() + { + return new EnablePluginForm($this, $this->plugin); + } +} + + diff --git a/actions/pluginenable.php b/actions/pluginenable.php new file mode 100644 index 0000000000..2dbb3e3956 --- /dev/null +++ b/actions/pluginenable.php @@ -0,0 +1,166 @@ +. + * + * PHP version 5 + * + * @category Action + * @package StatusNet + * @author Brion Vibber + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Plugin enable action. + * + * (Re)-enables a plugin from the default plugins list. + * + * Takes parameters: + * + * - plugin: plugin name + * - token: session token to prevent CSRF attacks + * - ajax: boolean; whether to return Ajax or full-browser results + * + * Only works if the current user is logged in. + * + * @category Action + * @package StatusNet + * @author Brion Vibber + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class PluginEnableAction extends Action +{ + var $user; + var $plugin; + + /** + * Check pre-requisites and instantiate attributes + * + * @param Array $args array of arguments (URL, GET, POST) + * + * @return boolean success flag + */ + + function prepare($args) + { + parent::prepare($args); + + // @fixme these are pretty common, should a parent class factor these out? + + // Only allow POST requests + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError(_('This action only accepts POST requests.')); + return false; + } + + // CSRF protection + + $token = $this->trimmed('token'); + + if (!$token || $token != common_session_token()) { + $this->clientError(_('There was a problem with your session token.'. + ' Try again, please.')); + return false; + } + + // Only for logged-in users + + $this->user = common_current_user(); + + if (empty($this->user)) { + $this->clientError(_('Not logged in.')); + return false; + } + + if (!AdminPanelAction::canAdmin('plugins')) { + $this->clientError(_('You cannot administer plugins.')); + return false; + } + + $this->plugin = $this->arg('plugin'); + $defaultPlugins = common_config('plugins', 'default'); + if (!array_key_exists($this->plugin, $defaultPlugins)) { + $this->clientError(_('No such plugin.')); + return false; + } + + return true; + } + + /** + * Handle request + * + * Does the subscription and returns results. + * + * @param Array $args unused. + * + * @return void + */ + + function handle($args) + { + $key = 'disable-' . $this->plugin; + Config::save('plugins', $key, $this->overrideValue()); + + // @fixme this is a pretty common pattern and should be refactored down + if ($this->boolean('ajax')) { + $this->startHTML('text/xml;charset=utf-8'); + $this->elementStart('head'); + $this->element('title', null, $this->successShortTitle()); + $this->elementEnd('head'); + $this->elementStart('body'); + $form = $this->successNextForm(); + $form->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $url = common_local_url('pluginsadminpanel'); + common_redirect($url, 303); + } + } + + /** + * Value to save into $config['plugins']['disable-'] + */ + protected function overrideValue() + { + return 0; + } + + protected function successShortTitle() + { + // TRANS: Page title for AJAX form return when enabling a plugin. + return _m('plugin', 'Enabled'); + } + + protected function successNextForm() + { + return new DisablePluginForm($this, $this->plugin); + } +} diff --git a/actions/pluginsadminpanel.php b/actions/pluginsadminpanel.php new file mode 100644 index 0000000000..bc400bd514 --- /dev/null +++ b/actions/pluginsadminpanel.php @@ -0,0 +1,110 @@ +. + * + * @category Settings + * @package StatusNet + * @author Brion Vibber + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Plugins settings + * + * @category Admin + * @package StatusNet + * @author Brion Vibber + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class PluginsadminpanelAction extends AdminPanelAction +{ + + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + // TRANS: Tab and title for plugins admin panel. + return _('Plugins'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + // TRANS: Instructions at top of plugin admin page. + return _('Additional plugins can be enabled and configured manually. ' . + 'See the online plugin ' . + 'documentation for more details.'); + } + + /** + * Show the plugins admin panel form + * + * @return void + */ + + function showForm() + { + $this->elementStart('fieldset', array('id' => 'settings_plugins_default')); + + // TRANS: Admin form section header + $this->element('legend', null, _('Default plugins'), 'default'); + + $this->showDefaultPlugins(); + + $this->elementEnd('fieldset'); + } + + /** + * Until we have a general plugin metadata infrastructure, for now + * we'll just list up the ones we know from the global default + * plugins list. + */ + protected function showDefaultPlugins() + { + $plugins = array_keys(common_config('plugins', 'default')); + natsort($plugins); + + if ($plugins) { + $list = new PluginList($plugins, $this); + $list->show(); + } else { + $this->element('p', null, + _('All default plugins have been disabled from the ' . + 'site\'s configuration file.')); + } + } +} diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php index a927e23336..d87981b6a7 100644 --- a/lib/adminpanelaction.php +++ b/lib/adminpanelaction.php @@ -407,6 +407,14 @@ class AdminPanelNav extends Widget $menu_title, $action_name == 'snapshotadminpanel', 'nav_snapshot_admin_panel'); } + if (AdminPanelAction::canAdmin('plugins')) { + // TRANS: Menu item title/tooltip + $menu_title = _('Plugins configuration'); + // TRANS: Menu item for site administration + $this->out->menuItem(common_local_url('pluginsadminpanel'), _('Plugins'), + $menu_title, $action_name == 'pluginsadminpanel', 'nav_design_admin_panel'); + } + Event::handle('EndAdminPanelNav', array($this)); } $this->action->elementEnd('ul'); diff --git a/lib/default.php b/lib/default.php index 10f3f1a97e..eb0cb0b64a 100644 --- a/lib/default.php +++ b/lib/default.php @@ -285,8 +285,9 @@ $default = 'RSSCloud' => null, 'OpenID' => null), ), + 'pluginlist' => array(), 'admin' => - array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice')), + array('panels' => array('design', 'site', 'user', 'paths', 'access', 'sessions', 'sitenotice', 'plugins')), 'singleuser' => array('enabled' => false, 'nickname' => null), diff --git a/lib/plugindisableform.php b/lib/plugindisableform.php new file mode 100644 index 0000000000..3cbabdb2ce --- /dev/null +++ b/lib/plugindisableform.php @@ -0,0 +1,93 @@ +. + * + * @category Form + * @package StatusNet + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Form for joining a group + * + * @category Form + * @package StatusNet + * @author Brion Vibber + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see PluginEnableForm + */ + +class PluginDisableForm extends PluginEnableForm +{ + /** + * ID of the form + * + * @return string ID of the form + */ + + function id() + { + return 'plugin-disable-' . $this->plugin; + } + + /** + * class of the form + * + * @return string of the form class + */ + + function formClass() + { + return 'form_plugin_disable'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('plugindisable', + array('plugin' => $this->plugin)); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + // TRANS: Plugin admin panel controls + $this->out->submit('submit', _m('plugin', 'Disable')); + } + +} diff --git a/lib/pluginenableform.php b/lib/pluginenableform.php new file mode 100644 index 0000000000..8683ffd0be --- /dev/null +++ b/lib/pluginenableform.php @@ -0,0 +1,114 @@ +. + * + * @category Form + * @package StatusNet + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/form.php'; + +/** + * Form for joining a group + * + * @category Form + * @package StatusNet + * @author Brion Vibber + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @see PluginDisableForm + */ + +class PluginEnableForm extends Form +{ + /** + * Plugin to enable/disable + */ + + var $plugin = null; + + /** + * Constructor + * + * @param HTMLOutputter $out output channel + * @param string $plugin plugin to enable/disable + */ + + function __construct($out=null, $plugin=null) + { + parent::__construct($out); + + $this->plugin = $plugin; + } + + /** + * ID of the form + * + * @return string ID of the form + */ + + function id() + { + return 'plugin-enable-' . $this->plugin; + } + + /** + * class of the form + * + * @return string of the form class + */ + + function formClass() + { + return 'form_plugin_enable'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('pluginenable', + array('plugin' => $this->plugin)); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + // TRANS: Plugin admin panel controls + $this->out->submit('submit', _m('plugin', 'Enable')); + } +} diff --git a/lib/pluginlist.php b/lib/pluginlist.php new file mode 100644 index 0000000000..07a17ba397 --- /dev/null +++ b/lib/pluginlist.php @@ -0,0 +1,213 @@ +. + * + * @category Settings + * @package StatusNet + * @author Brion Vibber + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require INSTALLDIR . "/lib/pluginenableform.php"; +require INSTALLDIR . "/lib/plugindisableform.php"; + +/** + * Plugin list + * + * @category Admin + * @package StatusNet + * @author Brion Vibber + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class PluginList extends Widget +{ + var $plugins = array(); + + function __construct($plugins, $out) + { + parent::__construct($out); + $this->plugins = $plugins; + } + + function show() + { + $this->startList(); + $this->showPlugins(); + $this->endList(); + } + + function startList() + { + $this->out->elementStart('table', 'plugin_list'); + } + + function endList() + { + $this->out->elementEnd('table'); + } + + function showPlugins() + { + foreach ($this->plugins as $plugin) { + $pli = $this->newListItem($plugin); + $pli->show(); + } + } + + function newListItem($plugin) + { + return new PluginListItem($plugin, $this->out); + } +} + +class PluginListItem extends Widget +{ + /** Current plugin. */ + var $plugin = null; + + /** Local cache for plugin version info */ + protected static $versions = false; + + function __construct($plugin, $out) + { + parent::__construct($out); + $this->plugin = $plugin; + } + + function show() + { + $meta = $this->metaInfo(); + + $this->out->elementStart('tr', array('id' => 'plugin-' . $this->plugin)); + + // Name and controls + $this->out->elementStart('td'); + $this->out->elementStart('div'); + if (!empty($meta['homepage'])) { + $this->out->elementStart('a', array('href' => $meta['homepage'])); + } + $this->out->text($this->plugin); + if (!empty($meta['homepage'])) { + $this->out->elementEnd('a'); + } + $this->out->elementEnd('div'); + + $form = $this->getControlForm(); + $form->show(); + + $this->out->elementEnd('td'); + + // Version and authors + $this->out->elementStart('td'); + if (!empty($meta['version'])) { + $this->out->elementStart('div'); + $this->out->text($meta['version']); + $this->out->elementEnd('div'); + } + if (!empty($meta['author'])) { + $this->out->elementStart('div'); + $this->out->text($meta['author']); + $this->out->elementEnd('div'); + } + $this->out->elementEnd('td'); + + // Description + $this->out->elementStart('td'); + if (!empty($meta['rawdescription'])) { + $this->out->raw($meta['rawdescription']); + } + $this->out->elementEnd('td'); + + $this->out->elementEnd('tr'); + } + + /** + * Pull up the appropriate control form for this plugin, depending + * on its current state. + * + * @return Form + */ + protected function getControlForm() + { + $key = 'disable-' . $this->plugin; + if (common_config('plugins', $key)) { + return new PluginEnableForm($this->out, $this->plugin); + } else { + return new PluginDisableForm($this->out, $this->plugin); + } + } + + /** + * Grab metadata about this plugin... + * Warning: horribly inefficient and may explode! + * Doesn't work for disabled plugins either. + * + * @fixme pull structured data from plugin source + */ + function metaInfo() + { + $versions = self::getPluginVersions(); + $found = false; + + foreach ($versions as $info) { + // hack for URL shorteners... "LilUrl (ur1.ca)" etc + list($name, ) = explode(' ', $info['name']); + + if ($name == $this->plugin) { + if ($found) { + // hack for URL shorteners... + $found['rawdescription'] .= "
\n" . $info['rawdescription']; + } else { + $found = $info; + } + } + } + + if ($found) { + return $found; + } else { + return array('name' => $this->plugin, + 'rawdescription' => _m('plugin-description', + '(Plugin descriptions unavailable when disabled.)')); + } + } + + /** + * Lazy-load the set of active plugin version info + * @return array + */ + protected static function getPluginVersions() + { + if (!is_array(self::$versions)) { + $versions = array(); + Event::handle('PluginVersion', array(&$versions)); + self::$versions = $versions; + } + return self::$versions; + } +} diff --git a/lib/router.php b/lib/router.php index a48ee875e1..9fe2f60ae7 100644 --- a/lib/router.php +++ b/lib/router.php @@ -658,6 +658,13 @@ class Router $m->connect('admin/sessions', array('action' => 'sessionsadminpanel')); $m->connect('admin/sitenotice', array('action' => 'sitenoticeadminpanel')); $m->connect('admin/snapshot', array('action' => 'snapshotadminpanel')); + $m->connect('admin/plugins', array('action' => 'pluginsadminpanel')); + $m->connect('admin/plugins/enable/:plugin', + array('action' => 'pluginenable'), + array('plugin' => '[A-Za-z0-9_]+')); + $m->connect('admin/plugins/disable/:plugin', + array('action' => 'plugindisable'), + array('plugin' => '[A-Za-z0-9_]+')); $m->connect('getfile/:filename', array('action' => 'getfile'), diff --git a/lib/statusnet.php b/lib/statusnet.php index 776cfb579b..98f25c8a08 100644 --- a/lib/statusnet.php +++ b/lib/statusnet.php @@ -163,6 +163,11 @@ class StatusNet { // Load default plugins foreach (common_config('plugins', 'default') as $name => $params) { + $key = 'disable-' . $name; + if (common_config('plugins', $key)) { + continue; + } + if (is_null($params)) { addPlugin($name); } else if (is_array($params)) {