.
*/
/**
* Plugins panel.
*
* @package Admin\Plugin
*/
use Textpattern\Search\Filter;
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_upload' => true,
'plugin_load' => true,
'plugin_verify' => true,
'switch_status' => true,
'plugin_multi_edit' => true,
'plugin_change_pageby' => true,
);
if ($step && bouncer($step, $available_steps) && is_callable($step)) {
$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(
'page',
'sort',
'dir',
'crit',
'search_method',
)));
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', PREF_HIDDEN, '', 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', PREF_HIDDEN, '', 0, PREF_PRIVATE);
}
$sort_sql = "$sort $dir";
$switch_dir = ($dir == 'desc') ? 'asc' : 'desc';
$search = new Filter($event,
array(
'name' => array(
'column' => 'txp_plugin.name',
'label' => gTxt('plugin'),
),
'author' => array(
'column' => 'txp_plugin.author',
'label' => gTxt('author'),
),
'author_uri' => array(
'column' => 'txp_plugin.author_uri',
'label' => gTxt('website'),
),
'description' => array(
'column' => 'txp_plugin.description',
'label' => gTxt('description'),
),
'code' => array(
'column' => 'txp_plugin.code',
'label' => gTxt('code'),
),
'help' => array(
'column' => 'txp_plugin.help',
'label' => gTxt('help'),
),
'textpack' => array(
'column' => 'txp_plugin.textpack',
'label' => 'Textpack',
),
'status' => array(
'column' => 'txp_plugin.status',
'label' => gTxt('active'),
'type' => 'boolean',
),
'type' => array(
'column' => 'txp_plugin.type',
'label' => gTxt('type'),
'type' => 'numeric',
),
'load_order' => array(
'column' => 'txp_plugin.load_order',
'label' => gTxt('order'),
'type' => 'numeric',
),
)
);
$alias_yes = '1, Yes';
$alias_no = '0, No';
$search->setAliases('status', array($alias_no, $alias_yes));
list($criteria, $crit, $search_method) = $search->getFilter();
$search_render_options = array('placeholder' => 'search_plugins');
$total = safe_count('txp_plugin', $criteria);
$searchBlock =
n.tag(
$search->renderForm('plugin', $search_render_options),
'div', array(
'class' => 'txp-layout-4col-3span',
'id' => $event.'_control',
)
);
$contentBlock = '';
$existing_files = get_filenames(PLUGINPATH.DS, GLOB_ONLYDIR) or $existing_files = array();
foreach (safe_column_num('name', 'txp_plugin', 1) as $name) {
unset($existing_files[$name]);
}
$paginator = new \Textpattern\Admin\Paginator($event, 'plugin');
$limit = $paginator->getLimit();
list($page, $offset, $numPages) = pager($total, $limit, $page);
if ($total < 1) {
if ($crit !== '') {
$contentBlock .= graf(
span(null, array('class' => 'ui-icon ui-icon-info')).' '.
gTxt('no_results_found'),
array('class' => 'alert-block information')
);
}
} else {
$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, type",
'txp_plugin',
"$criteria ORDER BY $sort_sql LIMIT $offset, $limit"
);
$publicOn = get_pref('use_plugins');
$adminOn = get_pref('admin_side_plugins');
$contentBlock .=
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',
'tabindex' => 0,
'aria-label' => gTxt('list'),
)).
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(
'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('options'), 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 = array(
'event' => 'plugin',
'step' => 'plugin_edit',
'name' => $name,
'sort' => $sort,
'dir' => $dir,
'page' => $page,
'search_method' => $search_method,
'crit' => $crit,
'_txp_token' => form_token(),
);
$statusLink = status_link($status, $name, yes_no($status));
$statusDisplay = (!$publicOn && $type == 0) || (!$adminOn && in_array($type, array(3, 4))) || (!$publicOn && !$adminOn && in_array($type, array(0, 1, 3, 4, 5)))
? tag($statusLink, 's')
: $statusLink;
$contentBlock .= tr(
td(
fInput('checkbox', 'selected[]', $name), '', 'txp-list-col-multi-edit'
).
hCell(
href($name, $edit_url), '', ' class="txp-list-col-name" scope="row"'
).
td(
($author_uri ? href($author, $a['author_uri'], array('rel' => 'external')) : $author), '', '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(
$statusDisplay, '', 'txp-list-col-status'
).
td(
$load_order, '', 'txp-list-col-load-order'
).
td(
$manage_items, '', 'txp-list-col-manage'
),
$status ? ' class="active"' : ''
);
unset($name);
}
$contentBlock .=
n.tag_end('tbody').
n.tag_end('table').
n.tag_end('div'). // End of .txp-listtables.
plugin_multiedit_form($page, $sort, $dir, $crit, $search_method).
tInput().
n.tag_end('form');
}
if (!is_dir(PLUGINPATH) || !is_writeable(PLUGINPATH)) {
$createBlock =
graf(
span(null, array('class' => 'ui-icon ui-icon-alert')).' '.
gTxt('plugin_dir_not_writeable', array('{plugindir}' => PLUGINPATH)),
array('class' => 'alert-block warning')
).n;
} else {
$createBlock = tag(plugin_form($existing_files), 'div', array('class' => 'txp-control-panel'));
}
$pageBlock = $paginator->render().
nav_form('plugin', $page, $numPages, $sort, $dir, $crit, $search_method, $total, $limit);
$table = new \Textpattern\Admin\Table();
echo $table->render(compact('total', 'crit') + array('heading' => 'tab_plugins'), $searchBlock, $createBlock, $contentBlock, $pageBlock);
}
/**
* Toggles a plugin's status.
*/
function switch_status()
{
extract(array_map('assert_string', gpsa(array('thing', 'value'))));
$change = ($value == gTxt('yes')) ? 0 : 1;
Txp::get('\Textpattern\Plugin\Plugin')->changestatus($thing, $change);
echo gTxt($change ? 'yes' : 'no');
}
/**
* Renders and outputs the plugin editor panel.
*/
function plugin_edit()
{
$name = gps('name');
pagetop(gTxt('edit_plugins'));
echo plugin_edit_form($name);
}
/**
* Plugin help viewer panel.
*/
function plugin_help()
{
$name = gps('name');
// Note that TEXTPATTERN_DEFAULT_LANG is not used here.
// The assumption is that plugin help is in English, unless otherwise stated.
$default_lang = $lang_plugin = 'en';
pagetop(gTxt('plugin_help'));
$help = ($name) ? safe_field('help', 'txp_plugin', "name = '".doSlash($name)."'") : '';
$helpArray = do_list($help, n);
if (preg_match('/^#@language\s+(.+)$/', $helpArray[0], $m)) {
$lang_plugin = $m[1];
$help = implode(n, array_slice($helpArray, 1));
}
if ($lang_plugin !== $default_lang) {
$direction = safe_field('data', 'txp_lang', "lang = '".doSlash($lang_plugin)."' AND name='lang_dir'");
}
if (empty($direction) || !in_array($direction, array('ltr', 'rtl'))) {
$direction = 'ltr';
}
echo n.tag($help, 'div', array(
'class' => 'txp-layout-textbox',
'lang' => $lang_plugin,
'dir' => $direction,
));
}
/**
* 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(
sLink('plugin', '', gTxt('cancel'), 'txp-button').
fInput('submit', '', gTxt('save'), 'publish'),
array('class' => 'txp-edit-actions')
).
eInput('plugin').
sInput('plugin_save').
hInput('name', $name).
hInput('sort', gps('sort')).
hInput('dir', gps('dir')).
hInput('page', gps('page')).
hInput('search_method', gps('search_method')).
hInput('crit', gps('crit')).
hInput('name', $name), '', '', 'post', 'edit-plugin-code', '', 'plugin_details');
}
/**
* Saves edited plugin code.
*/
function plugin_save()
{
extract(array_map('assert_string', gpsa(array('name', 'code'))));
safe_update('txp_plugin', "code = '".doSlash($code)."'", "name = '".doSlash($name)."'");
Txp::get('\Textpattern\Plugin\Plugin')->updateFile($name, $code);
$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()
{
$plugin64 = assert_string(ps('plugin'));
if ($plugin = Txp::get('\Textpattern\Plugin\Plugin')->extract($plugin64)) {
$source = '';
$textpack = '';
if (isset($plugin['help_raw']) && empty($plugin['allow_html_help'])) {
$textile = new \Textpattern\Textile\RestrictedParser();
$help_source = $textile->setLite(false)->setImages(true)->parse($plugin['help_raw']);
} else {
$help_source = $plugin['help'] ? str_replace(array(t), array(sp.sp.sp.sp), txpspecialchars($plugin['help'])) : '';
}
if (isset($plugin['textpack'])) {
$textpack = $plugin['textpack'];
}
$source .= txpspecialchars($plugin['code']);
$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(
tag($source, 'code', array(
'class' => 'language-php',
'dir' => 'ltr',
)),
'pre', array('id' => 'preview-plugin')
).
($help_source
? hed(gTxt('plugin_help'), 2).
tag(
tag($help_source, 'code', array(
'class' => 'language-markup',
'dir' => 'ltr',
)),
'pre', array('id' => 'preview-help')
)
: ''
).
($textpack
? hed(tag('Textpack', 'bdi', array('dir' => 'ltr')), 2).
tag(
tag($textpack, 'code', array('dir' => 'ltr')), 'pre', array('id' => 'preview-textpack')
)
: ''
).
$sub.
sInput('plugin_install').
eInput('plugin').
hInput('plugin64', $plugin64), '', '', 'post', 'plugin-info', '', 'plugin_preview'
);
return;
}
plugin_list(array(gTxt('bad_plugin_code'), E_ERROR));
}
/**
* Installs a plugin.
*/
function plugin_install()
{
$plugin64 = assert_string(ps('plugin64'));
$message = Txp::get('\Textpattern\Plugin\Plugin')->install($plugin64);
plugin_list($message);
}
/**
* Uploads a plugin.
*/
function plugin_upload()
{
$plugin = array();
if ($_FILES["theplugin"]["name"]) {
$filename = $_FILES["theplugin"]["name"];
$source = $_FILES["theplugin"]["tmp_name"];
$target_path = rtrim(get_pref('tempdir', PLUGINPATH), DS).DS.$filename;
if (move_uploaded_file($source, $target_path)) {
extract(pathinfo($target_path));
if (strtolower($extension) === 'php') {
$write = true;
$plugin = Txp::get('\Textpattern\Plugin\Plugin')->read(array($filename, $target_path));
} elseif (class_exists('ZipArchive')) {
$zip = new ZipArchive();
$x = $zip->open($target_path);
if ($x === true) {
for ($i = 0; $i < $zip->numFiles; $i++) {
if (strpos($zip->getNameIndex($i), $filename.'/') !== 0) {
$makedir = true;
break;
}
}
$zip->extractTo(PLUGINPATH.(empty($makedir) ? '' : DS.$filename));
$zip->close();
$plugin = Txp::get('\Textpattern\Plugin\Plugin')->read($filename);
}
}
unlink($target_path);
}
}
$message = Txp::get('\Textpattern\Plugin\Plugin')->install($plugin, null, !empty($write));
plugin_list($message);
}
/**
* Uploads a plugin.
*/
function plugin_load()
{
$plugin = array();
if ($filename = gps('filename')) {
$plugin = Txp::get('\Textpattern\Plugin\Plugin')->read($filename);
}
$message = Txp::get('\Textpattern\Plugin\Plugin')->install($plugin);
plugin_list($message);
}
/**
* Renders a plugin installation form.
*
* @param array $existing_files
* @return string HTML
* @access private
* @see form()
*/
function plugin_form($existing_files = array())
{
return tag(
tag(gTxt('upload_plugin'), 'label', ' for="plugin-upload"').popHelp('upload_plugin').
n.tag_void('input', array(
'type' => 'file',
'name' => 'theplugin',
'id' => 'plugin-upload',
'accept' => (class_exists('ZipArchive') ? "application/x-zip-compressed, application/zip, " : '').".php",
'required' => 'required',
)).
fInput('submit', 'install_new', gTxt('upload')).
eInput('plugin').
sInput('plugin_upload').
tInput().n, 'form', array(
'class' => 'plugin-file',
'id' => 'plugin_upload_form',
'method' => 'post',
'action' => 'index.php',
'enctype' => 'multipart/form-data'
)
).br.
($existing_files ? form(
eInput('plugin').
sInput('plugin_load').
tag(gTxt('import_from_disk'), 'label', array('for' => 'file-existing')).
selectInput('filename', $existing_files, null, false, '', 'file-existing').
fInput('submit', '', gTxt('import')),
'', '', 'post', 'assign-existing-form txp-async-update', '', 'assign_file'
) : '').
form(
tag(gTxt('install_plugin'), 'label', ' for="plugin-install"').popHelp('install_plugin').
n.''.
fInput('submit', 'install_new', gTxt('upload')).
eInput('plugin').
sInput('plugin_verify'), '', '', 'post', 'plugin-data', '', 'plugin_install_form'
);
}
/**
* Updates pageby value.
*/
function plugin_change_pageby()
{
global $event;
Txp::get('\Textpattern\Admin\Paginator', $event, 'plugin')->change();
plugin_list();
}
/**
* 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' => array(
'label' => gTxt('changestatus'),
'html' => onoffRadio('setStatus', 1),
),
'changeorder' => array(
'label' => gTxt('changeorder'),
'html' => $orders,
),
'update' => gTxt('update_from_disk'),
'delete' => array(
'label' => gTxt('delete'),
'html' => checkbox2('sync', gps('sync'), 0, 'sync').n.
tag(gTxt('plugin_delete_entirely'), 'label', array('for' => 'sync'))
)
);
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();
}
$plugin = new \Textpattern\Plugin\Plugin();
switch ($method) {
case 'delete':
foreach ($selected as $name) {
$plugin->delete($name);
}
break;
case 'changestatus':
foreach ($selected as $name) {
$plugin->changeStatus($name, ps('setStatus'));
}
break;
case 'changeorder':
foreach ($selected as $name) {
$plugin->changeOrder($name, ps('order'));
}
break;
case 'update':
foreach ($selected as $name) {
$plugin->install($plugin->read($name));
}
break;
}
$message = gTxt('plugin_'.($method == 'delete' ? 'deleted' : 'updated'), array('{name}' => join(', ', $selected)));
plugin_list($message);
}