[
'html' => '<',
'nonHtml' => '<'
],
'≤REAL_GT≥' => [
'html' => '>',
'nonHtml' => '>'
],
'≤REAL_NBSP≥' => [
'html' => ' ',
'nonHtml' => ' '
],
/*on post back the page is parsed, which turns & into &
this is done to prevent that from happening, we are just
protecting some chars from letting the parser nab them*/
'≤REAL_AMP≥' => [
'html' => '& ',
'nonHtml' => '& '
],
];
/*
* Parsing "options". Some of these are real parsing parameters, such as protect_email and security options.
* Others (like is_html) define the markup's semantic.
*
* TODO: Separate real parsing parameters from properties of the parsable markup
* TO DO: To ease usage tracking, it may be best to replace $option with individual properties.
* Or replace setOptions() with individual setters?
*/
public $option = []; // An associative array of (most) parameters (despite the singular)
public function setOptions($option = [])
{
global $page, $prefs;
$this->option = array_merge(
[
'is_html' => false,
'is_markdown' => (isset($prefs['markdown_enabled']) && $prefs['markdown_enabled'] === 'y' && $prefs['markdown_default'] === 'markdown'),
/* Determines if "Tiki syntax" is parsed in some circumstances.
Currently, when is_html is true, but that is probably wrong.
Overriden by the HTML plugin to force wiki parsing */
'parse_wiki' => ! isset($prefs['wysiwyg_wiki_parsed']) || $prefs['wysiwyg_wiki_parsed'] === 'y',
'absolute_links' => false,
'language' => '',
'noparseplugins' => false,
'stripplugins' => false,
'noheaderinc' => false,
'page' => $page,
'print' => false,
'parseimgonly' => false,
'preview_mode' => false,
'suppress_icons' => false,
'parsetoc' => true,
'inside_pretty' => false,
'process_wiki_paragraphs' => true,
'min_one_paragraph' => false,
'skipvalidation' => false,
'ck_editor' => false,
'namespace' => false,
'protect_email' => true,
'exclude_plugins' => [],
'exclude_all_plugins' => false,
'include_plugins' => [],
'typography' => true,
'autotoc' => false,
],
empty($option) ? [] : (array) $this->option,
(array)$option
);
$this->option['include_plugins'] = array_map('strtolower', $this->option['include_plugins']);
$this->option['exclude_plugins'] = array_map('strtolower', $this->option['exclude_plugins']);
}
public function __construct()
{
$this->setOptions();
}
//*
public function parse_data_raw($data)
{
$data = $this->parse_data($data);
$data = str_replace("tiki-index", "tiki-index_raw", $data);
return $data;
}
// This function handles wiki codes for those special HTML characters
// that textarea won't leave alone.
//*
protected function parse_htmlchar(&$data)
{
// cleaning some user input
// ckeditor parses several times and messes things up, we should only let it parse once
if (! $this->option['ck_editor']) {
$data = str_replace('&', '&', $data);
}
// oft-used characters (case insensitive)
$data = preg_replace("/~bs~/i", "\", $data);
$data = preg_replace("/~hs~/i", " ", $data);
$data = preg_replace("/~amp~/i", "&", $data);
$data = preg_replace("/~ldq~/i", "“", $data);
$data = preg_replace("/~rdq~/i", "”", $data);
$data = preg_replace("/~lsq~/i", "‘", $data);
$data = preg_replace("/~rsq~/i", "’", $data);
$data = preg_replace("/~c~/i", "©", $data);
$data = preg_replace("/~--~/", "—", $data);
$data = preg_replace("/ -- /", " — ", $data);
$data = preg_replace("/~lt~/i", "<", $data);
$data = preg_replace("/~gt~/i", ">", $data);
// HTML numeric character entities
$data = preg_replace("/~([0-9]+)~/", "$1;", $data);
}
// This function handles the protection of html entities so that they are not mangled when
// parse_htmlchar runs, and as well so they can be properly seen, be it html or non-html
public function protectSpecialChars($data, $is_html = false)
{
if ((isset($this->option['is_html']) && $this->option['is_html'] != true) || ! empty($this->option['ck_editor'])) {
foreach ($this->specialChars as $key => $specialChar) {
if ($this->option['is_markdown'] && $specialChar['html'] == '>') {
// markdown uses greater-than character for blockquotes and links, so we need to keep it
continue;
}
$data = str_replace($specialChar['html'], $key, $data);
}
}
return $data;
}
// This function removed the protection of html entities so that they are rendered as expected by the viewer
public function unprotectSpecialChars($data, $is_html = false)
{
if (
( $is_html != false || ( isset($this->option['is_html']) && $this->option['is_html']))
|| $this->option['ck_editor']
) {
foreach ($this->specialChars as $key => $specialChar) {
$data = str_replace($key, $specialChar['html'], $data);
}
} else {
foreach ($this->specialChars as $key => $specialChar) {
$data = str_replace($key, $specialChar['nonHtml'], $data);
}
}
return $data;
}
// Reverses parse_first.
//*
public function replace_preparse(&$data, &$preparsed, &$noparsed, $is_html = false)
{
$data1 = $data;
$data2 = "";
// Cook until done. Handles nested cases.
while ($data1 != $data2) {
$data1 = $data;
if (isset($noparsed["key"]) and count($noparsed["key"]) and count($noparsed["key"]) == count($noparsed["data"])) {
$data = str_replace($noparsed["key"], $noparsed["data"], $data);
}
if (isset($preparsed["key"]) and count($preparsed["key"]) and count($preparsed["key"]) == count($preparsed["data"])) {
$data = str_replace($preparsed["key"], $preparsed["data"], $data);
}
// nested keys in plugins like {CODE} get the section chars encoded
$data = preg_replace('/§(\w{32})§/', '§$1§', $data);
$data2 = $data;
}
$data = $this->unprotectSpecialChars($data, $is_html);
}
/**
* Replace plugins with guid keys and store them in an array
*
* @param $data string data to be cleaned of plugins
* @param $noparsed array output array
* @see parserLib::plugins_replace()
*/
public function plugins_remove(&$data, &$noparsed, $removeCb = null)
{
$tikilib = TikiLib::lib('tiki');
if (isset($removeCb) && ! is_callable($removeCb)) {
throw new Exception('Invalid callback');
}
$matches = WikiParser_PluginMatcher::match($data); // find the plugins
foreach ($matches as $match) { // each plugin
if (isset($removeCb) && ! $removeCb($match)) {
continue;
}
$plugin = (string) $match;
$key = '§' . md5($tikilib->genPass()) . '§'; // by replace whole plugin with a guid
$noparsed['key'][] = $key;
$noparsed['data'][] = $plugin;
}
$data = isset($noparsed['data']) ? str_replace($noparsed['data'], $noparsed['key'], $data) : $data;
}
/**
* Restore plugins from array
*
* @param $data string data previously processed with plugins_remove()
* @param $noparsed array input array
*/
public function plugins_replace(&$data, $noparsed, $is_html = false)
{
$preparsed = []; // unused
$noparsed['data'] = isset($noparsed['data']) ? str_replace(' and ', 'Data char: $i, $char, $curlies, $parens\n.
\n";
if ($char == "{") {
$curlies++;
} elseif ($char == "(" && $plugins['type'] == 'long') {
$parens++;
} elseif ($char == "}") {
$curlies--;
if ($plugins['type'] == 'short') {
$lastParens = $i;
}
} elseif ($char == ")" && $plugins['type'] == 'long') {
$parens--;
$lastParens = $i;
}
// If we found the end of the match...
if ($curlies == 0 && $parens == 0) {
break;
}
$i++;
}
if ($curlies == 0 && $parens == 0) {
$plugins[2] = (string) substr($data, $pos_end, $lastParens - $pos_end);
$plugins[0] = $plugins[0] . (string) substr($data, $pos_end, $i - $pos_end + 1);
/*
print "Match found: ";
print( $plugins[2] );
print "";
*/
}
$plugins['arguments'] = isset($plugins[2]) ? $this->plugin_split_args($plugins[2]) : [];
} else {
$plugins[1] = $plugins[0];
$plugins[2] = "";
}
}
/*
print "Plugin match end:";
print_r( $plugins );
print "";
*/
}
//*
public function plugin_split_args($params_string)
{
$parser = new WikiParser_PluginArgumentParser();
return $parser->parse($params_string);
}
// get all the plugins of a text- can be limitted only to some
//*
public function getPlugins($data, $only = null)
{
$plugins = [];
for (;;) {
$this->plugin_match($data, $plugin);
if (empty($plugin)) {
break;
}
if (empty($only) || in_array($plugin[1], $only) || in_array(TikiLib::strtoupper($plugin[1]), $only) || in_array(TikiLib::strtolower($plugin[1]), $only)) {
$plugins[] = $plugin;
}
$pos = strpos($data, $plugin[0]);
$data = substr_replace($data, '', $pos, strlen($plugin[0]));
}
return $plugins;
}
// Transitional wrapper over WikiParser_Parsable::parse_first()
// This recursive function handles pre- and no-parse sections and plugins
public function parse_first(&$data, &$preparsed, &$noparsed, $real_start_diff = '0')
{
return WikiParser_Parsable::instantiate('')->parse_first($data, $preparsed, $noparsed, $real_start_diff);
}
protected function strip_unparsed_block(&$data, &$noparsed, $protect = false)
{
$tikilib = TikiLib::lib('tiki');
$start = -1;
while (false !== $start = strpos($data, '~np~', $start + 1)) {
if (false !== $end = strpos($data, '~/np~', $start)) {
$content = substr($data, $start + 4, $end - $start - 4);
if ($protect) {
$content = $this->protectSpecialChars($content, $this->option['is_html']);
}
// ~pp~ type "plugins"
$key = "§" . md5($tikilib->genPass()) . "§";
$noparsed["key"][] = preg_quote($key);
$noparsed["data"][] = $content;
$data = substr($data, 0, $start) . $key . substr($data, $end + 5);
}
}
}
//
// Call 'wikiplugin_.*_description()' from given file
//
public function get_plugin_description($name, &$enabled, $area_id = 'editwiki')
{
if (( ! $info = $this->plugin_info($name) ) && $this->plugin_exists($name, true)) {
$enabled = true;
$func_name = "wikiplugin_{$name}_help";
if (! function_exists($func_name)) {
return false;
}
$ret = $func_name();
return $this->parse_data($ret);
} else {
$smarty = TikiLib::lib('smarty');
$enabled = true;
$ret = $info;
if (isset($ret['prefs'])) {
global $prefs;
// If the plugin defines required preferences, they should all be to 'y'
foreach ($ret['prefs'] as $pref) {
if (! isset($prefs[$pref]) || $prefs[$pref] != 'y') {
$enabled = false;
return;
}
}
}
if (isset($ret['documentation']) && ctype_alnum($ret['documentation'])) {
$ret['documentation'] = "http://doc.tiki.org/{$ret['documentation']}";
}
$smarty->assign('area_id', $area_id);
$smarty->assign('plugin', $ret);
$smarty->assign('plugin_name', TikiLib::strtoupper($name));
return $smarty->fetch('tiki-plugin_help.tpl');
}
}
//*
public function plugin_get_list($includeReal = true, $includeAlias = true)
{
return WikiPlugin_Negotiator_Wiki::getList($includeReal, $includeAlias);
}
//*
public function plugin_exists($name, $include = false)
{
$php_name = 'lib/wiki-plugins/wikiplugin_';
$php_name .= TikiLib::strtolower($name) . '.php';
$exists = file_exists($php_name);
if ($include && $exists) {
include_once $php_name;
}
if ($exists) {
return true;
} elseif ($info = WikiPlugin_Negotiator_Wiki_Alias::info($name)) {
// Make sure the underlying implementation exists
return $this->plugin_exists($info['implementation'], $include);
}
return false;
}
//*
public function plugin_info($name, $args = [])
{
static $known = [];
if (isset($known[$name]) && $name != 'package') {
return $known[$name];
}
if (! $this->plugin_exists($name, true)) {
return $known[$name] = false;
}
$func_name_info = "wikiplugin_{$name}_info";
if (! function_exists($func_name_info)) {
if ($info = WikiPlugin_Negotiator_Wiki_Alias::info($name)) {
return $known[$name] = $info['description'];
} else {
return $known[$name] = false;
}
}
// Support Tiki Packages param overrides for Package plugin
if ($name == 'package' && ! empty($args['package']) && ! empty($args['plugin'])) {
$info = $func_name_info();
$parts = explode('/', $args['package']);
if ($extensionPackage = \Tiki\Package\ExtensionManager::get($args['package'])) {
$path = $extensionPackage->getPath() . '/lib/wiki-plugins/' . $args['plugin'] . '.php';
} else {
$path = '';
}
if (! file_exists($path)) {
return $known[$name] = $info;
}
require_once($path);
$namespace = $extensionPackage->getBaseNamespace();
if (! empty($namespace)) {
$namespace .= '\\PackagePlugins\\';
}
$functionname = $namespace . $args['plugin'] . "_info";
if (! function_exists($functionname)) {
return $known[$name] = $info;
}
$viewinfo = $functionname();
if (isset($viewinfo['params'])) {
$combinedparams = $viewinfo['params'] + $info['params'];
} else {
$combinedparams = $info['params'];
}
$info = $viewinfo + $info;
$info['params'] = $combinedparams;
return $known[$name] = $info;
}
return $known[$name] = $func_name_info();
}
//*
public function plugin_alias_info($name)
{
return WikiPlugin_Negotiator_Wiki_Alias::info($name);
}
//*
public function plugin_alias_store($name, $data)
{
return WikiPlugin_Negotiator_Wiki_Alias::store($name, $data);
}
//*
public function plugin_alias_delete($name)
{
return WikiPlugin_Negotiator_Wiki_Alias::delete($name);
}
//*
public function plugin_enabled($name, &$output)
{
if (! $meta = $this->plugin_info($name)) {
return true; // Legacy plugins always execute
}
global $prefs;
$missing = [];
if (isset($meta['prefs'])) {
foreach ($meta['prefs'] as $pref) {
if ($prefs[$pref] != 'y') {
$missing[] = $pref;
}
}
}
if (count($missing) > 0) {
$output = WikiParser_PluginOutput::disabled($name, $missing);
return false;
}
return true;
}
//*
public function plugin_is_inline($name)
{
if (! $meta = $this->plugin_info($name)) {
return true; // Legacy plugins always inline
}
global $prefs;
$inline = false;
if (isset($meta['inline']) && $meta['inline']) {
return true;
}
$inline_pref = 'wikiplugininline_' . $name;
if (isset($prefs[ $inline_pref ]) && $prefs[ $inline_pref ] == 'y') {
return true;
}
return false;
}
/**
* Check if possible to execute a plugin
*
* @param string $name
* @param string $data
* @param array $args
* @param bool $dont_modify
* @return bool|string Boolean true if can execute, string 'rejected' if can't execute and plugin fingerprint if pending
*/
//*
public function plugin_can_execute($name, $data = '', $args = [], $dont_modify = false)
{
global $prefs;
// If validation is disabled, anything can execute
if ($prefs['wiki_validate_plugin'] != 'y') {
return true;
}
$meta = $this->plugin_info($name, $args);
if (! isset($meta['validate'])) {
return true;
}
$fingerprint = $this->plugin_fingerprint($name, $meta, $data, $args);
if ($fingerprint === '') { // only args or body were being validated and they're empty or safe
return true;
}
$val = $this->plugin_fingerprint_check($fingerprint, $dont_modify);
if (strpos($val, 'accept') === 0) {
return true;
} elseif (strpos($val, 'reject') === 0) {
return 'rejected';
} else {
global $tiki_p_plugin_approve, $tiki_p_plugin_preview, $user;
if (
isset($_SERVER['REQUEST_METHOD'])
&& $_SERVER['REQUEST_METHOD'] == 'POST'
&& isset($_POST['plugin_fingerprint'])
&& $_POST['plugin_fingerprint'] == $fingerprint
) {
if ($tiki_p_plugin_approve == 'y') {
if (isset($_POST['plugin_accept'])) {
$tikilib = TikiLib::lib('tiki');
$this->plugin_fingerprint_store($fingerprint, 'accept');
$tikilib->invalidate_cache($this->option['page']);
return true;
} elseif (isset($_POST['plugin_reject'])) {
$tikilib = TikiLib::lib('tiki');
$this->plugin_fingerprint_store($fingerprint, 'reject');
$tikilib->invalidate_cache($this->option['page']);
return 'rejected';
}
}
if (
$tiki_p_plugin_preview == 'y'
&& isset($_POST['plugin_preview'])
) {
return true;
}
}
return $fingerprint;
}
}
//*
public function plugin_fingerprint_check($fp, $dont_modify = false)
{
global $user;
$tikilib = TikiLib::lib('tiki');
$limit = date('Y-m-d H:i:s', time() - 15 * 24 * 3600);
$result = $this->query("SELECT `status`, if (`status`='pending' AND `last_update` < ?, 'old', '') flag FROM `tiki_plugin_security` WHERE `fingerprint` = ?", [ $limit, $fp ]);
$needUpdate = false;
if ($row = $result->fetchRow()) {
$status = $row['status'];
$flag = $row['flag'];
if ($status == 'accept' || $status == 'reject') {
return $status;
}
if ($flag == 'old') {
$needUpdate = true;
}
} else {
$needUpdate = true;
}
if ($needUpdate && ! $dont_modify) {
if ($this->option['page']) {
$objectType = 'wiki page';
$objectId = $this->option['page'];
} else {
$objectType = '';
$objectId = '';
}
if (! $user) {
$user = tra('Anonymous');
}
$pluginSecurity = $tikilib->table('tiki_plugin_security');
$pluginSecurity->delete(['fingerprint' => $fp]);
$pluginSecurity->insert(
['fingerprint' => $fp, 'status' => 'pending', 'added_by' => $user, 'last_objectType' => $objectType, 'last_objectId' => $objectId]
);
}
return '';
}
//*
public function plugin_fingerprint_store($fp, $type)
{
global $prefs, $user;
$tikilib = TikiLib::lib('tiki');
if ($this->option['page']) {
$objectType = 'wiki page';
$objectId = $this->option['page'];
} else {
$objectType = '';
$objectId = '';
}
$pluginSecurity = $tikilib->table('tiki_plugin_security');
$pluginSecurity->delete(['fingerprint' => $fp]);
$pluginSecurity->insert(
['fingerprint' => $fp,'status' => $type,'added_by' => $user,'last_objectType' => $objectType,'last_objectId' => $objectId]
);
}
//*
public function plugin_clear_fingerprint($fp)
{
$tikilib = TikiLib::lib('tiki');
$pluginSecurity = $tikilib->table('tiki_plugin_security');
$pluginSecurity->delete(['fingerprint' => $fp]);
}
//*
public function list_plugins_pending_approval()
{
$tikilib = TikiLib::lib('tiki');
return $tikilib->fetchAll("SELECT `fingerprint`, `added_by`, `last_update`, `last_objectType`, `last_objectId` FROM `tiki_plugin_security` WHERE `status` = 'pending' ORDER BY `last_update` DESC");
}
/**
* Return a list of plugins by status
*
* @param string|array $statuses
* @return array
*/
public function listPluginsByStatus($statuses)
{
if (! empty($statuses) && ! is_array($statuses)) {
$statuses = [$statuses];
}
$tikiLib = TikiLib::lib('tiki');
$pluginSecurity = $tikiLib->table('tiki_plugin_security');
return $pluginSecurity->fetchAll(
['fingerprint', 'added_by', 'last_update', 'last_objectType', 'last_objectId', 'status'],
['status' => $pluginSecurity->in($statuses)],
-1,
-1,
['last_update' => 'DESC']
);
}
//*
public function approve_all_pending_plugins()
{
global $user;
$tikilib = TikiLib::lib('tiki');
$pluginSecurity = $tikilib->table('tiki_plugin_security');
$pluginSecurity->updateMultiple(['status' => 'accept', 'approval_by' => $user], ['status' => 'pending',]);
}
//*
public function approve_selected_pending_plugings($fp)
{
global $user;
$tikilib = TikiLib::lib('tiki');
$pluginSecurity = $tikilib->table('tiki_plugin_security');
$pluginSecurity->update(['status' => 'accept', 'approval_by' => $user], ['fingerprint' => $fp]);
}
//*
public function plugin_fingerprint($name, $meta, $data, $args)
{
$validate = (isset($meta['validate']) ? $meta['validate'] : '');
$data = $this->unprotectSpecialChars($data, true);
if ($validate == 'all' || $validate == 'body') {
// Tiki 6 and ulterior may insert sequences in plugin body to break XSS exploits. The replacement works around removing them to keep fingerprints identical for upgrades from previous versions.
$validateBody = str_replace('
s from non-html
$data = str_replace(['
', "\n", $data);
}
if ($this->contains_html_block($plugin_result)) {
$elem = 'div';
} else {
$elem = 'span';
}
$elem_style = 'position:relative;display:inline-block;';
if (! $enabled) {
$elem_style .= 'opacity:0.3;';
}
if (in_array($name, ['img', 'div']) && preg_match('/<' . $name . '[^>]*style="(.*?)"/i', $plugin_result, $m)) {
if (count($m)) {
$elem_style .= $m[1];
}
}
$ret = '~np~<' . $elem . ' contenteditable="false" unselectable="on" class="tiki_plugin" data-plugin="' . $name . '" style="' . $elem_style . '"' .
' data-syntax="' . htmlentities($ck_editor_plugin, ENT_QUOTES, 'UTF-8') . '"' .
' data-args="' . htmlentities($arg_str, ENT_QUOTES, 'UTF-8') . '"' .
' data-body="' . htmlentities($data, ENT_QUOTES, 'UTF-8') . '">' . // not ' . $elem . '>~/np~';
return $ret;
}
public function find_plugins($data, $name = null)
{
$parserlib = TikiLib::lib('parser');
$argumentParser = new WikiParser_PluginArgumentParser();
$matches = WikiParser_PluginMatcher::match($data);
$occurrences = [];
foreach ($matches as $match) {
$plugin = [
'name' => $match->getName(),
'arguments' => $argumentParser->parse($match->getArguments()),
'body' => $match->getBody(),
];
$dummy_output = '';
if ($parserlib->plugin_enabled($plugin['name'], $dummy_output)) {
if ($name === null || $plugin['name'] == $name) {
$occurrences[] = $plugin;
}
}
}
return $occurrences;
}
public function process_save_plugins($data, array $context)
{
$parserlib = TikiLib::lib('parser');
$argumentParser = new WikiParser_PluginArgumentParser();
$matches = WikiParser_PluginMatcher::match($data);
foreach ($matches as $match) {
$plugin_name = $match->getName();
$body = $match->getBody();
$arguments = $argumentParser->parse($match->getArguments());
$dummy_output = '';
if ($parserlib->plugin_enabled($plugin_name, $dummy_output)) {
$func_name = 'wikiplugin_' . $plugin_name . '_rewrite';
if (function_exists($func_name)) {
$parserlib->plugin_apply_filters($plugin_name, $data, $arguments);
$output = $func_name($body, $arguments, $context);
if ($output !== false) {
$match->replaceWith($output);
}
}
if ($plugin_name == 'translationof') {
$this->add_translationof_relation($data, $arguments, $context['itemId']);
}
}
}
$matches_text = $matches->getText();
return $matches_text;
}
//*
protected function plugin_apply_filters($name, &$data, &$args)
{
$tikilib = TikiLib::lib('tiki');
$info = $this->plugin_info($name, $args);
$default = TikiFilter::get(isset($info['defaultfilter']) ? $info['defaultfilter'] : 'xss');
// Apply filters on the body
$filter = isset($info['filter']) ? TikiFilter::get($info['filter']) : $default;
//$data = TikiLib::htmldecode($data); // jb 9.0 commented out in fix for html entitles
$data = $filter->filter($data);
if (isset($this->option) && (! empty($this->option['is_html']) && (! $this->option['is_html']))) {
$noparsed = ['data' => [], 'key' => []];
$this->strip_unparsed_block($data, $noparsed);
$data = str_replace(['<', '>'], ['<', '>'], $data);
foreach ($noparsed['data'] as &$instance) {
$instance = '~np~' . $instance . '~/np~';
}
unset($instance);
$data = str_replace($noparsed['key'], $noparsed['data'], $data);
}
// Make sure all arguments are declared
if (isset($info['params'])) {
$params = $info['params'];
}
$argsCopy = $args;
if (! isset($info['extraparams']) && isset($params) && is_array($params)) {
$args = array_intersect_key($args, $params);
}
// Apply filters on values individually
if (! empty($args)) {
foreach ($args as $argKey => &$argValue) {
if (! isset($params[$argKey])) {
continue;// extra params
}
$paramInfo = $params[$argKey];
$filter = isset($paramInfo['filter']) ? TikiFilter::get($paramInfo['filter']) : $default;
$argValue = TikiLib::htmldecode($argValue);
if (isset($paramInfo['separator'])) {
$vals = [];
$vals = $tikilib->array_apply_filter($tikilib->multi_explode($paramInfo['separator'], $argValue), $filter);
$argValue = array_values($vals);
} else {
$argValue = $filter->filter($argValue);
}
}
}
}
//*
protected function convert_plugin_output($output, $from, $to)
{
if (! $output instanceof WikiParser_PluginOutput) {
if ($from === 'wiki') {
$output = WikiParser_PluginOutput::wiki($output);
} elseif ($from === 'html') {
$output = WikiParser_PluginOutput::html($output);
}
}
if ($to === 'html') {
return $output->toHtml($this->option);
} elseif ($to === 'wiki') {
return $output->toWiki();
}
}
//*
public function plugin_replace_args($content, $rules, $args)
{
$patterns = [];
$replacements = [];
foreach ($rules as $token => $info) {
$patterns[] = "%$token%";
if (isset($info['input']) && ! empty($info['input'])) {
$token = $info['input'];
}
if (isset($args[$token])) {
$value = $args[$token];
} else {
$value = isset($info['default']) ? $info['default'] : '';
}
switch (isset($info['encoding']) ? $info['encoding'] : 'none') {
case 'html':
$replacements[] = htmlentities($value, ENT_QUOTES, 'UTF-8');
break;
case 'url':
$replacements[] = rawurlencode($value);
break;
default:
$replacements[] = $value;
}
}
return str_replace($patterns, $replacements, $content);
}
//*
public function plugin_is_editable($name, $pluginArgs = null)
{
global $tiki_p_edit, $prefs, $section;
$info = $this->plugin_info($name);
$is_allowed = $this->check_permission_from_plugin_params($pluginArgs);
// note that for 3.0 the plugin editor only works in wiki pages, but could be extended later
return $section == 'wiki page' && $info && ($tiki_p_edit == 'y' || $is_allowed == 'y') && $prefs['wiki_edit_plugin'] == 'y'
&& ! $this->plugin_is_inline($name);
}
// Checking permission from plugin params
public function check_permission_from_plugin_params($pluginArgs) {
global $user;
$userlib = TikiLib::lib('user');
$is_allowed = 'n';
if (isset($pluginArgs)) {
if (isset($pluginArgs['editable_by_user'])) {
$usersAllowed = array_map('trim', explode(',', $pluginArgs['editable_by_user']));
foreach ($usersAllowed as $allowedUser) {
if (strtolower($user) == strtolower($allowedUser)) {
$is_allowed = 'y';
return $is_allowed;
}
}
}
if (isset($pluginArgs['editable_by_groups'])) {
$userGroups = array_map('strtolower',$userlib->get_user_info($user)['groups']);
$groupsAllowed = array_map('strtolower', explode(',',$pluginArgs['editable_by_groups']));
foreach ($groupsAllowed as $group) {
if (in_array($group, $userGroups)) {
$is_allowed = 'y';
return $is_allowed;
}
}
}
}
return $is_allowed;
}
/**
* Gets a wiki parseable content and substitutes links for $oldName by
* links for $newName.
*
* @param string wiki parseable content
* @param string old page name
* @param string new page name
* @return string new wiki parseable content with links replaced
*/
public function replace_links($data, $oldName, $newName)
{
global $prefs;
$quotedOldName = preg_quote($oldName, '/');
$semanticlib = TikiLib::lib('semantic');
// FIXME: Affects non-parsed sections
foreach ($semanticlib->getAllTokens() as $sem) {
$data = str_replace("($sem($oldName", "($sem($newName", $data);
}
if ($prefs['feature_wikiwords'] == 'y') {
if (strstr($newName, ' ')) {
$data = preg_replace("/(?<= |\n|\t|\r|\,|\;|^)$quotedOldName(?= |\n|\t|\r|\,|\;|$)/", '((' . $newName . '))', $data);
} else {
$data = preg_replace("/(?<= |\n|\t|\r|\,|\;|^)$quotedOldName(?= |\n|\t|\r|\,|\;|$)/", $newName, $data);
}
}
$data = preg_replace("/(?<=\(\()$quotedOldName(?=\)\)|\|)/i", $newName, $data);
$data = preg_replace("/(?<=\]\()$quotedOldName(?=\))/i", $newName, $data);
$quotedOldHtmlName = preg_quote(urlencode($oldName), '/');
$htmlSearch = '//i';
$data = preg_replace($htmlWantedSearch, '((' . $newName . '))', $data);
return $data;
}
// Replace hotwords in given line
//*
public function replace_hotwords($line, $words = null)
{
global $prefs;
if ($prefs['feature_hotwords'] == 'y') {
$hotw_nw = ($prefs['feature_hotwords_nw'] == 'y') ? "target='_blank'" : '';
// FIXME: Replacements may fail if the value contains an unescaped metacharacters (which is why the default value contains escape characters). The value should probably be escaped with preg_quote().
$sep = empty($prefs['feature_hotwords_sep']) ? " \n\t\r\,\;\(\)\.\:\[\]\{\}\!\?\"" : $prefs['feature_hotwords_sep'];
if (is_null($words)) {
$words = $this->get_hotwords();
}
foreach ($words as $word => $url) {
$escapedWord = preg_quote($word, '/');
/* In CVS revisions 1.429 and 1.373.2.40 of tikilib.php, mose added the following magic steps, commenting "fixed the hotwords autolinking in case it is in a description field".
* I do not understand what description fields this refers to. I do not know if this is correct and still needed. Step 1 seems to prevent replacements in an HTML tag. Chealer 2017-03-08
*/
// Step 1: Insert magic sequences which will neutralize step 2 in some cases.
$line = preg_replace("/(=(\"|')[^\"']*[$sep'])$escapedWord([$sep][^\"']*(\"|'))/i", "$1:::::$word,:::::$3", $line);
// Step 2: Add links where the hotword appears (not neutralized)
$line = preg_replace("/([$sep']|^)$escapedWord($|[$sep])/i", "$1$word$2", $line);
// Step 3: Remove magic sequences inserted in step 1
$line = preg_replace("/:::::$escapedWord,:::::/i", "$word", $line);
}
}
return $line;
}
// Make plain text URIs in text into clickable hyperlinks
// check to see if autolinks is enabled before calling this function ($prefs['feature_autolinks'] == "y")
//*
public function autolinks($text)
{
if ($text) {
global $prefs;
static $mail_protect_pattern = '';
$attrib = '';
if ($prefs['popupLinks'] == 'y') {
$attrib .= 'target="_blank" ';
}
if ($prefs['feature_wiki_ext_icon'] == 'y') {
$attrib .= 'class="wiki external" ';
include_once(__DIR__ . '/../smarty_tiki/function.icon.php');
$ext_icon = smarty_function_icon(['name' => 'link-external'], TikiLib::lib('smarty')->getEmptyInternalTemplate());
} else {
$attrib .= 'class="wiki" ';
$ext_icon = "";
}
// add a space so we can match links starting at the beginning of the first line
$text = " " . $text;
$patterns = [];
$replacements = [];
// protocol://suffix
$patterns[] = "#([\n\( ])([a-z0-9]+?)://([^<,\) \n\r]+)#i";
$replacements[] = "\\1\\2://\\3$ext_icon";
// www.domain.ext/optionalpath
$patterns[] = "#([\n ])www\.([a-z0-9\-]+)\.([a-z0-9\-.\~]+)((?:/[^,< \n\r]*)?)#i";
$replacements[] = "\\1www.\\2.\\3\\4$ext_icon";
// email address (foo@domain.ext)
$patterns[] = "#([\n ])([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i";
if ($this->option['protect_email'] && $this->option['print'] !== 'y' && $prefs['feature_wiki_protect_email'] == 'y') {
if (! $mail_protect_pattern) {
$mail_protect_pattern = "\\1" . TikiLib::protect_email("\\2", "\\3");
}
$replacements[] = $mail_protect_pattern;
} else {
$replacements[] = "\\1\\2@\\3";
}
$patterns[] = "#([\n ])magnet\:\?([^,< \n\r]+)#i";
$replacements[] = "\\1magnet:?\\2";
$text = preg_replace($patterns, $replacements, $text);
// strip the space we added
$text = substr($text, 1);
}
return $text;
// } else {
// return $text;
// }
}
/**
* close_blocks - Close out open paragraph, lists, and div's
*
* During parse_data, information is kept on blocks of text (paragraphs, lists, divs)
* that need to be closed out. This function does that, rather than duplicating the
* code inline.
*
* @param $data - Output data
* @param $in_paragraph - TRUE if there is an open paragraph
* @param $listbeg - array of open list terminators
* @param $divdepth - array indicating how many div's are open
* @param $close_paragraph - TRUE if open paragraph should be closed.
* @param $close_lists - TRUE if open lists should be closed.
* @param $close_divs - TRUE if open div's should be closed.
*/
/* private */
//*
public function close_blocks(&$data, &$in_paragraph, &$listbeg, &$divdepth, $close_paragraph, $close_lists, $close_divs)
{
$closed = 0; // Set to non-zero if something has been closed out
// Close the paragraph if inside one.
if ($close_paragraph && $in_paragraph) {
$data .= "
tags when parsed, and also respects HTML rendering preferences. * * @param $data string wiki/html to be parsed * @param $optionsOverride array options to override the current defaults * @param $inlineFirstP boolean If the returned data starts with a
, this option will force it to display as inline:block * useful when the returned data is required to display without adding overhead spacing caused by
* * @return string parsed data */ public function parse_data_plugin($data, $inlineFirstP = false, $optionsOverride = []) { $options['is_html'] = ($GLOBALS['prefs']['feature_wiki_allowhtml'] === 'y' && isset($GLOBALS['info']['is_html']) && $GLOBALS['info']['is_html'] == true) ? true : false; foreach ($optionsOverride as $name => $value) { $options[$name] = $value; } // record initial whitespace preg_match('(^\s*)', $data, $bwhite); preg_match('(\s*$)', $data, $ewhite); // remove all the whitespace $data = trim($data); $data = $this->parse_data($data, $options); // remove whitespace that was added while parsing (yes it does happen) $data = trim($data); if ($inlineFirstP) { $data = preg_replace('/^(\s*?)
/', '$1
', ' ' . $data);
}
// add original whitespace back to preserve spacing
return ($bwhite[0] . $data . $ewhite[0]);
}
/** Simpler and faster parse than parse_data()
* This is only called from the parse Smarty modifier, for preference definitions.
*/
public function parse_data_simple($data)
{
$data = $this->parse_data_wikilinks($data, true);
$data = $this->parse_data_externallinks($data, true);
$data = $this->parse_data_inline_syntax($data);
if ($this->option['typography'] && ! $this->option['ck_editor']) {
$data = typography($data, $this->option['language']);
}
return $data;
}
//*
protected function parse_data_wikilinks($data, $simple_wiki, $ck_editor = false) //TODO: need a wikilink handler
{
global $page_regex, $prefs;
// definitively put out the protected words ))protectedWord((
if ($prefs['feature_wikiwords'] == 'y') {
preg_match_all("/\)\)(\S+?)\(\(/", $data, $matches);
$noParseWikiLinksK = [];
$noParseWikiLinksT = [];
foreach ($matches[0] as $mi => $match) {
do {
$randNum = chr(0xff) . rand(0, 1048576) . chr(0xff);
} while (strstr($data, $randNum));
$data = str_replace($match, $randNum, $data);
$noParseWikiLinksK[] = $randNum;
$noParseWikiLinksT[] = $matches[1][$mi];
}
}
// Links with description
preg_match_all("/\(([a-z0-9-]+)?\(($page_regex)\|([^\)]*?)\)\)/", $data, $pages);
$temp_max = count($pages[1]);
for ($i = 0; $i < $temp_max; $i++) {
$exactMatch = $pages[0][$i];
$description = $pages[6][$i];
$anchor = null;
if ($description && $description[0] == '#') {
$temp = $description;
$anchor = strtok($temp, '|');
$description = strtok('|');
}
$replacement = $this->get_wiki_link_replacement($pages[2][$i], ['description' => $description,'reltype' => $pages[1][$i],'anchor' => $anchor], $ck_editor);
$data = str_replace($exactMatch, $replacement, $data);
}
// Wiki page syntax without description
preg_match_all("/\(([a-z0-9-]+)?\( *($page_regex) *\)\)/", $data, $pages);
foreach ($pages[2] as $idx => $page_parse) {
$exactMatch = $pages[0][$idx];
$replacement = $this->get_wiki_link_replacement($page_parse, [ 'reltype' => $pages[1][$idx] ], $ck_editor);
$data = str_replace($exactMatch, $replacement, $data);
}
// Links to internal pages
// If they are parenthesized then don't treat as links
// Prevent ))PageName(( from being expanded \"\'
//[A-Z][a-z0-9_\-]+[A-Z][a-z0-9_\-]+[A-Za-z0-9\-_]*
if ($prefs['feature_wiki'] == 'y' && $prefs['feature_wikiwords'] == 'y') {
if (! $simple_wiki) {
// The first part is now mandatory to prevent [Foo|MyPage] from being converted!
if ($prefs['feature_wikiwords_usedash'] == 'y') {
preg_match_all("/(?<=[ \n\t\r\,\;]|^)([A-Z][a-z0-9_\-\x80-\xFF]+[A-Z][a-z0-9_\-\x80-\xFF]+[A-Za-z0-9\-_\x80-\xFF]*)(?=$|[ \n\t\r\,\;\.])/", $data, $pages);
} else {
preg_match_all("/(?<=[ \n\t\r\,\;]|^)([A-Z][a-z0-9\x80-\xFF]+[A-Z][a-z0-9\x80-\xFF]+[A-Za-z0-9\x80-\xFF]*)(?=$|[ \n\t\r\,\;\.])/", $data, $pages);
}
//TODO to have a real utf8 Wikiword where the capitals can be a utf8 capital
$words = ($prefs['feature_hotwords'] == 'y') ? $this->get_hotwords() : [];
foreach (array_unique($pages[1]) as $page_parse) {
if (! array_key_exists($page_parse, $words)) { // If this is not a hotword
$repl = $this->get_wiki_link_replacement($page_parse, ['plural' => $prefs['feature_wiki_plurals'] == 'y'], $ck_editor);
$data = preg_replace("/(?<=[ \n\t\r\,\;]|^)$page_parse(?=$|[ \n\t\r\,\;\.])/", "$1" . $repl . "$2", $data);
}
}
}
// Reinsert ))Words((
$data = str_replace($noParseWikiLinksK, $noParseWikiLinksT, $data);
}
return $data;
}
protected function parse_data_externallinks($data, $suppress_icons = false)
{
global $prefs;
$tikilib = TikiLib::lib('tiki');
// *****
// This section handles external links of the form [url] and such.
// *****
$links = $tikilib->get_links($data);
$notcachedlinks = $tikilib->get_links_nocache($data);
$cachedlinks = array_diff($links, $notcachedlinks);
$tikilib->cache_links($cachedlinks);
// Note that there're links that are replaced
foreach ($links as $link) {
$target = '';
$class = 'class="wiki"';
$ext_icon = '';
$rel = '';
if ($prefs['popupLinks'] == 'y') {
$target = 'target="_blank"';
}
if (! strstr($link, '://')) {
$target = '';
} else {
$class = 'class="wiki external"';
if ($prefs['feature_wiki_ext_icon'] == 'y' && ! ($this->option['suppress_icons'] || $suppress_icons)) {
$smarty = TikiLib::lib('smarty');
include_once('lib/smarty_tiki/function.icon.php');
$ext_icon = smarty_function_icon(['name' => 'link-external'], $smarty->getEmptyInternalTemplate());
}
$rel = 'external';
if ($prefs['feature_wiki_ext_rel_nofollow'] == 'y') {
$rel .= ' nofollow';
}
}
// The (?is_cached($link)) {
//use of urlencode for using cached versions of dynamic sites
$cosa = "(cache)";
$link2 = str_replace("/", "\/", preg_quote($link));
$pattern = "/(?$1$ext_icon", $data);
$pattern = "/(?$1$ext_icon", $data);
} else {
$data = preg_replace($pattern, "$1$ext_icon $cosa", $data);
}
$pattern = "/(?$1$ext_icon $cosa", $data);
$pattern = "/(?$link$ext_icon $cosa", $data);
} else {
$link2 = str_replace("/", "\/", preg_quote($link));
$link = trim($link);
$link = str_replace('"', '%22', $link);
$data = str_replace("|nocache", "", $data);
$pattern = "/(?{$matches[1]}$ext_icon";
}, $data);
$pattern = "/(?$1$ext_icon", $data);
$pattern = "/(?$link$ext_icon", $data);
}
}
// Handle double square brackets. to display [foo] use [[foo] -rlpowell. Improved by sylvieg to avoid replacing them in [[code]] cases.
if (empty($this->option['process_double_brackets']) || $this->option['process_double_brackets'] != 'n') {
$data = preg_replace("/\[\[([^\]]*)\](?!\])/", "[$1]", $data);
$data = preg_replace("/\[\[([^\]]*)$/", "[$1", $data);
}
return $data;
}
//*
protected function parse_data_inline_syntax($line, $words = null, $ck_editor = false)
{
global $prefs;
// Replace monospaced text
$line = preg_replace("/(^|\s)-\+(.*?)\+-/", "$1$2", $line);
// Replace bold text
$line = preg_replace("/__(.*?)__/", "$1", $line);
// Replace italic text
$line = preg_replace("/\'\'(.*?)\'\'/", "$1", $line);
// Links that were ignored by convertAbsoluteLinksToRelative (autolinks will highlight)
$line = preg_replace("/(^|\s)==(.*?)==(\s|$)/", "$1$2$3", $line);
if (! $ck_editor) {
if ($prefs['feature_hotwords'] == 'y') {
// Replace Hotwords before begin
$line = $this->replace_hotwords($line, $words);
}
// Make plain URLs clickable hyperlinks
if ($prefs['feature_autolinks'] == 'y') {
$line = $this->autolinks($line);
}
}
if (! $ck_editor) {
// Replace definition lists
$line = preg_replace("/^;([^:]*):([^\/\/].*)/", "
| '; } // // for ($k ... $repl .= ' |
| '; } $repl .= ' |