.
*/
/**
* Plugins panel.
*
* @package Admin\Plugin
*/
if (!defined('txpinterface')) {
die('txpinterface is undefined.');
}
if ($event == 'plugin') {
require_privs('plugin');
$available_steps = array(
'plugin_edit' => true,
'plugin_help' => false,
'plugin_list' => false,
'plugin_install' => true,
'plugin_save' => true,
'plugin_verify' => true,
'switch_status' => true,
'plugin_multi_edit' => true,
);
if ($step && bouncer($step, $available_steps)) {
$step();
} else {
plugin_list();
}
}
/**
* The main panel listing all installed plugins.
*
* @param string|array $message The activity message
*/
function plugin_list($message = '')
{
global $event;
pagetop(gTxt('tab_plugins'), $message);
extract(gpsa(array(
'sort',
'dir',
)));
if ($sort === '') {
$sort = get_pref('plugin_sort_column', 'name');
} else {
if (!in_array($sort, array('name', 'status', 'author', 'version', 'modified', 'load_order'))) {
$sort = 'name';
}
set_pref('plugin_sort_column', $sort, 'plugin', 2, '', 0, PREF_PRIVATE);
}
if ($dir === '') {
$dir = get_pref('plugin_sort_dir', 'asc');
} else {
$dir = ($dir == 'desc') ? "desc" : "asc";
set_pref('plugin_sort_dir', $dir, 'plugin', 2, '', 0, PREF_PRIVATE);
}
$sort_sql = "$sort $dir";
$switch_dir = ($dir == 'desc') ? 'asc' : 'desc';
echo n.'
'.
n.tag(
hed(gTxt('tab_plugins'), 1, array('class' => 'txp-heading')),
'div', array('class' => 'txp-layout-1col')
).
n.tag_start('div', array(
'class' => 'txp-layout-1col',
'id' => $event.'_container',
)).
n.tag(plugin_form(), 'div', array('class' => 'txp-control-panel'));
$rs = safe_rows_start(
"name, status, author, author_uri, version, description, length(help) AS help, ABS(STRCMP(MD5(code), code_md5)) AS modified, load_order, flags",
'txp_plugin',
"1 = 1 ORDER BY $sort_sql"
);
if ($rs and numRows($rs) > 0) {
echo
n.tag_start('form', array(
'class' => 'multi_edit_form',
'id' => 'plugin_form',
'name' => 'longform',
'method' => 'post',
'action' => 'index.php',
)).
n.tag_start('div', array('class' => 'txp-listtables')).
n.tag_start('table', array('class' => 'txp-list')).
n.tag_start('thead').
tr(
hCell(
fInput('checkbox', 'select_all', 0, '', '', '', '', '', 'select_all'),
'', ' class="txp-list-col-multi-edit" scope="col" title="'.gTxt('toggle_all_selected').'"'
).
column_head(
'plugin', 'name', 'plugin', true, $switch_dir, '', '',
(('name' == $sort) ? "$dir " : '').'txp-list-col-name'
).
column_head(
'author', 'author', 'plugin', true, $switch_dir, '', '',
(('author' == $sort) ? "$dir " : '').'txp-list-col-author'
).
column_head(
'version', 'version', 'plugin', true, $switch_dir, '', '',
(('version' == $sort) ? "$dir " : '').'txp-list-col-version'
).
column_head(
'plugin_modified', 'modified', 'plugin', true, $switch_dir, '', '',
(('modified' == $sort) ? "$dir " : '').'txp-list-col-modified'
).
hCell(gTxt(
'description'), '', ' class="txp-list-col-description" scope="col"'
).
column_head(
'active', 'status', 'plugin', true, $switch_dir, '', '',
(('status' == $sort) ? "$dir " : '').'txp-list-col-status'
).
column_head(
'order', 'load_order', 'plugin', true, $switch_dir, '', '',
(('load_order' == $sort) ? "$dir " : '').'txp-list-col-load-order'
).
hCell(
gTxt('manage'), '', ' class="txp-list-col-manage" scope="col"'
)
).
n.tag_end('thead').
n.tag_start('tbody');
while ($a = nextRow($rs)) {
foreach ($a as $key => $value) {
$$key = txpspecialchars($value);
}
// Fix up the description for clean cases.
$description = preg_replace(
array(
'#<br />#',
'#<(/?(a|b|i|em|strong))>#',
'#<a href="(https?|\.|\/|ftp)([A-Za-z0-9:/?.=_]+?)">#',
),
array(
'
',
'<$1>',
'
',
),
$description
);
if (!empty($help)) {
$help = href(gTxt('help'), array(
'event' => 'plugin',
'step' => 'plugin_help',
'name' => $name,
), array('class' => 'plugin-help'));
}
if ($flags & PLUGIN_HAS_PREFS) {
$plugin_prefs = span(
sp.span('|', array('role' => 'separator')).
sp.href(gTxt('plugin_prefs'), array('event' => 'plugin_prefs.'.$name)),
array('class' => 'plugin-prefs')
);
} else {
$plugin_prefs = '';
}
$manage = array();
if ($help) {
$manage[] = $help;
}
if ($plugin_prefs) {
$manage[] = $plugin_prefs;
}
$manage_items = ($manage) ? join($manage) : '-';
$edit_url = eLink('plugin', 'plugin_edit', 'name', $name, $name);
echo tr(
td(
fInput('checkbox', 'selected[]', $name), '', 'txp-list-col-multi-edit'
).
hCell(
$edit_url, '', ' class="txp-list-col-name" scope="row"'
).
td(
href($author, $a['author_uri'], array('rel' => 'external')), '', 'txp-list-col-author'
).
td(
$version, '', 'txp-list-col-version'
).
td(
($modified ? span(gTxt('yes'), array('class' => 'warning')) : ''), '', 'txp-list-col-modified'
).
td(
$description, '', 'txp-list-col-description'
).
td(
status_link($status, $name, yes_no($status)), '', 'txp-list-col-status'
).
td(
$load_order, '', 'txp-list-col-load-order'
).
td(
$manage_items, '', 'txp-list-col-manage'
),
$status ? ' class="active"' : ''
);
unset($name, $page, $deletelink);
}
echo
n.tag_end('tbody').
n.tag_end('table').
n.tag_end('div'). // End of .txp-listtables.
plugin_multiedit_form('', $sort, $dir, '', '').
tInput().
n.tag_end('form');
}
echo n.tag_end('div'). // End of .txp-layout-1col.
n.' '; // End of .txp-layout.
}
/**
* Toggles a plugin's status.
*/
function switch_status()
{
extract(array_map('assert_string', gpsa(array('thing', 'value'))));
$change = ($value == gTxt('yes')) ? 0 : 1;
safe_update('txp_plugin', "status = $change", "name = '".doSlash($thing)."'");
if (safe_field('flags', 'txp_plugin', "name = '".doSlash($thing)."'") & PLUGIN_LIFECYCLE_NOTIFY) {
load_plugin($thing, true);
$message = callback_event("plugin_lifecycle.$thing", $change ? 'enabled' : 'disabled');
}
echo gTxt($change ? 'yes' : 'no');
}
/**
* Renders and outputs the plugin editor panel.
*/
function plugin_edit()
{
global $event;
$name = gps('name');
pagetop(gTxt('edit_plugins'));
echo plugin_edit_form($name);
}
/**
* Plugin help viewer panel.
*/
function plugin_help()
{
global $event;
$name = gps('name');
pagetop(gTxt('plugin_help'));
$help = ($name) ? safe_field('help', 'txp_plugin', "name = '".doSlash($name)."'") : '';
echo n.tag($help, 'div', array('class' => 'txp-layout-textbox'));
}
/**
* Renders an editor form for plugins.
*
* @param string $name The plugin
* @return string HTML
*/
function plugin_edit_form($name = '')
{
assert_string($name);
$code = ($name) ? fetch('code', 'txp_plugin', 'name', $name) : '';
$thing = ($code) ? $code : '';
return
form(
hed(gTxt('edit_plugin', array('{name}' => $name)), 2).
graf('', ' class="edit-plugin-code"').
graf(
sLink('plugin', '', gTxt('cancel'), 'txp-button').
fInput('submit', '', gTxt('save'), 'publish'),
array('class' => 'txp-edit-actions')
).
eInput('plugin').
sInput('plugin_save').
hInput('name', $name), '', '', 'post', '', '', 'plugin_details');
}
/**
* Saves edited plugin code.
*/
function plugin_save()
{
extract(doSlash(array_map('assert_string', gpsa(array('name', 'code')))));
safe_update('txp_plugin', "code = '$code'", "name = '$name'");
$message = gTxt('plugin_saved', array('{name}' => $name));
plugin_list($message);
}
/**
* Renders a status link.
*
* @param string $status The new status
* @param string $name The plugin
* @param string $linktext The label
* @return string HTML
* @access private
* @see asyncHref()
*/
function status_link($status, $name, $linktext)
{
return asyncHref(
$linktext,
array('step' => 'switch_status', 'thing' => $name)
);
}
/**
* Plugin installation's preview step.
*
* Outputs a panel displaying the plugin's source code
* and the included help file.
*/
function plugin_verify()
{
global $event;
if (ps('txt_plugin')) {
$plugin = join("\n", file($_FILES['theplugin']['tmp_name']));
} else {
$plugin = assert_string(ps('plugin'));
}
// Check for pre-4.0 style plugin.
if (strpos($plugin, '$plugin=\'') !== false) {
// Try to increase PCRE's backtrack limit in PHP 5.2+ to accommodate to
// x-large plugins. See https://bugs.php.net/bug.php?id=40846.
@ini_set('pcre.backtrack_limit', '1000000');
$plugin = preg_replace('@.*\$plugin=\'([\w=+/]+)\'.*@s', '$1', $plugin);
// Have we hit yet another PCRE restriction?
if ($plugin === null) {
plugin_list(array(gTxt('plugin_pcre_error', array('{errno}' => preg_last_error())), E_ERROR));
return;
}
}
// Strip out #comment lines.
$plugin = preg_replace('/^#.*$/m', '', $plugin);
if ($plugin === null) {
plugin_list(array(gTxt('plugin_pcre_error', array('{errno}' => preg_last_error())), E_ERROR));
return;
}
if (isset($plugin)) {
$plugin_encoded = $plugin;
$plugin = base64_decode($plugin);
if (strncmp($plugin, "\x1F\x8B", 2) === 0) {
if (function_exists('gzinflate')) {
$plugin = gzinflate(substr($plugin, 10));
} else {
plugin_list(array(gTxt('plugin_compression_unsupported'), E_ERROR));
return;
}
}
if ($plugin = @unserialize($plugin)) {
if (is_array($plugin)) {
$source = '';
if (isset($plugin['help_raw']) && empty($plugin['allow_html_help'])) {
$textile = new \Textpattern\Textile\Parser();
$help_source = $textile->textileRestricted($plugin['help_raw'], 0, 0);
} else {
$help_source = highlight_string($plugin['help'], true);
}
$source .= highlight_string('', true);
$sub = graf(
sLink('plugin', '', gTxt('cancel'), 'txp-button').
fInput('submit', '', gTxt('install'), 'publish'),
array('class' => 'txp-edit-actions')
);
pagetop(gTxt('verify_plugin'));
echo form(
hed(gTxt('previewing_plugin'), 2).
tag($source, 'div', ' class="code" id="preview-plugin" dir="ltr"').
hed(gTxt('plugin_help').':', 2).
tag($help_source, 'div', ' class="code" id="preview-help" dir="ltr"').
$sub.
sInput('plugin_install').
eInput('plugin').
hInput('plugin64', $plugin_encoded), '', '', 'post', 'plugin-info', '', 'plugin_preview'
);
return;
}
}
}
plugin_list(array(gTxt('bad_plugin_code'), E_ERROR));
}
/**
* Installs a plugin.
*/
function plugin_install()
{
$plugin = assert_string(ps('plugin64'));
if (strpos($plugin, '$plugin=\'') !== false) {
@ini_set('pcre.backtrack_limit', '1000000');
$plugin = preg_replace('@.*\$plugin=\'([\w=+/]+)\'.*@s', '$1', $plugin);
}
$plugin = preg_replace('/^#.*$/m', '', $plugin);
if (trim($plugin)) {
$plugin = base64_decode($plugin);
if (strncmp($plugin, "\x1F\x8B", 2) === 0) {
$plugin = gzinflate(substr($plugin, 10));
}
if ($plugin = unserialize($plugin)) {
if (is_array($plugin)) {
extract($plugin);
$type = empty($type) ? 0 : min(max(intval($type), 0), 5);
$order = empty($order) ? 5 : min(max(intval($order), 1), 9);
$flags = empty($flags) ? 0 : intval($flags);
$exists = fetch('name', 'txp_plugin', 'name', $name);
if (isset($help_raw) && empty($plugin['allow_html_help'])) {
// Default: help is in Textile format.
$textile = new \Textpattern\Textile\Parser();
$help = $textile->textileRestricted($help_raw, 0, 0);
}
if ($exists) {
$rs = safe_update(
'txp_plugin',
"type = $type,
author = '".doSlash($author)."',
author_uri = '".doSlash($author_uri)."',
version = '".doSlash($version)."',
description = '".doSlash($description)."',
help = '".doSlash($help)."',
code = '".doSlash($code)."',
code_restore = '".doSlash($code)."',
code_md5 = '".doSlash($md5)."',
flags = $flags",
"name = '".doSlash($name)."'"
);
} else {
$rs = safe_insert(
'txp_plugin',
"name = '".doSlash($name)."',
status = 0,
type = $type,
author = '".doSlash($author)."',
author_uri = '".doSlash($author_uri)."',
version = '".doSlash($version)."',
description = '".doSlash($description)."',
help = '".doSlash($help)."',
code = '".doSlash($code)."',
code_restore = '".doSlash($code)."',
code_md5 = '".doSlash($md5)."',
load_order = '".$order."',
flags = $flags"
);
}
if ($rs and $code) {
if (!empty($textpack)) {
// Plugins tag their Textpack by plugin name.
// The ownership may be overridden in the Textpack itself.
$textpack = "#@owner {$name}".n.$textpack;
install_textpack($textpack, false);
}
if ($flags & PLUGIN_LIFECYCLE_NOTIFY) {
load_plugin($name, true);
$message = callback_event("plugin_lifecycle.$name", 'installed');
}
if (empty($message)) {
$message = gTxt('plugin_installed', array('{name}' => $name));
}
plugin_list($message);
return;
} else {
$message = array(gTxt('plugin_install_failed', array('{name}' => $name)), E_ERROR);
plugin_list($message);
return;
}
}
}
}
plugin_list(array(gTxt('bad_plugin_code'), E_ERROR));
}
/**
* Renders a plugin installation form.
*
* @return string HTML
* @access private
* @see form()
*/
function plugin_form()
{
return form(
tag(gTxt('install_plugin'), 'label', ' for="plugin-install"').popHelp('install_plugin').
''.
fInput('submit', 'install_new', gTxt('upload')).
eInput('plugin').
sInput('plugin_verify'), '', '', 'post', 'plugin-data', '', 'plugin_install_form');
}
/**
* Renders a multi-edit form widget for plugins.
*
* @param int $page The current page
* @param string $sort The sort criteria
* @param string $dir The sort direction
* @param string $crit The search term
* @param string $search_method The search method
* @return string HTML
*/
function plugin_multiedit_form($page, $sort, $dir, $crit, $search_method)
{
$orders = selectInput('order', array(
1 => 1,
2 => 2,
3 => 3,
4 => 4,
5 => 5,
6 => 6,
7 => 7,
8 => 8,
9 => 9,
), 5, false);
$methods = array(
'changestatus' => gTxt('changestatus'),
'changeorder' => array('label' => gTxt('changeorder'), 'html' => $orders),
'delete' => gTxt('delete'),
);
return multi_edit($methods, 'plugin', 'plugin_multi_edit', $page, $sort, $dir, $crit, $search_method);
}
/**
* Processes multi-edit actions.
*/
function plugin_multi_edit()
{
$selected = ps('selected');
$method = assert_string(ps('edit_method'));
if (!$selected or !is_array($selected)) {
return plugin_list();
}
$where = "name IN ('".join("','", doSlash($selected))."')";
switch ($method) {
case 'delete':
foreach ($selected as $name) {
if (safe_field("flags", 'txp_plugin', "name = '".doSlash($name)."'") & PLUGIN_LIFECYCLE_NOTIFY) {
load_plugin($name, true);
callback_event("plugin_lifecycle.$name", 'disabled');
callback_event("plugin_lifecycle.$name", 'deleted');
}
}
// Remove plugins.
safe_delete('txp_plugin', $where);
// Remove plugin's l10n strings.
safe_delete('txp_lang', "owner IN ('".join("','", doSlash($selected))."')");
break;
case 'changestatus':
foreach ($selected as $name) {
if (safe_field("flags", 'txp_plugin', "name = '".doSlash($name)."'") & PLUGIN_LIFECYCLE_NOTIFY) {
$status = safe_field("status", 'txp_plugin', "name = '".doSlash($name)."'");
load_plugin($name, true);
// Note: won't show returned messages anywhere due to
// potentially overwhelming verbiage.
callback_event("plugin_lifecycle.$name", $status ? 'disabled' : 'enabled');
}
}
safe_update('txp_plugin', "status = (1 - status)", $where);
break;
case 'changeorder':
$order = min(max(intval(ps('order')), 1), 9);
safe_update('txp_plugin', "load_order = $order", $where);
break;
}
$message = gTxt('plugin_'.($method == 'delete' ? 'deleted' : 'updated'), array('{name}' => join(', ', $selected)));
plugin_list($message);
}