diff options
Diffstat (limited to 'phpBB/phpbb/textformatter')
| -rw-r--r-- | phpBB/phpbb/textformatter/cache_interface.php | 31 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/data_access.php | 252 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/parser_interface.php | 112 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/renderer_interface.php | 92 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/factory.php | 652 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/link_helper.php | 118 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/parser.php | 399 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/quote_helper.php | 81 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/renderer.php | 313 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/s9e/utils.php | 152 | ||||
| -rw-r--r-- | phpBB/phpbb/textformatter/utils_interface.php | 79 | 
11 files changed, 2281 insertions, 0 deletions
| diff --git a/phpBB/phpbb/textformatter/cache_interface.php b/phpBB/phpbb/textformatter/cache_interface.php new file mode 100644 index 0000000000..f6b5f195c7 --- /dev/null +++ b/phpBB/phpbb/textformatter/cache_interface.php @@ -0,0 +1,31 @@ +<?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; + +/** +* Currently only used to signal that something that could effect the rendering has changed. +* BBCodes, smilies, censored words, templates, etc... +*/ +interface cache_interface +{ +	/** +	* Invalidate and/or regenerate this text formatter's cache(s) +	*/ +	public function invalidate(); + +	/** +	* Tidy/prune this text formatter's cache(s) +	*/ +	public function tidy(); +} diff --git a/phpBB/phpbb/textformatter/data_access.php b/phpBB/phpbb/textformatter/data_access.php new file mode 100644 index 0000000000..0d37e62c87 --- /dev/null +++ b/phpBB/phpbb/textformatter/data_access.php @@ -0,0 +1,252 @@ +<?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; + +/** +* Data access layer that fetchs BBCodes, smilies and censored words from the database. +* To be extended to include insert/update/delete operations. +* +* Also used to get templates. +*/ +class data_access +{ +	/** +	* @var string Name of the BBCodes table +	*/ +	protected $bbcodes_table; + +	/** +	* @var \phpbb\db\driver\driver_interface +	*/ +	protected $db; + +	/** +	* @var string Name of the smilies table +	*/ +	protected $smilies_table; + +	/** +	* @var string Name of the styles table +	*/ +	protected $styles_table; + +	/** +	* @var string Path to the styles dir +	*/ +	protected $styles_path; + +	/** +	* @var string Name of the words table +	*/ +	protected $words_table; + +	/** +	* Constructor +	* +	* @param \phpbb\db\driver\driver_interface $db Database connection +	* @param string $bbcodes_table Name of the BBCodes table +	* @param string $smilies_table Name of the smilies table +	* @param string $styles_table  Name of the styles table +	* @param string $words_table   Name of the words table +	* @param string $styles_path   Path to the styles dir +	*/ +	public function __construct(\phpbb\db\driver\driver_interface $db, $bbcodes_table, $smilies_table, $styles_table, $words_table, $styles_path) +	{ +		$this->db = $db; + +		$this->bbcodes_table = $bbcodes_table; +		$this->smilies_table = $smilies_table; +		$this->styles_table  = $styles_table; +		$this->words_table   = $words_table; + +		$this->styles_path = $styles_path; +	} + +	/** +	* Return the list of custom BBCodes +	* +	* @return array +	*/ +	public function get_bbcodes() +	{ +		$sql = 'SELECT bbcode_match, bbcode_tpl FROM ' . $this->bbcodes_table; + +		return $this->fetch_decoded_rowset($sql, ['bbcode_match']); +	} + +	/** +	* Return the list of smilies +	* +	* @return array +	*/ +	public function get_smilies() +	{ +		// NOTE: smilies that are displayed on the posting page are processed first because they're +		//       typically the most used smilies and it ends up producing a slightly more efficient +		//       renderer +		$sql = 'SELECT code, emotion, smiley_url, smiley_width, smiley_height +			FROM ' . $this->smilies_table . ' +			ORDER BY display_on_posting DESC'; + +		return $this->fetch_decoded_rowset($sql, ['code', 'emotion', 'smiley_url']); +	} + +	/** +	* Return the list of installed styles +	* +	* @return array +	*/ +	protected function get_styles() +	{ +		$sql = 'SELECT style_id, style_path, style_parent_id, bbcode_bitfield FROM ' . $this->styles_table; + +		return $this->fetch_decoded_rowset($sql); +	} + +	/** +	* Return the bbcode.html template for every installed style +	* +	* @return array 2D array. style_id as keys, each element is an array with a "template" element that contains the style's bbcode.html and a "bbcodes" element that contains the name of each BBCode that is to be stylised +	*/ +	public function get_styles_templates() +	{ +		$templates = array(); + +		$bbcode_ids = array( +			'quote' => 0, +			'b'     => 1, +			'i'     => 2, +			'url'   => 3, +			'img'   => 4, +			'size'  => 5, +			'color' => 6, +			'u'     => 7, +			'code'  => 8, +			'list'  => 9, +			'*'     => 9, +			'email' => 10, +			'flash' => 11, +			'attachment' => 12, +		); + +		$styles = array(); +		foreach ($this->get_styles() as $row) +		{ +			$styles[$row['style_id']] = $row; +		} + +		foreach ($styles as $style_id => $style) +		{ +			$bbcodes = array(); + +			// Collect the name of the BBCodes whose bit is set in the style's bbcode_bitfield +			$template_bitfield = new \bitfield($style['bbcode_bitfield']); +			foreach ($bbcode_ids as $bbcode_name => $bit) +			{ +				if ($template_bitfield->get($bit)) +				{ +					$bbcodes[] = $bbcode_name; +				} +			} + +			$filename = $this->resolve_style_filename($styles, $style); +			if ($filename === false) +			{ +				// Ignore this style, it will use the default templates +				continue; +			} + +			$templates[$style_id] = array( +				'bbcodes'  => $bbcodes, +				'template' => file_get_contents($filename), +			); +		} + +		return $templates; +	} + +	/** +	* Resolve inheritance for given style and return the path to their bbcode.html file +	* +	* @param  array       $styles Associative array of [style_id => style] containing all styles +	* @param  array       $style  Style for which we resolve +	* @return string|bool         Path to this style's bbcode.html, or FALSE +	*/ +	protected function resolve_style_filename(array $styles, array $style) +	{ +		// Look for a bbcode.html in this style's dir +		$filename = $this->styles_path . $style['style_path'] . '/template/bbcode.html'; +		if (file_exists($filename)) +		{ +			return $filename; +		} + +		// Resolve using this style's parent +		$parent_id = $style['style_parent_id']; +		if ($parent_id && !empty($styles[$parent_id])) +		{ +			return $this->resolve_style_filename($styles, $styles[$parent_id]); +		} + +		return false; +	} + +	/** +	* Return the list of censored words +	* +	* @return array +	*/ +	public function get_censored_words() +	{ +		$sql = 'SELECT word, replacement FROM ' . $this->words_table; + +		return $this->fetch_decoded_rowset($sql, ['word', 'replacement']); +	} + +	/** +	* Decode HTML special chars in given rowset +	* +	* @param  array $rows    Original rowset +	* @param  array $columns List of columns to decode +	* @return array          Decoded rowset +	*/ +	protected function decode_rowset(array $rows, array $columns) +	{ +		foreach ($rows as &$row) +		{ +			foreach ($columns as $column) +			{ +				$row[$column] = htmlspecialchars_decode($row[$column]); +			} +		} + +		return $rows; +	} + +	/** +	* Fetch all rows for given query and decode plain text columns +	* +	* @param  string $sql     SELECT query +	* @param  array  $columns List of columns to decode +	* @return array +	*/ +	protected function fetch_decoded_rowset($sql, array $columns = []) +	{ +		$result = $this->db->sql_query($sql); +		$rows = $this->db->sql_fetchrowset($result); +		$this->db->sql_freeresult($result); + +		return $this->decode_rowset($rows, $columns); +	} +} diff --git a/phpBB/phpbb/textformatter/parser_interface.php b/phpBB/phpbb/textformatter/parser_interface.php new file mode 100644 index 0000000000..ad611fb5b4 --- /dev/null +++ b/phpBB/phpbb/textformatter/parser_interface.php @@ -0,0 +1,112 @@ +<?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; + +interface parser_interface +{ +	/** +	* Parse given text +	* +	* @param  string $text +	* @return string +	*/ +	public function parse($text); + +	/** +	* Disable a specific BBCode +	* +	* @param  string $name BBCode name +	* @return null +	*/ +	public function disable_bbcode($name); + +	/** +	* Disable BBCodes in general +	*/ +	public function disable_bbcodes(); + +	/** +	* Disable the censor +	*/ +	public function disable_censor(); + +	/** +	* Disable magic URLs +	*/ +	public function disable_magic_url(); + +	/** +	* Disable smilies +	*/ +	public function disable_smilies(); + +	/** +	* Enable a specific BBCode +	* +	* @param  string $name BBCode name +	* @return null +	*/ +	public function enable_bbcode($name); + +	/** +	* Enable BBCodes in general +	*/ +	public function enable_bbcodes(); + +	/** +	* Enable the censor +	*/ +	public function enable_censor(); + +	/** +	* Enable magic URLs +	*/ +	public function enable_magic_url(); + +	/** +	* Enable smilies +	*/ +	public function enable_smilies(); + +	/** +	* Get the list of errors that were generated during last parsing +	* +	* @return array[] Array of arrays. Each array contains a lang string at index 0 plus any number +	*                 of optional parameters +	*/ +	public function get_errors(); + +	/** +	* Set a variable to be used by the parser +	* +	*  - max_font_size +	*  - max_img_height +	*  - max_img_width +	*  - max_smilies +	*  - max_urls +	* +	* @param  string $name +	* @param  mixed  $value +	* @return null +	*/ +	public function set_var($name, $value); + +	/** +	* Set multiple variables to be used by the parser +	* +	* @param  array $vars Associative array of [name => value] +	* @return null +	*/ +	public function set_vars(array $vars); +} diff --git a/phpBB/phpbb/textformatter/renderer_interface.php b/phpBB/phpbb/textformatter/renderer_interface.php new file mode 100644 index 0000000000..609b0bb642 --- /dev/null +++ b/phpBB/phpbb/textformatter/renderer_interface.php @@ -0,0 +1,92 @@ +<?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; + +interface renderer_interface +{ +	/** +	* Render given text +	* +	* @param  string $text Text, as parsed by something that implements \phpbb\textformatter\parser +	* @return string +	*/ +	public function render($text); + +	/** +	* Set the smilies' path +	* +	* @return null +	*/ +	public function set_smilies_path($path); + +	/** +	* Return the value of the "viewcensors" option +	* +	* @return bool Option's value +	*/ +	public function get_viewcensors(); + +	/** +	* Return the value of the "viewflash" option +	* +	* @return bool Option's value +	*/ +	public function get_viewflash(); + +	/** +	* Return the value of the "viewimg" option +	* +	* @return bool Option's value +	*/ +	public function get_viewimg(); + +	/** +	* Return the value of the "viewsmilies" option +	* +	* @return bool Option's value +	*/ +	public function get_viewsmilies(); + +	/** +	* Set the "viewcensors" option +	* +	* @param  bool $value Option's value +	* @return null +	*/ +	public function set_viewcensors($value); + +	/** +	* Set the "viewflash" option +	* +	* @param  bool $value Option's value +	* @return null +	*/ +	public function set_viewflash($value); + +	/** +	* Set the "viewimg" option +	* +	* @param  bool $value Option's value +	* @return null +	*/ +	public function set_viewimg($value); + +	/** +	* Set the "viewsmilies" option +	* +	* @param  bool $value Option's value +	* @return null +	*/ +	public function set_viewsmilies($value); +} diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php new file mode 100644 index 0000000000..d5ad8283d9 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -0,0 +1,652 @@ +<?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\textformatter\s9e\link_helper +	*/ +	protected $link_helper; + +	/** +	* @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} #createChild=LI]{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} $forceLookahead=true]{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 \phpbb\textformatter\s9e\link_helper $link_helper +	* @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, \phpbb\textformatter\s9e\link_helper $link_helper, $cache_dir, $cache_key_parser, $cache_key_renderer) +	{ +		$this->link_helper = $link_helper; +		$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']); +		} +		if (isset($configurator->tags['QUOTE'])) +		{ +			// Remove the nesting limit and let other services remove quotes at parsing time +			$configurator->tags['QUOTE']->nestingLimit = PHP_INT_MAX; +		} + +		// 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->set( +				$row['code'], +				'<img class="smilies" src="{$T_SMILIES_PATH}/' . $this->escape_html_attribute($row['smiley_url']) . '" width="' . $row['smiley_width'] . '" height="' . $row['smiley_height'] . '" alt="{.}" title="' . $this->escape_html_attribute($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 .\\]]'; + +			// Ignore emoticons that are immediately followed by a "word" character +			$configurator->Emoticons->notBefore = '\\w'; +		} + +		// 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) +			{ +				$configurator->Censor->add($row['word'], $row['replacement']); +			} +		} + +		// Load the magic links plugins. We do that after BBCodes so that they use the same tags +		$this->configure_autolink($configurator); + +		// 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->omitImageSize(); +		$configurator->Emoji->useSVG(); +		$tag = $configurator->Emoji->getTag(); +		$tag->template = '<xsl:choose><xsl:when test="$S_VIEWSMILIES">' . str_replace('class="emoji"', 'class="emoji 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(); + +		/** +		* Access the objects returned by finalize() before they are saved to cache +		* +		* @event core.text_formatter_s9e_configure_finalize +		* @var array objects Array containing a "parser" object, a "renderer" object and optionally a "js" string +		* @since 3.2.2-RC1 +		*/ +		$vars = array('objects'); +		extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_finalize', compact($vars))); + +		$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); +	} + +	/** +	* Configure the Autolink / Autoemail plugins used to linkify text +	* +	* @param  \s9e\TextFormatter\Configurator $configurator +	* @return void +	*/ +	protected function configure_autolink(Configurator $configurator) +	{ +		$configurator->plugins->load('Autoemail'); +		$configurator->plugins->load('Autolink', array('matchWww' => true)); + +		// Add a tag filter that creates a tag that stores and replace the +		// content of a link created by the Autolink plugin +		$configurator->Autolink->getTag()->filterChain +			->add(array($this->link_helper, 'generate_link_text_tag')) +			->resetParameters() +			->addParameterByName('tag') +			->addParameterByName('parser'); + +		// Create a tag that will be used to display the truncated text by +		// replacing the original content with the content of the @text attribute +		$tag = $configurator->tags->add('LINK_TEXT'); +		$tag->attributes->add('text'); +		$tag->template = '<xsl:value-of select="@text"/>'; + +		$tag->filterChain +			->add(array($this->link_helper, 'truncate_local_url')) +			->resetParameters() +			->addParameterByName('tag') +			->addParameterByValue(generate_board_url() . '/'); +		$tag->filterChain +			->add(array($this->link_helper, 'truncate_text')) +			->resetParameters() +			->addParameterByName('tag'); +		$tag->filterChain +			->add(array($this->link_helper, 'cleanup_tag')) +			->resetParameters() +			->addParameterByName('tag') +			->addParameterByName('parser'); +	} + +	/** +	* Escape a literal to be used in an HTML attribute in an XSL template +	* +	* Escapes "HTML special chars" for obvious reasons and curly braces to avoid them +	* being interpreted as an attribute value template +	* +	* @param  string $value Original string +	* @return string        Escaped string +	*/ +	protected function escape_html_attribute($value) +	{ +		return htmlspecialchars(strtr($value, ['{' => '{{', '}' => '}}']), ENT_COMPAT | ENT_XML1, 'UTF-8'); +	} + +	/** +	* 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 +		// Allow either phpBB template or the Twig syntax +		preg_match_all('#<!-- BEGIN (.*?) -->(.*?)<!-- END .*? -->#s', $template, $matches, PREG_SET_ORDER) ?: +			preg_match_all('#{% for (.*?) in .*? %}(.*?){% endfor %}#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/link_helper.php b/phpBB/phpbb/textformatter/s9e/link_helper.php new file mode 100644 index 0000000000..0f44603dec --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/link_helper.php @@ -0,0 +1,118 @@ +<?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 link_helper +{ +	/** +	* Clean up and invalidate a LINK_TEXT tag if applicable +	* +	* Will invalidate the tag if its replacement text is the same as the original +	* text and would have no visible effect +	* +	* @param  \s9e\TextFormatter\Parser\Tag $tag    LINK_TEXT tag +	* @param  \s9e\TextFormatter\Parser     $parser Parser +	* @return bool                                  Whether the tag is valid +	*/ +	public function cleanup_tag(\s9e\TextFormatter\Parser\Tag $tag, \s9e\TextFormatter\Parser $parser) +	{ +		// Invalidate if the content of the tag matches the text attribute +		$text = substr($parser->getText(), $tag->getPos(), $tag->getLen()); + +		return ($text !== $tag->getAttribute('text')); +	} + +	/** +	* Create a LINK_TEXT tag inside of a link +	* +	* Meant to only apply to linkified URLs and [url] BBCodes without a parameter +	* +	* @param  \s9e\TextFormatter\Parser\Tag $tag    URL tag (start tag) +	* @param  \s9e\TextFormatter\Parser     $parser Parser +	* @return bool                                  Always true to indicate that the tag is valid +	*/ +	public function generate_link_text_tag(\s9e\TextFormatter\Parser\Tag $tag, \s9e\TextFormatter\Parser $parser) +	{ +		// Only create a LINK_TEXT tag if the start tag is paired with an end +		// tag, which is the case with tags from the Autolink plugins and with +		// the [url] BBCode when its content is used for the URL +		if (!$tag->getEndTag() || !$this->should_shorten($tag, $parser->getText())) +		{ +			return true; +		} + +		// Capture the text between the start tag and its end tag +		$start  = $tag->getPos() + $tag->getLen(); +		$end    = $tag->getEndTag()->getPos(); +		$length = $end - $start; +		$text   = substr($parser->getText(), $start, $length); + +		// Create a tag that consumes the link's text +		$parser->addSelfClosingTag('LINK_TEXT', $start, $length)->setAttribute('text', $text); + +		return true; +	} + +	/** +	* Test whether we should shorten this tag's text +	* +	* Will test whether the tag either does not use any markup or uses a single +	* [url] BBCode +	* +	* @param  \s9e\TextFormatter\Parser\Tag $tag  URL tag +	* @param  string                        $text Original text +	* @return bool +	*/ +	protected function should_shorten(\s9e\TextFormatter\Parser\Tag $tag, $text) +	{ +		return ($tag->getLen() === 0 || strtolower(substr($text, $tag->getPos(), $tag->getLen())) === '[url]'); +	} + +	/** +	* Remove the board's root URL from a the start of a string +	* +	* @param  \s9e\TextFormatter\Parser\Tag $tag       LINK_TEXT tag +	* @param  string                        $board_url Forum's root URL (with trailing slash) +	* @return bool                                     Always true to indicate that the tag is valid +	*/ +	public function truncate_local_url(\s9e\TextFormatter\Parser\Tag $tag, $board_url) +	{ +		$text = $tag->getAttribute('text'); +		if (stripos($text, $board_url) === 0 && strlen($text) > strlen($board_url)) +		{ +			$tag->setAttribute('text', substr($text, strlen($board_url))); +		} + +		return true; +	} + +	/** +	* Truncate the replacement text set in a LINK_TEXT tag +	* +	* @param  \s9e\TextFormatter\Parser\Tag $tag LINK_TEXT tag +	* @return bool                               Always true to indicate that the tag is valid +	*/ +	public function truncate_text(\s9e\TextFormatter\Parser\Tag $tag) +	{ +		$text = $tag->getAttribute('text'); +		if (utf8_strlen($text) > 55) +		{ +			$text = utf8_substr($text, 0, 39) . ' ... ' . utf8_substr($text, -10); +		} + +		$tag->setAttribute('text', $text); + +		return true; +	} +} diff --git a/phpBB/phpbb/textformatter/s9e/parser.php b/phpBB/phpbb/textformatter/s9e/parser.php new file mode 100644 index 0000000000..05ddfffa11 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/parser.php @@ -0,0 +1,399 @@ +<?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'); +		$this->parser->disablePlugin('Emoji'); +	} + +	/** +	* {@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'); +		$this->parser->enablePlugin('Emoji'); +	} + +	/** +	* {@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(, $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..86c33c7591 --- /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}', false); +		$this->profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={USER_ID}', false); +		$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..6fcd2b0a98 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/renderer.php @@ -0,0 +1,313 @@ +<?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))); + +		$html = $this->renderer->render($xml); +		if (isset($this->censor) && $this->viewcensors) +		{ +			$html = $this->censor->censorHtml($html, true); +		} + +		/** +		* 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..a9a6d4b892 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/utils.php @@ -0,0 +1,152 @@ +<?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); +	} + +	/** +	 * {@inheritdoc} +	 */ +	public function is_empty($text) +	{ +		if ($text === null || $text === '') +		{ +			return true; +		} + +		return trim($this->unparse($text)) === ''; +	} +} diff --git a/phpBB/phpbb/textformatter/utils_interface.php b/phpBB/phpbb/textformatter/utils_interface.php new file mode 100644 index 0000000000..4b7392976a --- /dev/null +++ b/phpBB/phpbb/textformatter/utils_interface.php @@ -0,0 +1,79 @@ +<?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; + +/** +* Used to manipulate a parsed text +*/ +interface utils_interface +{ +	/** +	* Replace BBCodes and other formatting elements with whitespace +	* +	* NOTE: preserves smilies as text +	* +	* @param  string $text Parsed text +	* @return string       Plain text +	*/ +	public function clean_formatting($text); + +	/** +	* Create a quote block for given text +	* +	* Possible attributes: +	*   - author:  author's name (usually a username) +	*   - post_id: post_id of the post being quoted +	*   - user_id: user_id of the user being quoted +	*   - time:    timestamp of the original message +	* +	* @param  string $text       Quote's text +	* @param  array  $attributes Quote's attributes +	* @return string             Quote block to be used in a new post/text +	*/ +	public function generate_quote($text, array $attributes = array()); + +	/** +	* Get a list of quote authors, limited to the outermost quotes +	* +	* @param  string   $text Parsed text +	* @return string[]       List of authors +	*/ +	public function get_outermost_quote_authors($text); + +	/** +	* Remove given BBCode and its content, at given nesting depth +	* +	* @param  string  $text        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($text, $bbcode_name, $depth = 0); + +	/** +	 * Return a parsed text to its original form +	 * +	 * @param  string $text Parsed text +	 * @return string       Original plain text +	 */ +	public function unparse($text); + +	/** +	 * Return whether or not a parsed text represent an empty text. +	 * +	 * @param  string $text Parsed text +	 * @return bool         Tue if the original text is empty +	 */ +	public function is_empty($text); +} | 
