diff options
Diffstat (limited to 'phpBB/phpbb/textformatter/s9e')
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/factory.php | 570 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/parser.php | 396 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/quote_helper.php | 81 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/renderer.php | 314 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/utils.php | 139 | 
5 files changed, 1500 insertions, 0 deletions
| diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php new file mode 100644 index 0000000000..63b23d2fd0 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -0,0 +1,570 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +use s9e\TextFormatter\Configurator; +use s9e\TextFormatter\Configurator\Items\AttributeFilters\RegexpFilter; +use s9e\TextFormatter\Configurator\Items\UnsafeTemplate; + +/** +* Creates s9e\TextFormatter objects +*/ +class factory implements \phpbb\textformatter\cache_interface +{ +	/** +	* @var \phpbb\cache\driver\driver_interface +	*/ +	protected $cache; + +	/** +	* @var string Path to the cache dir +	*/ +	protected $cache_dir; + +	/** +	* @var string Cache key used for the parser +	*/ +	protected $cache_key_parser; + +	/** +	* @var string Cache key used for the renderer +	*/ +	protected $cache_key_renderer; + +	/** +	* @var \phpbb\config\config +	*/ +	protected $config; + +	/** +	* @var array Custom tokens used in bbcode.html and their corresponding token from the definition +	*/ +	protected $custom_tokens = array( +		'email' => array('{DESCRIPTION}' => '{TEXT}'), +		'flash' => array('{WIDTH}' => '{NUMBER1}', '{HEIGHT}' => '{NUMBER2}'), +		'img'   => array('{URL}' => '{IMAGEURL}'), +		'list'  => array('{LIST_TYPE}' => '{HASHMAP}'), +		'quote' => array('{USERNAME}' => '{TEXT1}'), +		'size'  => array('{SIZE}' => '{FONTSIZE}'), +		'url'   => array('{DESCRIPTION}' => '{TEXT}'), +	); + +	/** +	* @var \phpbb\textformatter\data_access +	*/ +	protected $data_access; + +	/** +	* @var array Default BBCode definitions +	*/ +	protected $default_definitions = array( +		'attachment' => '[ATTACHMENT index={NUMBER} filename={TEXT;useContent}]', +		'b'     => '[B]{TEXT}[/B]', +		'code'  => '[CODE lang={IDENTIFIER;optional}]{TEXT}[/CODE]', +		'color' => '[COLOR={COLOR}]{TEXT}[/COLOR]', +		'email' => '[EMAIL={EMAIL;useContent} subject={TEXT;optional;postFilter=rawurlencode} body={TEXT;optional;postFilter=rawurlencode}]{TEXT}[/EMAIL]', +		'flash' => '[FLASH={NUMBER1},{NUMBER2} width={NUMBER1;postFilter=#flashwidth} height={NUMBER2;postFilter=#flashheight} url={URL;useContent} /]', +		'i'     => '[I]{TEXT}[/I]', +		'img'   => '[IMG src={IMAGEURL;useContent}]', +		'list'  => '[LIST type={HASHMAP=1:decimal,a:lower-alpha,A:upper-alpha,i:lower-roman,I:upper-roman;optional;postFilter=#simpletext}]{TEXT}[/LIST]', +		'li'    => '[* $tagName=LI]{TEXT}[/*]', +		'quote' => +			"[QUOTE +				author={TEXT1;optional} +				post_id={UINT;optional} +				post_url={URL;optional;postFilter=#false} +				profile_url={URL;optional;postFilter=#false} +				time={UINT;optional} +				url={URL;optional} +				user_id={UINT;optional} +				author={PARSE=/^\\[url=(?'url'.*?)](?'author'.*)\\[\\/url]$/i} +				author={PARSE=/^\\[url](?'author'(?'url'.*?))\\[\\/url]$/i} +				author={PARSE=/(?'url'https?:\\/\\/[^[\\]]+)/i} +			]{TEXT2}[/QUOTE]", +		'size'  => '[SIZE={FONTSIZE}]{TEXT}[/SIZE]', +		'u'     => '[U]{TEXT}[/U]', +		'url'   => '[URL={URL;useContent}]{TEXT}[/URL]', +	); + +	/** +	* @var array Default templates, taken from bbcode::bbcode_tpl() +	*/ +	protected $default_templates = array( +		'b'     => '<span style="font-weight: bold"><xsl:apply-templates/></span>', +		'i'     => '<span style="font-style: italic"><xsl:apply-templates/></span>', +		'u'     => '<span style="text-decoration: underline"><xsl:apply-templates/></span>', +		'img'   => '<img src="{IMAGEURL}" class="postimage" alt="{L_IMAGE}"/>', +		'size'  => '<span style="font-size: {FONTSIZE}%; line-height: normal"><xsl:apply-templates/></span>', +		'color' => '<span style="color: {COLOR}"><xsl:apply-templates/></span>', +		'email' => '<a> +			<xsl:attribute name="href"> +				<xsl:text>mailto:</xsl:text> +				<xsl:value-of select="@email"/> +				<xsl:if test="@subject or @body"> +					<xsl:text>?</xsl:text> +					<xsl:if test="@subject">subject=<xsl:value-of select="@subject"/></xsl:if> +					<xsl:if test="@body"><xsl:if test="@subject">&</xsl:if>body=<xsl:value-of select="@body"/></xsl:if> +				</xsl:if> +			</xsl:attribute> +			<xsl:apply-templates/> +		</a>', +	); + +	/** +	* @var \phpbb\event\dispatcher_interface +	*/ +	protected $dispatcher; + +	/** +	* Constructor +	* +	* @param \phpbb\textformatter\data_access $data_access +	* @param \phpbb\cache\driver\driver_interface $cache +	* @param \phpbb\event\dispatcher_interface $dispatcher +	* @param \phpbb\config\config $config +	* @param string $cache_dir          Path to the cache dir +	* @param string $cache_key_parser   Cache key used for the parser +	* @param string $cache_key_renderer Cache key used for the renderer +	*/ +	public function __construct(\phpbb\textformatter\data_access $data_access, \phpbb\cache\driver\driver_interface $cache, \phpbb\event\dispatcher_interface $dispatcher, \phpbb\config\config $config, $cache_dir, $cache_key_parser, $cache_key_renderer) +	{ +		$this->cache = $cache; +		$this->cache_dir = $cache_dir; +		$this->cache_key_parser = $cache_key_parser; +		$this->cache_key_renderer = $cache_key_renderer; +		$this->config = $config; +		$this->data_access = $data_access; +		$this->dispatcher = $dispatcher; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function invalidate() +	{ +		$this->regenerate(); +	} + +	/** +	* {@inheritdoc} +	* +	* Will remove old renderers from the cache dir but won't touch the current renderer +	*/ +	public function tidy() +	{ +		// Get the name of current renderer +		$renderer_data = $this->cache->get($this->cache_key_renderer); +		$renderer_file = ($renderer_data) ? $renderer_data['class'] . '.php' : null; + +		foreach (glob($this->cache_dir . 's9e_*') as $filename) +		{ +			// Only remove the file if it's not the current renderer +			if (!$renderer_file || substr($filename, -strlen($renderer_file)) !== $renderer_file) +			{ +				unlink($filename); +			} +		} +	} + +	/** +	* Generate and return a new configured instance of s9e\TextFormatter\Configurator +	* +	* @return Configurator +	*/ +	public function get_configurator() +	{ +		// Create a new Configurator +		$configurator = new Configurator; + +		/** +		* Modify the s9e\TextFormatter configurator before the default settings are set +		* +		* @event core.text_formatter_s9e_configure_before +		* @var \s9e\TextFormatter\Configurator configurator Configurator instance +		* @since 3.2.0-a1 +		*/ +		$vars = array('configurator'); +		extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_before', compact($vars))); + +		// Reset the list of allowed schemes +		foreach ($configurator->urlConfig->getAllowedSchemes() as $scheme) +		{ +			$configurator->urlConfig->disallowScheme($scheme); +		} +		foreach (explode(',', $this->config['allowed_schemes_links']) as $scheme) +		{ +			$configurator->urlConfig->allowScheme(trim($scheme)); +		} + +		// Convert newlines to br elements by default +		$configurator->rootRules->enableAutoLineBreaks(); + +		// Don't automatically ignore text in places where text is not allowed +		$configurator->rulesGenerator->remove('IgnoreTextIfDisallowed'); + +		// Don't remove comments and instead convert them to xsl:comment elements +		$configurator->templateNormalizer->remove('RemoveComments'); +		$configurator->templateNormalizer->add('TransposeComments'); + +		// Set the rendering engine and configure it to save to the cache dir +		$configurator->rendering->engine = 'PHP'; +		$configurator->rendering->engine->cacheDir = $this->cache_dir; +		$configurator->rendering->engine->defaultClassPrefix = 's9e_renderer_'; +		$configurator->rendering->engine->enableQuickRenderer = true; + +		// Create custom filters for BBCode tokens that are supported in phpBB but not in +		// s9e\TextFormatter +		$filter = new RegexpFilter('#^' . get_preg_expression('relative_url') . '$#Du'); +		$configurator->attributeFilters->add('#local_url', $filter); +		$configurator->attributeFilters->add('#relative_url', $filter); + +		// INTTEXT regexp from acp_bbcodes +		$filter = new RegexpFilter('!^([\p{L}\p{N}\-+,_. ]+)$!Du'); +		$configurator->attributeFilters->add('#inttext', $filter); + +		// Create custom filters for Flash restrictions, which use the same values as the image +		// restrictions but have their own error message +		$configurator->attributeFilters +			->add('#flashheight', __NAMESPACE__ . '\\parser::filter_flash_height') +			->addParameterByName('max_img_height') +			->addParameterByName('logger'); + +		$configurator->attributeFilters +			->add('#flashwidth', __NAMESPACE__ . '\\parser::filter_flash_width') +			->addParameterByName('max_img_width') +			->addParameterByName('logger'); + +		// Create a custom filter for phpBB's per-mode font size limits +		$configurator->attributeFilters +			->add('#fontsize', __NAMESPACE__ . '\\parser::filter_font_size') +			->addParameterByName('max_font_size') +			->addParameterByName('logger') +			->markAsSafeInCSS(); + +		// Create a custom filter for image URLs +		$configurator->attributeFilters +			->add('#imageurl', __NAMESPACE__ . '\\parser::filter_img_url') +			->addParameterByName('urlConfig') +			->addParameterByName('logger') +			->addParameterByName('max_img_height') +			->addParameterByName('max_img_width') +			->markAsSafeAsURL(); + +		// Add default BBCodes +		foreach ($this->get_default_bbcodes($configurator) as $bbcode) +		{ +			$configurator->BBCodes->addCustom($bbcode['usage'], $bbcode['template']); +		} + +		// Modify the template to disable images/flash depending on user's settings +		foreach (array('FLASH', 'IMG') as $name) +		{ +			$tag = $configurator->tags[$name]; +			$tag->template = '<xsl:choose><xsl:when test="$S_VIEW' . $name . '">' . $tag->template . '</xsl:when><xsl:otherwise><xsl:apply-templates/></xsl:otherwise></xsl:choose>'; +		} + +		// Load custom BBCodes +		foreach ($this->data_access->get_bbcodes() as $row) +		{ +			// Insert the board's URL before {LOCAL_URL} tokens +			$tpl = preg_replace_callback( +				'#\\{LOCAL_URL\\d*\\}#', +				function ($m) +				{ +					return generate_board_url() . '/' . $m[0]; +				}, +				$row['bbcode_tpl'] +			); + +			try +			{ +				$configurator->BBCodes->addCustom($row['bbcode_match'], new UnsafeTemplate($tpl)); +			} +			catch (\Exception $e) +			{ +				/** +				* @todo log an error? +				*/ +			} +		} + +		// Load smilies +		foreach ($this->data_access->get_smilies() as $row) +		{ +			$configurator->Emoticons->add( +				$row['code'], +				'<img class="smilies" src="{$T_SMILIES_PATH}/' . htmlspecialchars($row['smiley_url']) . '" alt="{.}" title="' . htmlspecialchars($row['emotion']) . '"/>' +			); +		} + +		if (isset($configurator->Emoticons)) +		{ +			// Force emoticons to be rendered as text if $S_VIEWSMILIES is not set +			$configurator->Emoticons->notIfCondition = 'not($S_VIEWSMILIES)'; + +			// Only parse emoticons at the beginning of the text or if they're preceded by any +			// one of: a new line, a space, a dot, or a right square bracket +			$configurator->Emoticons->notAfter = '[^\\n .\\]]'; +		} + +		// Load the censored words +		$censor = $this->data_access->get_censored_words(); +		if (!empty($censor)) +		{ +			// Use a namespaced tag to avoid collisions +			$configurator->plugins->load('Censor', array('tagName' => 'censor:tag')); +			foreach ($censor as $row) +			{ +				// NOTE: words are stored as HTML, we need to decode them to plain text +				$configurator->Censor->add(htmlspecialchars_decode($row['word']),  htmlspecialchars_decode($row['replacement'])); +			} +		} + +		// Load the magic links plugins. We do that after BBCodes so that they use the same tags +		$configurator->plugins->load('Autoemail'); +		$configurator->plugins->load('Autolink', array('matchWww' => true)); + +		// Register some vars with a default value. Those should be set at runtime by whatever calls +		// the parser +		$configurator->registeredVars['max_font_size'] = 0; +		$configurator->registeredVars['max_img_height'] = 0; +		$configurator->registeredVars['max_img_width'] = 0; + +		// Load the Emoji plugin and modify its tag's template to obey viewsmilies +		$configurator->Emoji->setImageSize(18); +		$tag = $configurator->Emoji->getTag(); +		$tag->template = '<xsl:choose><xsl:when test="$S_VIEWSMILIES">' . str_replace('class="emoji"', 'class="smilies"', $tag->template) . '</xsl:when><xsl:otherwise><xsl:value-of select="."/></xsl:otherwise></xsl:choose>'; + +		/** +		* Modify the s9e\TextFormatter configurator after the default settings are set +		* +		* @event core.text_formatter_s9e_configure_after +		* @var \s9e\TextFormatter\Configurator configurator Configurator instance +		* @since 3.2.0-a1 +		*/ +		$vars = array('configurator'); +		extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_after', compact($vars))); + +		return $configurator; +	} + +	/** +	* Regenerate and cache a new parser and renderer +	* +	* @return array Associative array with at least two elements: "parser" and "renderer" +	*/ +	public function regenerate() +	{ +		$configurator = $this->get_configurator(); + +		// Get the censor helper and remove the Censor plugin if applicable +		if (isset($configurator->Censor)) +		{ +			$censor = $configurator->Censor->getHelper(); +			unset($configurator->Censor); +			unset($configurator->tags['censor:tag']); +		} + +		$objects  = $configurator->finalize(); +		$parser   = $objects['parser']; +		$renderer = $objects['renderer']; + +		// Cache the parser as-is +		$this->cache->put($this->cache_key_parser, $parser); + +		// We need to cache the name of the renderer's generated class +		$renderer_data = array('class' => get_class($renderer)); +		if (isset($censor)) +		{ +			$renderer_data['censor'] = $censor; +		} +		$this->cache->put($this->cache_key_renderer, $renderer_data); + +		return array('parser' => $parser, 'renderer' => $renderer); +	} + +	/** +	* Return the default BBCodes configuration +	* +	* @return array 2D array. Each element has a 'usage' key, a 'template' key, and an optional 'options' key +	*/ +	protected function get_default_bbcodes($configurator) +	{ +		// For each BBCode, build an associative array matching style_ids to their template +		$templates = array(); +		foreach ($this->data_access->get_styles_templates() as $style_id => $data) +		{ +			foreach ($this->extract_templates($data['template']) as $bbcode_name => $template) +			{ +				$templates[$bbcode_name][$style_id] = $template; +			} + +			// Add default templates wherever missing, or for BBCodes that were not specified in +			// this template's bitfield. For instance, prosilver has a custom template for b but its +			// bitfield does not enable it so the default template is used instead +			foreach ($this->default_templates as $bbcode_name => $template) +			{ +				if (!isset($templates[$bbcode_name][$style_id]) || !in_array($bbcode_name, $data['bbcodes'], true)) +				{ +					$templates[$bbcode_name][$style_id] = $template; +				} +			} +		} + +		// Replace custom tokens and normalize templates +		foreach ($templates as $bbcode_name => $style_templates) +		{ +			foreach ($style_templates as $i => $template) +			{ +				if (isset($this->custom_tokens[$bbcode_name])) +				{ +					$template = strtr($template, $this->custom_tokens[$bbcode_name]); +				} + +				$templates[$bbcode_name][$i] = $configurator->templateNormalizer->normalizeTemplate($template); +			} +		} + +		$bbcodes = array(); +		foreach ($this->default_definitions as $bbcode_name => $usage) +		{ +			$bbcodes[$bbcode_name] = array( +				'usage'    => $usage, +				'template' => $this->merge_templates($templates[$bbcode_name]), +			); +		} + +		return $bbcodes; +	} + +	/** +	* Extract and recompose individual BBCode templates from a style's template file +	* +	* @param  string $template Style template (bbcode.html) +	* @return array Associative array matching BBCode names to their template +	*/ +	protected function extract_templates($template) +	{ +		// Capture the template fragments +		preg_match_all('#<!-- BEGIN (.*?) -->(.*?)<!-- END .*? -->#s', $template, $matches, PREG_SET_ORDER); + +		$fragments = array(); +		foreach ($matches as $match) +		{ +			// Normalize the whitespace +			$fragment = preg_replace('#>\\n\\t*<#', '><', trim($match[2])); + +			$fragments[$match[1]] = $fragment; +		} + +		// Automatically recompose templates split between *_open and *_close +		foreach ($fragments as $fragment_name => $fragment) +		{ +			if (preg_match('#^(\\w+)_close$#', $fragment_name, $match)) +			{ +				$bbcode_name = $match[1]; + +				if (isset($fragments[$bbcode_name . '_open'])) +				{ +					$templates[$bbcode_name] = $fragments[$bbcode_name . '_open'] . '<xsl:apply-templates/>' . $fragment; +				} +			} +		} + +		// Manually recompose and overwrite irregular templates +		$templates['list'] = +			'<xsl:choose> +				<xsl:when test="not(@type)"> +					' . $fragments['ulist_open_default'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . ' +				</xsl:when> +				<xsl:when test="contains(\'upperlowerdecim\',substring(@type,1,5))"> +					' . $fragments['olist_open'] . '<xsl:apply-templates/>' . $fragments['olist_close'] . ' +				</xsl:when> +				<xsl:otherwise> +					' . $fragments['ulist_open'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . ' +				</xsl:otherwise> +			</xsl:choose>'; + +		$templates['li'] = $fragments['listitem'] . '<xsl:apply-templates/>' . $fragments['listitem_close']; + +		// Replace the regular quote template with the extended quote template if available +		if (isset($fragments['quote_extended'])) +		{ +			$templates['quote'] = $fragments['quote_extended']; +		} + +		// The [attachment] BBCode uses the inline_attachment template to output a comment that +		// is post-processed by parse_attachments() +		$templates['attachment'] = $fragments['inline_attachment_open'] . '<xsl:comment> ia<xsl:value-of select="@index"/> </xsl:comment><xsl:value-of select="@filename"/><xsl:comment> ia<xsl:value-of select="@index"/> </xsl:comment>' . $fragments['inline_attachment_close']; + +		// Add fragments as templates +		foreach ($fragments as $fragment_name => $fragment) +		{ +			if (preg_match('#^\\w+$#', $fragment_name)) +			{ +				$templates[$fragment_name] = $fragment; +			} +		} + +		// Keep only templates that are named after an existing BBCode +		$templates = array_intersect_key($templates, $this->default_definitions); + +		return $templates; +	} + +	/** +	* Merge the templates from any number of styles into one BBCode template +	* +	* When multiple templates are available for the same BBCode (because of multiple styles) we +	* merge them into a single template that uses an xsl:choose construct that determines which +	* style to use at rendering time. +	* +	* @param  array  $style_templates Associative array matching style_ids to their template +	* @return string +	*/ +	protected function merge_templates(array $style_templates) +	{ +		// Return the template as-is if there's only one style or all styles share the same template +		if (count(array_unique($style_templates)) === 1) +		{ +			return end($style_templates); +		} + +		// Group identical templates together +		$grouped_templates = array(); +		foreach ($style_templates as $style_id => $style_template) +		{ +			$grouped_templates[$style_template][] = '$STYLE_ID=' . $style_id; +		} + +		// Sort templates by frequency descending +		$templates_cnt = array_map('sizeof', $grouped_templates); +		array_multisort($grouped_templates, $templates_cnt); + +		// Remove the most frequent template from the list; It becomes the default +		reset($grouped_templates); +		$default_template = key($grouped_templates); +		unset($grouped_templates[$default_template]); + +		// Build an xsl:choose switch +		$template = '<xsl:choose>'; +		foreach ($grouped_templates as $style_template => $exprs) +		{ +			$template .= '<xsl:when test="' . implode(' or ', $exprs) . '">' . $style_template . '</xsl:when>'; +		} +		$template .= '<xsl:otherwise>' . $default_template . '</xsl:otherwise></xsl:choose>'; + +		return $template; +	} +} diff --git a/phpBB/phpbb/textformatter/s9e/parser.php b/phpBB/phpbb/textformatter/s9e/parser.php new file mode 100644 index 0000000000..838c211e56 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/parser.php @@ -0,0 +1,396 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +use s9e\TextFormatter\Parser\BuiltInFilters; +use s9e\TextFormatter\Parser\Logger; + +/** +* s9e\TextFormatter\Parser adapter +*/ +class parser implements \phpbb\textformatter\parser_interface +{ +	/** +	* @var \phpbb\event\dispatcher_interface +	*/ +	protected $dispatcher; + +	/** +	* @var \s9e\TextFormatter\Parser +	*/ +	protected $parser; + +	/** +	* Constructor +	* +	* @param \phpbb\cache\driver_interface $cache +	* @param string $key Cache key +	* @param factory $factory +	* @param \phpbb\event\dispatcher_interface $dispatcher +	*/ +	public function __construct(\phpbb\cache\driver\driver_interface $cache, $key, factory $factory, \phpbb\event\dispatcher_interface $dispatcher) +	{ +		$parser = $cache->get($key); +		if (!$parser) +		{ +			$objects = $factory->regenerate(); +			$parser  = $objects['parser']; +		} + +		$this->dispatcher = $dispatcher; +		$this->parser = $parser; +		$parser = $this; + +		/** +		* Configure the parser service +		* +		* Can be used to: +		*  - toggle features or BBCodes +		*  - register variables or custom parsers in the s9e\TextFormatter parser +		*  - configure the s9e\TextFormatter parser's runtime settings +		* +		* @event core.text_formatter_s9e_parser_setup +		* @var \phpbb\textformatter\s9e\parser parser This parser service +		* @since 3.2.0-a1 +		*/ +		$vars = array('parser'); +		extract($dispatcher->trigger_event('core.text_formatter_s9e_parser_setup', compact($vars))); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function parse($text) +	{ +		$parser = $this; + +		/** +		* Modify a text before it is parsed +		* +		* @event core.text_formatter_s9e_parse_before +		* @var \phpbb\textformatter\s9e\parser parser This parser service +		* @var string text The original text +		* @since 3.2.0-a1 +		*/ +		$vars = array('parser', 'text'); +		extract($this->dispatcher->trigger_event('core.text_formatter_s9e_parse_before', compact($vars))); + +		$xml = $this->parser->parse($text); + +		/** +		* Modify a parsed text in its XML form +		* +		* @event core.text_formatter_s9e_parse_after +		* @var \phpbb\textformatter\s9e\parser parser This parser service +		* @var string xml The parsed text, in XML +		* @since 3.2.0-a1 +		*/ +		$vars = array('parser', 'xml'); +		extract($this->dispatcher->trigger_event('core.text_formatter_s9e_parse_after', compact($vars))); + +		return $xml; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function disable_bbcode($name) +	{ +		$this->parser->disableTag(strtoupper($name)); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function disable_bbcodes() +	{ +		$this->parser->disablePlugin('BBCodes'); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function disable_censor() +	{ +		$this->parser->disablePlugin('Censor'); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function disable_magic_url() +	{ +		$this->parser->disablePlugin('Autoemail'); +		$this->parser->disablePlugin('Autolink'); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function disable_smilies() +	{ +		$this->parser->disablePlugin('Emoticons'); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function enable_bbcode($name) +	{ +		$this->parser->enableTag(strtoupper($name)); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function enable_bbcodes() +	{ +		$this->parser->enablePlugin('BBCodes'); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function enable_censor() +	{ +		$this->parser->enablePlugin('Censor'); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function enable_magic_url() +	{ +		$this->parser->enablePlugin('Autoemail'); +		$this->parser->enablePlugin('Autolink'); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function enable_smilies() +	{ +		$this->parser->enablePlugin('Emoticons'); +	} + +	/** +	* {@inheritdoc} +	* +	* This will convert the log entries found in s9e\TextFormatter's logger into phpBB error +	* messages +	*/ +	public function get_errors() +	{ +		$errors = array(); +		foreach ($this->parser->getLogger()->get() as $entry) +		{ +			list($type, $msg, $context) = $entry; + +			if ($msg === 'Tag limit exceeded') +			{ +				if ($context['tagName'] === 'E') +				{ +					$errors[] = array('TOO_MANY_SMILIES', $context['tagLimit']); +				} +				else if ($context['tagName'] === 'URL') +				{ +					$errors[] = array('TOO_MANY_URLS', $context['tagLimit']); +				} +			} +			else if ($msg === 'MAX_FONT_SIZE_EXCEEDED') +			{ +				$errors[] = array($msg, $context['max_size']); +			} +			else if (preg_match('/^MAX_(?:FLASH|IMG)_(HEIGHT|WIDTH)_EXCEEDED$/D', $msg, $m)) +			{ +				$errors[] = array($msg, $context['max_' . strtolower($m[1])]); +			} +			else if ($msg === 'Tag is disabled') +			{ +				$name = strtolower($context['tag']->getName()); +				$errors[] = array('UNAUTHORISED_BBCODE', '[' . $name . ']'); +			} +			else if ($msg === 'UNABLE_GET_IMAGE_SIZE') +			{ +				$errors[] = array($msg); +			} +		} + +		// Deduplicate error messages. array_unique() only works on strings so we have to serialize +		if (!empty($errors)) +		{ +			$errors = array_map('unserialize', array_unique(array_map('serialize', $errors))); +		} + +		return $errors; +	} + +	/** +	* Return the instance of s9e\TextFormatter\Parser used by this object +	* +	* @return \s9e\TextFormatter\Parser +	*/ +	public function get_parser() +	{ +		return $this->parser; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function set_var($name, $value) +	{ +		if ($name === 'max_smilies') +		{ +			$this->parser->setTagLimit('E', $value ?: PHP_INT_MAX); +		} +		else if ($name === 'max_urls') +		{ +			$this->parser->setTagLimit('URL', $value ?: PHP_INT_MAX); +		} +		else +		{ +			$this->parser->registeredVars[$name] = $value; +		} +	} + +	/** +	* {@inheritdoc} +	*/ +	public function set_vars(array $vars) +	{ +		foreach ($vars as $name => $value) +		{ +			$this->set_var($name, $value); +		} +	} + +	/** +	* Filter a flash object's height +	* +	* @see bbcode_firstpass::bbcode_flash() +	* +	* @param  string  $height +	* @param  integer $max_height +	* @param  Logger  $logger +	* @return mixed              Original value if valid, FALSE otherwise +	*/ +	static public function filter_flash_height($height, $max_height, Logger $logger) +	{ +		if ($max_height && $height > $max_height) +		{ +			$logger->err('MAX_FLASH_HEIGHT_EXCEEDED', array('max_height' => $max_height)); + +			return false; +		} + +		return $height; +	} + +	/** +	* Filter a flash object's width +	* +	* @see bbcode_firstpass::bbcode_flash() +	* +	* @param  string  $width +	* @param  integer $max_width +	* @param  Logger  $logger +	* @return mixed              Original value if valid, FALSE otherwise +	*/ +	static public function filter_flash_width($width, $max_width, Logger $logger) +	{ +		if ($max_width && $width > $max_width) +		{ +			$logger->err('MAX_FLASH_WIDTH_EXCEEDED', array('max_width' => $max_width)); + +			return false; +		} + +		return $width; +	} + +	/** +	* Filter the value used in a [size] BBCode +	* +	* @see bbcode_firstpass::bbcode_size() +	* +	* @param  string  $size     Original size +	* @param  integer $max_size Maximum allowed size +	* @param  Logger  $logger +	* @return mixed             Original value if valid, FALSE otherwise +	*/ +	static public function filter_font_size($size, $max_size, Logger $logger) +	{ +		if ($max_size && $size > $max_size) +		{ +			$logger->err('MAX_FONT_SIZE_EXCEEDED', array('max_size' => $max_size)); + +			return false; +		} + +		if ($size < 1) +		{ +			return false; +		} + +		return $size; +	} + +	/** +	* Filter an image's URL to enforce restrictions on its dimensions +	* +	* @see bbcode_firstpass::bbcode_img() +	* +	* @param  string  $url        Original URL +	* @param  array   $url_config Config used by the URL filter +	* @param  Logger  $logger +	* @param  integer $max_height Maximum height allowed +	* @param  integer $max_width  Maximum width allowed +	* @return string|bool         Original value if valid, FALSE otherwise +	*/ +	static public function filter_img_url($url, array $url_config, Logger $logger, $max_height, $max_width) +	{ +		// Validate the URL +		$url = BuiltInFilters::filterUrl($url, $url_config, $logger); +		if ($url === false) +		{ +			return false; +		} + +		if ($max_height || $max_width) +		{ +			$imagesize = new \fastImageSize\fastImageSize(); +			$size_info = $imagesize->getImageSize($url); +			if ($size_info === false) +			{ +				$logger->err('UNABLE_GET_IMAGE_SIZE'); +				return false; +			} + +			if ($max_height && $max_height < $size_info['height']) +			{ +				$logger->err('MAX_IMG_HEIGHT_EXCEEDED', array('max_height' => $max_height)); +				return false; +			} + +			if ($max_width && $max_width < $size_info['width']) +			{ +				$logger->err('MAX_IMG_WIDTH_EXCEEDED', array('max_width' => $max_width)); +				return false; +			} +		} + +		return $url; +	} +} diff --git a/phpBB/phpbb/textformatter/s9e/quote_helper.php b/phpBB/phpbb/textformatter/s9e/quote_helper.php new file mode 100644 index 0000000000..24109ac8cc --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/quote_helper.php @@ -0,0 +1,81 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +class quote_helper +{ +	/** +	* @var string Base URL for a post link, uses {POST_ID} as placeholder +	*/ +	protected $post_url; + +	/** +	* @var string Base URL for a profile link, uses {USER_ID} as placeholder +	*/ +	protected $profile_url; + +	/** +	* @var \phpbb\user +	*/ +	protected $user; + +	/** +	* Constructor +	* +	* @param \phpbb\user $user +	* @param string $root_path +	* @param string $php_ext +	*/ +	public function __construct(\phpbb\user $user, $root_path, $php_ext) +	{ +		$this->post_url = append_sid($root_path . 'viewtopic.' . $php_ext, 'p={POST_ID}#p{POST_ID}'); +		$this->profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={USER_ID}'); +		$this->user = $user; +	} + +	/** +	* Inject dynamic metadata into QUOTE tags in given XML +	* +	* @param  string $xml Original XML +	* @return string      Modified XML +	*/ +	public function inject_metadata($xml) +	{ +		$post_url = $this->post_url; +		$profile_url = $this->profile_url; +		$user = $this->user; + +		return \s9e\TextFormatter\Utils::replaceAttributes( +			$xml, +			'QUOTE', +			function ($attributes) use ($post_url, $profile_url, $user) +			{ +				if (isset($attributes['post_id'])) +				{ +					$attributes['post_url'] = str_replace('{POST_ID}', $attributes['post_id'], $post_url); +				} +				if (isset($attributes['time'])) +				{ +					$attributes['date'] = $user->format_date($attributes['time']); +				} +				if (isset($attributes['user_id'])) +				{ +					$attributes['profile_url'] = str_replace('{USER_ID}', $attributes['user_id'], $profile_url); +				} + +				return $attributes; +			} +		); +	} +} diff --git a/phpBB/phpbb/textformatter/s9e/renderer.php b/phpBB/phpbb/textformatter/s9e/renderer.php new file mode 100644 index 0000000000..2206605ba2 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/renderer.php @@ -0,0 +1,314 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +/** +* s9e\TextFormatter\Renderer adapter +*/ +class renderer implements \phpbb\textformatter\renderer_interface +{ +	/** +	* @var \s9e\TextFormatter\Plugins\Censor\Helper +	*/ +	protected $censor; + +	/** +	* @var \phpbb\event\dispatcher_interface +	*/ +	protected $dispatcher; + +	/** +	* @var quote_helper +	*/ +	protected $quote_helper; + +	/** +	* @var \s9e\TextFormatter\Renderer +	*/ +	protected $renderer; + +	/** +	* @var bool Status of the viewcensors option +	*/ +	protected $viewcensors = false; + +	/** +	* @var bool Status of the viewflash option +	*/ +	protected $viewflash = false; + +	/** +	* @var bool Status of the viewimg option +	*/ +	protected $viewimg = false; + +	/** +	* @var bool Status of the viewsmilies option +	*/ +	protected $viewsmilies = false; + +	/** +	* Constructor +	* +	* @param \phpbb\cache\driver\driver_interface $cache +	* @param string $cache_dir Path to the cache dir +	* @param string $key Cache key +	* @param factory $factory +	* @param \phpbb\event\dispatcher_interface $dispatcher +	*/ +	public function __construct(\phpbb\cache\driver\driver_interface $cache, $cache_dir, $key, factory $factory, \phpbb\event\dispatcher_interface $dispatcher) +	{ +		$renderer_data = $cache->get($key); +		if ($renderer_data) +		{ +			$class = $renderer_data['class']; +			if (!class_exists($class, false)) +			{ +				// Try to load the renderer class from its cache file +				$cache_file = $cache_dir . $class . '.php'; + +				if (file_exists($cache_file)) +				{ +					include($cache_file); +				} +			} +			if (class_exists($class, false)) +			{ +				$renderer = new $class; +			} +			if (isset($renderer_data['censor'])) +			{ +				$censor = $renderer_data['censor']; +			} +		} +		if (!isset($renderer)) +		{ +			$objects  = $factory->regenerate(); +			$renderer = $objects['renderer']; +		} + +		if (isset($censor)) +		{ +			$this->censor = $censor; +		} +		$this->dispatcher = $dispatcher; +		$this->renderer = $renderer; +		$renderer = $this; + +		/** +		* Configure the renderer service +		* +		* @event core.text_formatter_s9e_renderer_setup +		* @var \phpbb\textformatter\s9e\renderer renderer This renderer service +		* @since 3.2.0-a1 +		*/ +		$vars = array('renderer'); +		extract($dispatcher->trigger_event('core.text_formatter_s9e_renderer_setup', compact($vars))); +	} + +	/** +	* Configure the quote_helper object used to display extended information in quotes +	* +	* @param  quote_helper $quote_helper +	*/ +	public function configure_quote_helper(quote_helper $quote_helper) +	{ +		$this->quote_helper = $quote_helper; +	} + +	/** +	* Automatically set the smilies path based on config +	* +	* @param  \phpbb\config\config $config +	* @param  \phpbb\path_helper   $path_helper +	* @return null +	*/ +	public function configure_smilies_path(\phpbb\config\config $config, \phpbb\path_helper $path_helper) +	{ +		/** +		* @see smiley_text() +		*/ +		$root_path = (defined('PHPBB_USE_BOARD_URL_PATH') && PHPBB_USE_BOARD_URL_PATH) ? generate_board_url() . '/' : $path_helper->get_web_root_path(); + +		$this->set_smilies_path($root_path . $config['smilies_path']); +	} + +	/** +	* Configure this renderer as per the user's settings +	* +	* Should set the locale as well as the viewcensor/viewflash/viewimg/viewsmilies options. +	* +	* @param  \phpbb\user          $user +	* @param  \phpbb\config\config $config +	* @param  \phpbb\auth\auth     $auth +	* @return null +	*/ +	public function configure_user(\phpbb\user $user, \phpbb\config\config $config, \phpbb\auth\auth $auth) +	{ +		$censor = $user->optionget('viewcensors') || !$config['allow_nocensors'] || !$auth->acl_get('u_chgcensors'); + +		$this->set_viewcensors($censor); +		$this->set_viewflash($user->optionget('viewflash')); +		$this->set_viewimg($user->optionget('viewimg')); +		$this->set_viewsmilies($user->optionget('viewsmilies')); + +		// Set the stylesheet parameters +		foreach (array_keys($this->renderer->getParameters()) as $param_name) +		{ +			if (strpos($param_name, 'L_') === 0) +			{ +				// L_FOO is set to $user->lang('FOO') +				$this->renderer->setParameter($param_name, $user->lang(substr($param_name, 2))); +			} +		} + +		// Set this user's style id and other parameters +		$this->renderer->setParameters(array( +			'S_IS_BOT'          => $user->data['is_bot'], +			'S_REGISTERED_USER' => $user->data['is_registered'], +			'S_USER_LOGGED_IN'  => ($user->data['user_id'] != ANONYMOUS), +			'STYLE_ID'          => $user->style['style_id'], +		)); +	} + +	/** +	* Return the instance of s9e\TextFormatter\Renderer used by this object +	* +	* @return \s9e\TextFormatter\Renderer +	*/ +	public function get_renderer() +	{ +		return $this->renderer; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function get_viewcensors() +	{ +		return $this->viewcensors; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function get_viewflash() +	{ +		return $this->viewflash; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function get_viewimg() +	{ +		return $this->viewimg; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function get_viewsmilies() +	{ +		return $this->viewsmilies; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function render($xml) +	{ +		if (isset($this->quote_helper)) +		{ +			$xml = $this->quote_helper->inject_metadata($xml); +		} +		$renderer = $this; + +		/** +		* Modify a parsed text before it is rendered +		* +		* @event core.text_formatter_s9e_render_before +		* @var \phpbb\textformatter\s9e\renderer renderer This renderer service +		* @var string xml The parsed text, in its XML form +		* @since 3.2.0-a1 +		*/ +		$vars = array('renderer', 'xml'); +		extract($this->dispatcher->trigger_event('core.text_formatter_s9e_render_before', compact($vars))); + +		if (isset($this->censor) && $this->viewcensors) +		{ +			// NOTE: censorHtml() is XML-safe +			$xml = $this->censor->censorHtml($xml, true); +		} + +		$html = $this->renderer->render($xml); + +		/** +		* Modify a rendered text +		* +		* @event core.text_formatter_s9e_render_after +		* @var string html The rendered text's HTML +		* @var \phpbb\textformatter\s9e\renderer renderer This renderer service +		* @since 3.2.0-a1 +		*/ +		$vars = array('html', 'renderer'); +		extract($this->dispatcher->trigger_event('core.text_formatter_s9e_render_after', compact($vars))); + +		return $html; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function set_smilies_path($path) +	{ +		$this->renderer->setParameter('T_SMILIES_PATH', $path); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function set_viewcensors($value) +	{ +		$this->viewcensors = $value; +		$this->renderer->setParameter('S_VIEWCENSORS', $value); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function set_viewflash($value) +	{ +		$this->viewflash = $value; +		$this->renderer->setParameter('S_VIEWFLASH', $value); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function set_viewimg($value) +	{ +		$this->viewimg = $value; +		$this->renderer->setParameter('S_VIEWIMG', $value); +	} + +	/** +	* {@inheritdoc} +	*/ +	public function set_viewsmilies($value) +	{ +		$this->viewsmilies = $value; +		$this->renderer->setParameter('S_VIEWSMILIES', $value); +	} +} diff --git a/phpBB/phpbb/textformatter/s9e/utils.php b/phpBB/phpbb/textformatter/s9e/utils.php new file mode 100644 index 0000000000..b317fe4a8d --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/utils.php @@ -0,0 +1,139 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +/** +* Text manipulation utilities +*/ +class utils implements \phpbb\textformatter\utils_interface +{ +	/** +	* Replace BBCodes and other formatting elements with whitespace +	* +	* NOTE: preserves smilies as text +	* +	* @param  string $xml Parsed text +	* @return string      Plain text +	*/ +	public function clean_formatting($xml) +	{ +		// Insert a space before <s> and <e> then remove formatting +		$xml = preg_replace('#<[es]>#', ' $0', $xml); + +		return \s9e\TextFormatter\Utils::removeFormatting($xml); +	} + +	/** +	* Format given string to be used as an attribute value +	* +	* Will return the string as-is if it can be used in a BBCode without quotes. Otherwise, +	* it will use either single- or double- quotes depending on whichever requires less escaping. +	* Quotes and backslashes are escaped with backslashes where necessary +	* +	* @param  string $str Original string +	* @return string      Same string if possible, escaped string within quotes otherwise +	*/ +	protected function format_attribute_value($str) +	{ +		if (!preg_match('/[ "\'\\\\\\]]/', $str)) +		{ +			// Return as-is if it contains none of: space, ' " \ or ] +			return $str; +		} +		$singleQuoted = "'" . addcslashes($str, "\\'") . "'"; +		$doubleQuoted = '"' . addcslashes($str, '\\"') . '"'; + +		return (strlen($singleQuoted) < strlen($doubleQuoted)) ? $singleQuoted : $doubleQuoted; +	} + +	/** +	* {@inheritdoc} +	*/ +	public function generate_quote($text, array $attributes = array()) +	{ +		$text = trim($text); +		$quote = '[quote'; +		if (isset($attributes['author'])) +		{ +			// Add the author as the BBCode's default attribute +			$quote .= '=' . $this->format_attribute_value($attributes['author']); +			unset($attributes['author']); +		} + +		if (isset($attributes['user_id']) && $attributes['user_id'] == ANONYMOUS) +		{ +			unset($attributes['user_id']); +		} + +		ksort($attributes); +		foreach ($attributes as $name => $value) +		{ +			$quote .= ' ' . $name . '=' . $this->format_attribute_value($value); +		} +		$quote .= ']'; +		$newline = (strlen($quote . $text . '[/quote]') > 80 || strpos($text, "\n") !== false) ? "\n" : ''; +		$quote .= $newline . $text . $newline . '[/quote]'; + +		return $quote; +	} + +	/** +	* Get a list of quote authors, limited to the outermost quotes +	* +	* @param  string   $xml Parsed text +	* @return string[]      List of authors +	*/ +	public function get_outermost_quote_authors($xml) +	{ +		$authors = array(); +		if (strpos($xml, '<QUOTE ') === false) +		{ +			return $authors; +		} + +		$dom = new \DOMDocument; +		$dom->loadXML($xml); +		$xpath = new \DOMXPath($dom); +		foreach ($xpath->query('//QUOTE[not(ancestor::QUOTE)]/@author') as $author) +		{ +			$authors[] = $author->textContent; +		} + +		return $authors; +	} + +	/** +	* Remove given BBCode and its content, at given nesting depth +	* +	* @param  string  $xml         Parsed text +	* @param  string  $bbcode_name BBCode's name +	* @param  integer $depth       Minimum nesting depth (number of parents of the same name) +	* @return string               Parsed text +	*/ +	public function remove_bbcode($xml, $bbcode_name, $depth = 0) +	{ +		return \s9e\TextFormatter\Utils::removeTag($xml, strtoupper($bbcode_name), $depth); +	} + +	/** +	* Return a parsed text to its original form +	* +	* @param  string $xml Parsed text +	* @return string      Original plain text +	*/ +	public function unparse($xml) +	{ +		return \s9e\TextFormatter\Unparser::unparse($xml); +	} +} | 
