<?php
|
|
|
|
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
|
|
//
|
|
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
|
|
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
|
|
// $Id$
|
|
|
|
/**
|
|
* A text of markup, usually using Tiki's syntax ("wiki syntax"), which can be parsed
|
|
*
|
|
* This class is a contextual version of ParserLib. ParserLib is not contextual.
|
|
* This class can be used to analyze 2 different pages in a single request and recognize those as different contexts. 2 fragments of the same wiki page can also be different contexts.
|
|
* The extension of ParserLib is hopefully temporary. Ideally ParserLib would be replaced by a more complete version of this class.
|
|
* TODO: Move remaining ParserLib methods and option property here
|
|
*/
|
|
abstract class WikiParser_Parsable extends ParserLib
|
|
{
|
|
/** @var string Code usually containing text and markup */
|
|
private $markup;
|
|
|
|
// Properties used by parallel parsing functions to share data
|
|
|
|
/** @var array Footnotes added via the FOOTNOTE plugin. These are read by wikiplugin_footnotearea(). */
|
|
public $footnotes;
|
|
|
|
public static function instantiate($data, $options = [])
|
|
{
|
|
global $prefs;
|
|
if (isset($options['is_markdown']) && $options['is_markdown'] && $prefs['markdown_enabled'] === 'y') {
|
|
return new WikiParser_ParsableMarkdown($data);
|
|
} else {
|
|
return new WikiParser_ParsableWiki($data);
|
|
}
|
|
}
|
|
|
|
public function __construct($markup)
|
|
{
|
|
$this->markup = $markup;
|
|
}
|
|
|
|
// This recursive function handles pre- and no-parse sections and plugins
|
|
public function parse_first(&$data, &$preparsed, &$noparsed, $real_start_diff = '0')
|
|
{
|
|
global $tikilib, $tiki_p_edit, $prefs, $pluginskiplist;
|
|
$smarty = TikiLib::lib('smarty');
|
|
$smarty->loadPlugin('smarty_function_icon');
|
|
|
|
if (! is_array($pluginskiplist)) {
|
|
$pluginskiplist = [];
|
|
}
|
|
|
|
$is_html = (isset($this->option['is_html']) ? $this->option['is_html'] : false);
|
|
$data = $this->protectSpecialChars($data, $is_html);
|
|
|
|
$matches = WikiParser_PluginMatcher::match($data);
|
|
$argumentParser = new WikiParser_PluginArgumentParser();
|
|
|
|
foreach ($matches as $match) {
|
|
if ($this->option['parseimgonly'] && $this->getName() != 'img') {
|
|
continue;
|
|
}
|
|
|
|
//note parent plugin in case of plugins nested in an include - to suppress plugin edit icons below
|
|
$plugin_parent = isset($plugin_name) ? $plugin_name : false;
|
|
$plugin_name = $match->getName();
|
|
|
|
if (! $this->option['exclude_all_plugins'] && ! empty($this->option['exclude_plugins']) && in_array($plugin_name, $this->option['exclude_plugins'])) {
|
|
$match->replaceWith('');
|
|
continue;
|
|
}
|
|
|
|
if ($this->option['exclude_all_plugins'] && (empty($this->option['include_plugins']) || ! in_array($plugin_name, $this->option['include_plugins']))) {
|
|
$match->replaceWith('');
|
|
continue;
|
|
}
|
|
|
|
$plugin_data = $match->getBody();
|
|
$arguments = $argumentParser->parse($match->getArguments());
|
|
$start = $match->getStart();
|
|
|
|
$pluginOutput = null;
|
|
if ($this->plugin_enabled($plugin_name, $pluginOutput) || $this->option['ck_editor']) {
|
|
static $plugin_indexes = [];
|
|
|
|
if (! array_key_exists($plugin_name, $plugin_indexes)) {
|
|
$plugin_indexes[$plugin_name] = 0;
|
|
}
|
|
|
|
$current_index = ++$plugin_indexes[$plugin_name];
|
|
|
|
// get info to test for preview with auto_save
|
|
if (! $this->option['skipvalidation']) {
|
|
$status = $this->plugin_can_execute($plugin_name, $plugin_data, $arguments, $this->option['preview_mode'] || $this->option['ck_editor']);
|
|
} else {
|
|
$status = true;
|
|
}
|
|
global $tiki_p_plugin_viewdetail, $tiki_p_plugin_preview, $tiki_p_plugin_approve;
|
|
$details = $tiki_p_plugin_viewdetail == 'y' && $status != 'rejected';
|
|
$preview = $tiki_p_plugin_preview == 'y' && $details && ! $this->option['preview_mode'];
|
|
$approve = $tiki_p_plugin_approve == 'y' && $details && ! $this->option['preview_mode'];
|
|
|
|
if ($status === true || ($tiki_p_plugin_preview == 'y' && $details && $this->option['preview_mode'] && $prefs['ajax_autosave'] === 'y') || (isset($this->option['noparseplugins']) && $this->option['noparseplugins'])) {
|
|
if (isset($this->option['stripplugins']) && $this->option['stripplugins']) {
|
|
$ret = $plugin_data;
|
|
} elseif (isset($this->option['noparseplugins']) && $this->option['noparseplugins']) {
|
|
$ret = '~np~' . (string) $match . '~/np~';
|
|
} else {
|
|
//suppress plugin edit icons for plugins within includes since edit doesn't work for these yet
|
|
$suppress_icons = $this->option['suppress_icons'];
|
|
$this->option['suppress_icons'] = $plugin_name != 'include' && $plugin_parent && $plugin_parent == 'include' ?
|
|
true : $this->option['suppress_icons'];
|
|
|
|
$ret = $this->pluginExecute($plugin_name, $plugin_data, $arguments, $start, false);
|
|
|
|
// restore previous suppress_icons state
|
|
$this->option['suppress_icons'] = $suppress_icons;
|
|
}
|
|
} else {
|
|
if ($status != 'rejected') {
|
|
$smarty->assign('plugin_fingerprint', $status);
|
|
$status = 'pending';
|
|
}
|
|
|
|
if ($this->option['ck_editor']) {
|
|
$ret = $this->convert_plugin_for_ckeditor($plugin_name, $arguments, tra('Plugin execution pending approval'), $plugin_data, ['icon' => 'img/icons/error.png']);
|
|
} else {
|
|
$smarty->assign('plugin_name', $plugin_name);
|
|
$smarty->assign('plugin_index', $current_index);
|
|
|
|
$smarty->assign('plugin_status', $status);
|
|
|
|
if (! $this->option['inside_pretty']) {
|
|
$smarty->assign('plugin_details', $details);
|
|
} else {
|
|
$smarty->assign('plugin_details', '');
|
|
}
|
|
$smarty->assign('plugin_preview', $preview);
|
|
$smarty->assign('plugin_approve', $approve);
|
|
|
|
$smarty->assign('plugin_body', $plugin_data);
|
|
$smarty->assign('plugin_args', $arguments);
|
|
|
|
$ret = '~np~' . $smarty->fetch('tiki-plugin_blocked.tpl') . '~/np~';
|
|
}
|
|
}
|
|
} else {
|
|
$ret = $pluginOutput->toWiki();
|
|
}
|
|
|
|
if ($ret === false) {
|
|
continue;
|
|
}
|
|
|
|
if ($this->plugin_is_editable($plugin_name, $arguments) && (empty($this->option['preview_mode']) || ! $this->option['preview_mode']) && empty($this->option['indexing']) && (empty($this->option['print']) || ! $this->option['print']) && ! $this->option['suppress_icons']) {
|
|
$headerlib = TikiLib::lib('header');
|
|
$smarty->loadPlugin('smarty_function_icon');
|
|
|
|
$id = 'plugin-edit-' . $plugin_name . $current_index;
|
|
if (strlen($plugin_data) > 2000) {
|
|
$plugin_data = '~same~';
|
|
}
|
|
|
|
$headerlib->add_js(
|
|
"\$(document).ready( function() {
|
|
if ( \$('#$id') ) {
|
|
\$('#$id').click( function(event) {
|
|
popupPluginForm("
|
|
. json_encode('editwiki')
|
|
. ', '
|
|
. json_encode($plugin_name)
|
|
. ', '
|
|
. json_encode($current_index)
|
|
. ', '
|
|
. json_encode($this->option['page'])
|
|
. ', '
|
|
. json_encode($arguments)
|
|
. ', '
|
|
. json_encode($this->unprotectSpecialChars($plugin_data, true)) //we restore it back to html here so that it can be edited, we want no modification, ie, it is brought back to html
|
|
. ", event.target);
|
|
} );
|
|
}
|
|
} );
|
|
"
|
|
);
|
|
|
|
$displayIcon = $prefs['wiki_edit_icons_toggle'] != 'y' || isset($_COOKIE['wiki_plugin_edit_view']) ? $_COOKIE['wiki_plugin_edit_view'] : true;
|
|
|
|
$ret .= '~np~' .
|
|
'<a id="' . $id . '" href="javascript:void(1)" class="editplugin"' . ($displayIcon ? '' : ' style="display:none;"') . '>' .
|
|
smarty_function_icon(['name' => 'plugin', 'iclass' => 'tips', 'ititle' => tra('Edit plugin') . ':' . ucfirst($plugin_name)], $smarty->getEmptyInternalTemplate()) .
|
|
'</a>' .
|
|
'~/np~';
|
|
}
|
|
|
|
// End plugin handling
|
|
|
|
$ret = str_replace('~/np~~np~', '', $ret);
|
|
$match->replaceWith($ret);
|
|
}
|
|
|
|
$data = $matches->getText();
|
|
|
|
$this->strip_unparsed_block($data, $noparsed);
|
|
|
|
// ~pp~
|
|
$start = -1;
|
|
while (false !== $start = strpos($data, '~pp~', $start + 1)) {
|
|
if (false !== $end = strpos($data, '~/pp~', $start)) {
|
|
$content = substr($data, $start + 4, $end - $start - 4);
|
|
|
|
// ~pp~ type "plugins"
|
|
$key = "§" . md5($tikilib->genPass()) . "§";
|
|
$noparsed["key"][] = preg_quote($key);
|
|
$noparsed["data"][] = '<pre>' . $content . '</pre>';
|
|
$data = substr($data, 0, $start) . $key . substr($data, $end + 5);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Standard parsing
|
|
* options defaults : is_html => false, absolute_links => false, language => ''
|
|
* @return string
|
|
*/
|
|
public function parse($options)
|
|
{
|
|
// Don't bother if there's nothing...
|
|
if (gettype($this->markup) <> 'string' || mb_strlen($this->markup) < 1) {
|
|
return '';
|
|
}
|
|
|
|
$this->setOptions(); //reset options;
|
|
|
|
// Handle parsing options
|
|
if (! empty($options)) {
|
|
$this->setOptions($options);
|
|
}
|
|
|
|
$data = $this->markup;
|
|
|
|
$this->parse_wiki_argvariable($data);
|
|
|
|
$data = preg_replace('/(\{img [^\}]+li)<x>(nk[^\}]+\})/i', '\\1\\2', $data);
|
|
|
|
/* <x> XSS Sanitization handling */
|
|
|
|
// Fix false positive in wiki syntax
|
|
// It can't be done in the sanitizer, that can't know if the input will be wiki parsed or not
|
|
$data = preg_replace('/(\{img [^\}]+li)<x>(nk[^\}]+\})/i', '\\1\\2', $data);
|
|
|
|
// Handle pre- and no-parse sections and plugins
|
|
$preparsed = ['data' => [],'key' => []];
|
|
$noparsed = ['data' => [],'key' => []];
|
|
$this->strip_unparsed_block($data, $noparsed, true);
|
|
if (! $this->option['noparseplugins'] || $this->option['stripplugins']) {
|
|
$this->parse_first($data, $preparsed, $noparsed);
|
|
$this->parse_wiki_argvariable($data);
|
|
}
|
|
|
|
$data = $this->wikiParse($data, $noparsed);
|
|
|
|
$data = $this->parse_smileys($data);
|
|
$data = $this->parse_tagged_users($data);
|
|
$data = $this->parse_data_dynamic_variables($data, $this->option['language']);
|
|
|
|
// Put removed strings back.
|
|
$this->replace_preparse($data, $preparsed, $noparsed, $this->option['is_html']);
|
|
|
|
// Converts <x> (<x> tag using HTML entities) into the tag <x>. This tag comes from the input sanitizer (XSS filter).
|
|
// This is not HTML valid and avoids using <x> in a wiki text,
|
|
// but hide '<x>' text inside some words like 'style' that are considered as dangerous by the sanitizer.
|
|
$data = str_replace([ '<x>', '~np~', '~/np~' ], [ '<x>', '~np~', '~/np~' ], $data);
|
|
|
|
if ($this->option['typography'] && ! $this->option['ck_editor']) {
|
|
$data = typography($data, $this->option['language']);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
abstract public function wikiParse($data, $noparsed);
|
|
|
|
public function pluginExecute($name, $data = '', $args = [], $offset = 0, $validationPerformed = false, $option = [])
|
|
{
|
|
global $killtoc;
|
|
|
|
if (! empty($option)) {
|
|
$this->setOptions($option);
|
|
}
|
|
|
|
$data = $this->unprotectSpecialChars($data, true); // We want to give plugins original
|
|
$args = preg_replace(['/^"/', '/"$/'], '', $args); // Similarly remove the encoded " chars from the args
|
|
|
|
$outputFormat = 'wiki';
|
|
if (isset($this->option['context_format'])) {
|
|
$outputFormat = $this->option['context_format'];
|
|
}
|
|
|
|
if (! $this->plugin_exists($name, true)) {
|
|
return false;
|
|
}
|
|
|
|
if (! $validationPerformed && ! $this->plugin_enabled($name, $output)) {
|
|
return $this->convert_plugin_output($output, '', $outputFormat);
|
|
}
|
|
|
|
if ($this->option['inside_pretty'] === true) {
|
|
$trklib = TikiLib::lib('trk');
|
|
$trklib->replace_pretty_tracker_refs($args);
|
|
|
|
// Reset the tr_offset1 value, which comes from a list selection and specifies the offset to use within the resultset.
|
|
// Pretty trackers can contain other tracker plugins. These plugins should get the results from index = 0, and not the index in the calling list
|
|
if (isset($_REQUEST['tr_offset1'])) {
|
|
$_REQUEST['list_tr_offset1'] = $_REQUEST['tr_offset1'];
|
|
$_REQUEST['tr_offset1'] = 0;
|
|
}
|
|
foreach ($args as $arg) {
|
|
if (substr($arg, 0, 4) == '{$f_') {
|
|
return $name . ': ' . tr(
|
|
'Pretty tracker reference "%0" could not be replaced in plugin "%1".',
|
|
str_replace(['{','}'], '', $arg),
|
|
$name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
$func_name = 'wikiplugin_' . $name;
|
|
|
|
if (! $validationPerformed && ! $this->option['ck_editor']) {
|
|
$this->plugin_apply_filters($name, $data, $args);
|
|
}
|
|
|
|
if (function_exists($func_name)) {
|
|
$pluginFormat = 'wiki';
|
|
|
|
$info = $this->plugin_info($name, $args);
|
|
if (isset($info['format'])) {
|
|
$pluginFormat = $info['format'];
|
|
}
|
|
|
|
$killtoc = false;
|
|
|
|
if ($pluginFormat === 'wiki' && $this->option['preview_mode'] === true && $_SESSION['wysiwyg'] === 'y') { // fix lost new lines in wysiwyg plugins data
|
|
$data = nl2br($data);
|
|
}
|
|
|
|
$saved_options = $this->option; // save current options (but do not reset)
|
|
|
|
$output = $func_name($data, $args, $offset, $this);
|
|
|
|
$this->option = $saved_options; // restore parsing options after plugin has executed
|
|
|
|
//This was added to remove the table of contents sometimes returned by other plugins, to use, simply have global $killtoc, and $killtoc = true;
|
|
if ($killtoc == true) {
|
|
while (($maketoc_start = strpos($output, "{maketoc")) !== false) {
|
|
$maketoc_end = strpos($output, "}");
|
|
$output = substr_replace($output, "", $maketoc_start, $maketoc_end - $maketoc_start + 1);
|
|
}
|
|
}
|
|
|
|
$killtoc = false;
|
|
|
|
$plugin_result = $this->convert_plugin_output($output, $pluginFormat, $outputFormat);
|
|
if ($this->option['ck_editor'] == true) {
|
|
return $this->convert_plugin_for_ckeditor($name, $args, $plugin_result, $data, $info);
|
|
} else {
|
|
return $plugin_result;
|
|
}
|
|
} elseif (WikiPlugin_Negotiator_Wiki_Alias::findImplementation($name, $data, $args)) {
|
|
return $this->pluginExecute($name, $data, $args, $offset, $validationPerformed);
|
|
}
|
|
}
|
|
}
|