Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.49% |
195 / 196 |
|
90.91% |
10 / 11 |
CRAP | |
0.00% |
0 / 1 |
factory | |
99.49% |
195 / 196 |
|
90.91% |
10 / 11 |
48 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
invalidate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tidy | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
get_configurator | |
100.00% |
71 / 71 |
|
100.00% |
1 / 1 |
11 | |||
regenerate | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
3 | |||
add_bbcode | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
configure_autolink | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
1 | |||
escape_html_attribute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
get_default_bbcodes | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
10 | |||
extract_templates | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
9 | |||
merge_templates | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | namespace phpbb\textformatter\s9e; |
15 | |
16 | use s9e\TextFormatter\Configurator; |
17 | use s9e\TextFormatter\Configurator\Items\AttributeFilters\RegexpFilter; |
18 | use s9e\TextFormatter\Configurator\Items\UnsafeTemplate; |
19 | |
20 | /** |
21 | * Creates s9e\TextFormatter objects |
22 | */ |
23 | class factory implements \phpbb\textformatter\cache_interface |
24 | { |
25 | /** |
26 | * @var \phpbb\textformatter\s9e\link_helper |
27 | */ |
28 | protected $link_helper; |
29 | |
30 | /** |
31 | * @var \phpbb\cache\driver\driver_interface |
32 | */ |
33 | protected $cache; |
34 | |
35 | /** |
36 | * @var string Path to the cache dir |
37 | */ |
38 | protected $cache_dir; |
39 | |
40 | /** |
41 | * @var string Cache key used for the parser |
42 | */ |
43 | protected $cache_key_parser; |
44 | |
45 | /** |
46 | * @var string Cache key used for the renderer |
47 | */ |
48 | protected $cache_key_renderer; |
49 | |
50 | /** |
51 | * @var \phpbb\config\config |
52 | */ |
53 | protected $config; |
54 | |
55 | /** |
56 | * @var array Custom tokens used in bbcode.html and their corresponding token from the definition |
57 | */ |
58 | protected $custom_tokens = array( |
59 | 'email' => array('{DESCRIPTION}' => '{TEXT}'), |
60 | 'img' => array('{URL}' => '{IMAGEURL}'), |
61 | 'list' => array('{LIST_TYPE}' => '{HASHMAP}'), |
62 | 'quote' => array('{USERNAME}' => '{TEXT1}'), |
63 | 'size' => array('{SIZE}' => '{FONTSIZE}'), |
64 | 'url' => array('{DESCRIPTION}' => '{TEXT}'), |
65 | ); |
66 | |
67 | /** |
68 | * @var \phpbb\textformatter\data_access |
69 | */ |
70 | protected $data_access; |
71 | |
72 | /** |
73 | * @var array Default BBCode definitions |
74 | */ |
75 | protected $default_definitions = array( |
76 | 'attachment' => '[ATTACHMENT index={NUMBER} filename={TEXT;useContent}]', |
77 | 'b' => '[B]{TEXT}[/B]', |
78 | 'code' => '[CODE lang={IDENTIFIER;optional}]{TEXT}[/CODE]', |
79 | 'color' => '[COLOR={COLOR}]{TEXT}[/COLOR]', |
80 | 'email' => '[EMAIL={EMAIL;useContent} subject={TEXT1;optional;postFilter=rawurlencode} body={TEXT2;optional;postFilter=rawurlencode}]{TEXT}[/EMAIL]', |
81 | 'i' => '[I]{TEXT}[/I]', |
82 | 'img' => '[IMG src={IMAGEURL;useContent}]', |
83 | '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]', |
84 | 'li' => '[* $tagName=LI]{TEXT}[/*]', |
85 | 'mention' => |
86 | "[MENTION={PARSE=/^g:(?'group_id'\d+)|u:(?'user_id'\d+)$/} |
87 | group_id={UINT;optional} |
88 | profile_url={URL;optional;postFilter=#false} |
89 | user_id={UINT;optional} |
90 | ]{TEXT}[/MENTION]", |
91 | 'quote' => |
92 | "[QUOTE |
93 | author={TEXT1;optional} |
94 | post_id={UINT;optional} |
95 | post_url={URL;optional;postFilter=#false} |
96 | msg_id={UINT;optional} |
97 | msg_url={URL;optional;postFilter=#false} |
98 | profile_url={URL;optional;postFilter=#false} |
99 | time={UINT;optional} |
100 | url={URL;optional} |
101 | user_id={UINT;optional} |
102 | author={PARSE=/^\\[url=(?'url'.*?)](?'author'.*)\\[\\/url]$/i} |
103 | author={PARSE=/^\\[url](?'author'(?'url'.*?))\\[\\/url]$/i} |
104 | author={PARSE=/(?'url'https?:\\/\\/[^[\\]]+)/i} |
105 | ]{TEXT2}[/QUOTE]", |
106 | 'size' => '[SIZE={FONTSIZE}]{TEXT}[/SIZE]', |
107 | 'u' => '[U]{TEXT}[/U]', |
108 | 'url' => '[URL={URL;useContent} $forceLookahead=true]{TEXT}[/URL]', |
109 | ); |
110 | |
111 | /** |
112 | * @var array Default templates, taken from bbcode::bbcode_tpl() |
113 | */ |
114 | protected $default_templates = array( |
115 | 'b' => '<span style="font-weight: bold"><xsl:apply-templates/></span>', |
116 | 'i' => '<span style="font-style: italic"><xsl:apply-templates/></span>', |
117 | 'u' => '<span style="text-decoration: underline"><xsl:apply-templates/></span>', |
118 | 'img' => '<img src="{IMAGEURL}" class="postimage" alt="{L_IMAGE}"/>', |
119 | 'size' => '<span><xsl:attribute name="style"><xsl:text>font-size: </xsl:text><xsl:value-of select="substring(@size, 1, 4)"/><xsl:text>%; line-height: normal</xsl:text></xsl:attribute><xsl:apply-templates/></span>', |
120 | 'color' => '<span style="color: {COLOR}"><xsl:apply-templates/></span>', |
121 | 'email' => '<a> |
122 | <xsl:attribute name="href"> |
123 | <xsl:text>mailto:</xsl:text> |
124 | <xsl:value-of select="@email"/> |
125 | <xsl:if test="@subject or @body"> |
126 | <xsl:text>?</xsl:text> |
127 | <xsl:if test="@subject">subject=<xsl:value-of select="@subject"/></xsl:if> |
128 | <xsl:if test="@body"><xsl:if test="@subject">&</xsl:if>body=<xsl:value-of select="@body"/></xsl:if> |
129 | </xsl:if> |
130 | </xsl:attribute> |
131 | <xsl:apply-templates/> |
132 | </a>', |
133 | 'mention' => '<xsl:text>@</xsl:text> |
134 | <xsl:choose> |
135 | <xsl:when test="@profile_url"> |
136 | <a class="mention" href="{@profile_url}"> |
137 | <xsl:apply-templates/> |
138 | </a> |
139 | </xsl:when> |
140 | <xsl:otherwise> |
141 | <span class="mention"> |
142 | <xsl:apply-templates/> |
143 | </span> |
144 | </xsl:otherwise> |
145 | </xsl:choose>', |
146 | ); |
147 | |
148 | /** |
149 | * @var \phpbb\event\dispatcher_interface |
150 | */ |
151 | protected $dispatcher; |
152 | |
153 | /** |
154 | * @var \phpbb\log\log_interface |
155 | */ |
156 | protected $log; |
157 | |
158 | /** |
159 | * Constructor |
160 | * |
161 | * @param \phpbb\textformatter\data_access $data_access |
162 | * @param \phpbb\cache\driver\driver_interface $cache |
163 | * @param \phpbb\event\dispatcher_interface $dispatcher |
164 | * @param \phpbb\config\config $config |
165 | * @param \phpbb\textformatter\s9e\link_helper $link_helper |
166 | * @param \phpbb\log\log_interface $log |
167 | * @param string $cache_dir Path to the cache dir |
168 | * @param string $cache_key_parser Cache key used for the parser |
169 | * @param string $cache_key_renderer Cache key used for the renderer |
170 | */ |
171 | 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, \phpbb\log\log_interface $log, $cache_dir, $cache_key_parser, $cache_key_renderer) |
172 | { |
173 | $this->link_helper = $link_helper; |
174 | $this->cache = $cache; |
175 | $this->cache_dir = $cache_dir; |
176 | $this->cache_key_parser = $cache_key_parser; |
177 | $this->cache_key_renderer = $cache_key_renderer; |
178 | $this->config = $config; |
179 | $this->data_access = $data_access; |
180 | $this->dispatcher = $dispatcher; |
181 | $this->log = $log; |
182 | } |
183 | |
184 | /** |
185 | * {@inheritdoc} |
186 | */ |
187 | public function invalidate() |
188 | { |
189 | $this->regenerate(); |
190 | } |
191 | |
192 | /** |
193 | * {@inheritdoc} |
194 | * |
195 | * Will remove old renderers from the cache dir but won't touch the current renderer |
196 | */ |
197 | public function tidy() |
198 | { |
199 | // Get the name of current renderer |
200 | $renderer_data = $this->cache->get($this->cache_key_renderer); |
201 | $renderer_file = ($renderer_data) ? $renderer_data['class'] . '.php' : null; |
202 | |
203 | foreach (glob($this->cache_dir . 's9e_*') as $filename) |
204 | { |
205 | // Only remove the file if it's not the current renderer |
206 | if (!$renderer_file || substr($filename, -strlen($renderer_file)) !== $renderer_file) |
207 | { |
208 | unlink($filename); |
209 | } |
210 | } |
211 | } |
212 | |
213 | /** |
214 | * Generate and return a new configured instance of s9e\TextFormatter\Configurator |
215 | * |
216 | * @return Configurator |
217 | */ |
218 | public function get_configurator() |
219 | { |
220 | // Create a new Configurator |
221 | $configurator = new Configurator; |
222 | |
223 | /** |
224 | * Modify the s9e\TextFormatter configurator before the default settings are set |
225 | * |
226 | * @event core.text_formatter_s9e_configure_before |
227 | * @var Configurator configurator Configurator instance |
228 | * @since 3.2.0-a1 |
229 | * @psalm-ignore-var |
230 | */ |
231 | $vars = ['configurator']; |
232 | extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_before', compact($vars))); |
233 | |
234 | // Reset the list of allowed schemes |
235 | foreach ($configurator->urlConfig->getAllowedSchemes() as $scheme) |
236 | { |
237 | $configurator->urlConfig->disallowScheme($scheme); |
238 | } |
239 | foreach (array_filter(explode(',', $this->config['allowed_schemes_links'])) as $scheme) |
240 | { |
241 | $configurator->urlConfig->allowScheme(trim($scheme)); |
242 | } |
243 | |
244 | // Convert newlines to br elements by default |
245 | $configurator->rootRules->enableAutoLineBreaks(); |
246 | |
247 | // Don't automatically ignore text in places where text is not allowed |
248 | $configurator->rulesGenerator->remove('IgnoreTextIfDisallowed'); |
249 | |
250 | // Don't remove comments and instead convert them to xsl:comment elements |
251 | $configurator->templateNormalizer->remove('RemoveComments'); |
252 | $configurator->templateNormalizer->add('TransposeComments'); |
253 | |
254 | // Set the rendering engine and configure it to save to the cache dir |
255 | $configurator->rendering->engine = 'PHP'; |
256 | $configurator->rendering->engine->cacheDir = $this->cache_dir; |
257 | $configurator->rendering->engine->defaultClassPrefix = 's9e_renderer_'; |
258 | $configurator->rendering->engine->enableQuickRenderer = true; |
259 | |
260 | // Create custom filters for BBCode tokens that are supported in phpBB but not in |
261 | // s9e\TextFormatter |
262 | $filter = new RegexpFilter('#^' . get_preg_expression('relative_url') . '$#Du'); |
263 | $configurator->attributeFilters->add('#local_url', $filter); |
264 | $configurator->attributeFilters->add('#relative_url', $filter); |
265 | |
266 | // INTTEXT regexp from acp_bbcodes |
267 | $filter = new RegexpFilter('!^([\p{L}\p{N}\-+,_. ]+)$!Du'); |
268 | $configurator->attributeFilters->add('#inttext', $filter); |
269 | |
270 | // Create a custom filter for phpBB's per-mode font size limits |
271 | $configurator->attributeFilters |
272 | ->add('#fontsize', __NAMESPACE__ . '\\parser::filter_font_size') |
273 | ->addParameterByName('max_font_size') |
274 | ->addParameterByName('logger') |
275 | ->markAsSafeInCSS(); |
276 | |
277 | // Create a custom filter for image URLs |
278 | $configurator->attributeFilters |
279 | ->add('#imageurl', __NAMESPACE__ . '\\parser::filter_img_url') |
280 | ->addParameterByName('urlConfig') |
281 | ->addParameterByName('logger') |
282 | ->markAsSafeAsURL() |
283 | ->setJS('UrlFilter.filter'); |
284 | |
285 | // Add default BBCodes |
286 | foreach ($this->get_default_bbcodes($configurator) as $bbcode) |
287 | { |
288 | $this->add_bbcode($configurator, $bbcode['usage'], $bbcode['template']); |
289 | } |
290 | if (isset($configurator->tags['QUOTE'])) |
291 | { |
292 | // Remove the nesting limit and let other services remove quotes at parsing time |
293 | $configurator->tags['QUOTE']->nestingLimit = PHP_INT_MAX; |
294 | } |
295 | |
296 | // Modify the template to disable images/mentions depending on user's settings |
297 | foreach (['IMG', 'MENTION'] as $name) |
298 | { |
299 | $tag = $configurator->tags[$name]; |
300 | $tag->template = '<xsl:choose><xsl:when test="$S_VIEW' . $name . '">' . $tag->template . '</xsl:when><xsl:otherwise><xsl:apply-templates/></xsl:otherwise></xsl:choose>'; |
301 | } |
302 | |
303 | // Load custom BBCodes |
304 | foreach ($this->data_access->get_bbcodes() as $row) |
305 | { |
306 | // Insert the board's URL before {LOCAL_URL} tokens |
307 | $tpl = preg_replace_callback( |
308 | '#\\{LOCAL_URL\\d*\\}#', |
309 | function ($m) |
310 | { |
311 | return generate_board_url() . '/' . $m[0]; |
312 | }, |
313 | $row['bbcode_tpl'] |
314 | ); |
315 | $this->add_bbcode($configurator, $row['bbcode_match'], $tpl); |
316 | } |
317 | |
318 | // Load smilies |
319 | foreach ($this->data_access->get_smilies() as $row) |
320 | { |
321 | $configurator->Emoticons->set( |
322 | $row['code'], |
323 | '<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']) . '"/>' |
324 | ); |
325 | } |
326 | |
327 | if (isset($configurator->Emoticons)) |
328 | { |
329 | // Force emoticons to be rendered as text if $S_VIEWSMILIES is not set |
330 | $configurator->Emoticons->notIfCondition = 'not($S_VIEWSMILIES)'; |
331 | |
332 | // Only parse emoticons at the beginning of the text or if they're preceded by any |
333 | // one of: a new line, a space, a dot, or a right square bracket |
334 | $configurator->Emoticons->notAfter = '[^\\n .\\]]'; |
335 | |
336 | // Ignore emoticons that are immediately followed by a "word" character |
337 | $configurator->Emoticons->notBefore = '\\w'; |
338 | } |
339 | |
340 | // Load the censored words |
341 | $censor = $this->data_access->get_censored_words(); |
342 | if (!empty($censor)) |
343 | { |
344 | // Use a namespaced tag to avoid collisions |
345 | $configurator->plugins->load('Censor', array('tagName' => 'censor:tag')); |
346 | foreach ($censor as $row) |
347 | { |
348 | $configurator->Censor->add($row['word'], $row['replacement']); |
349 | } |
350 | } |
351 | |
352 | // Load the magic links plugins. We do that after BBCodes so that they use the same tags |
353 | $this->configure_autolink($configurator); |
354 | |
355 | // Register some vars with a default value. Those should be set at runtime by whatever calls |
356 | // the parser |
357 | $configurator->registeredVars['max_font_size'] = 0; |
358 | |
359 | // Load the Emoji plugin and modify its tag's template to obey viewsmilies |
360 | $tag = $configurator->Emoji->getTag(); |
361 | $tag->template = '<xsl:choose> |
362 | <xsl:when test="@tseq"> |
363 | <img alt="{.}" class="emoji" draggable="false" src="//cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/{@tseq}.svg"/> |
364 | </xsl:when> |
365 | <xsl:otherwise> |
366 | <img alt="{.}" class="emoji" draggable="false" src="//cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/{@seq}.svg"/> |
367 | </xsl:otherwise> |
368 | </xsl:choose>'; |
369 | $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>'; |
370 | |
371 | /** |
372 | * Modify the s9e\TextFormatter configurator after the default settings are set |
373 | * |
374 | * @event core.text_formatter_s9e_configure_after |
375 | * @var Configurator configurator Configurator instance |
376 | * @since 3.2.0-a1 |
377 | * @psalm-ignore-var |
378 | */ |
379 | $vars = ['configurator']; |
380 | extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_after', compact($vars))); |
381 | |
382 | return $configurator; |
383 | } |
384 | |
385 | /** |
386 | * Regenerate and cache a new parser and renderer |
387 | * |
388 | * @return array Associative array with at least two elements: "parser" and "renderer" |
389 | */ |
390 | public function regenerate() |
391 | { |
392 | $configurator = $this->get_configurator(); |
393 | |
394 | // Get the censor helper and remove the Censor plugin if applicable |
395 | if (isset($configurator->Censor)) |
396 | { |
397 | $censor = $configurator->Censor->getHelper(); |
398 | unset($configurator->Censor); |
399 | unset($configurator->tags['censor:tag']); |
400 | } |
401 | |
402 | $objects = $configurator->finalize(); |
403 | |
404 | /** |
405 | * Access the objects returned by finalize() before they are saved to cache |
406 | * |
407 | * @event core.text_formatter_s9e_configure_finalize |
408 | * @var array objects Array containing a "parser" object, a "renderer" object and optionally a "js" string |
409 | * @since 3.2.2-RC1 |
410 | */ |
411 | $vars = array('objects'); |
412 | extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_finalize', compact($vars))); |
413 | |
414 | $parser = $objects['parser']; |
415 | $renderer = $objects['renderer']; |
416 | |
417 | // Cache the parser as-is |
418 | $this->cache->put($this->cache_key_parser, $parser); |
419 | |
420 | // We need to cache the name of the renderer's generated class |
421 | $renderer_data = array('class' => get_class($renderer)); |
422 | if (isset($censor)) |
423 | { |
424 | $renderer_data['censor'] = $censor; |
425 | } |
426 | $this->cache->put($this->cache_key_renderer, $renderer_data); |
427 | |
428 | return array('parser' => $parser, 'renderer' => $renderer); |
429 | } |
430 | |
431 | /** |
432 | * Add a BBCode to given configurator |
433 | * |
434 | * @param Configurator $configurator |
435 | * @param string $usage |
436 | * @param string $template |
437 | * @return void |
438 | */ |
439 | protected function add_bbcode(Configurator $configurator, $usage, $template) |
440 | { |
441 | try |
442 | { |
443 | $configurator->BBCodes->addCustom($usage, new UnsafeTemplate($template)); |
444 | } |
445 | catch (\Exception $e) |
446 | { |
447 | $this->log->add('critical', ANONYMOUS, '', 'LOG_BBCODE_CONFIGURATION_ERROR', false, [$usage, $e->getMessage()]); |
448 | } |
449 | } |
450 | |
451 | /** |
452 | * Configure the Autolink / Autoemail plugins used to linkify text |
453 | * |
454 | * @param Configurator $configurator |
455 | * @return void |
456 | */ |
457 | protected function configure_autolink(Configurator $configurator) |
458 | { |
459 | $configurator->plugins->load('Autoemail'); |
460 | $configurator->plugins->load('Autolink', array('matchWww' => true)); |
461 | |
462 | // Add a tag filter that creates a tag that stores and replace the |
463 | // content of a link created by the Autolink plugin |
464 | $configurator->Autolink->getTag()->filterChain |
465 | ->add(array($this->link_helper, 'generate_link_text_tag')) |
466 | ->resetParameters() |
467 | ->addParameterByName('tag') |
468 | ->addParameterByName('parser'); |
469 | |
470 | // Create a tag that will be used to display the truncated text by |
471 | // replacing the original content with the content of the @text attribute |
472 | $tag = $configurator->tags->add('LINK_TEXT'); |
473 | $tag->attributes->add('text'); |
474 | $tag->template = '<xsl:value-of select="@text"/>'; |
475 | |
476 | $board_url = generate_board_url() . '/'; |
477 | $tag->filterChain |
478 | ->add(array($this->link_helper, 'truncate_local_url')) |
479 | ->resetParameters() |
480 | ->addParameterByName('tag') |
481 | ->addParameterByValue($board_url); |
482 | $tag->filterChain |
483 | ->add(array($this->link_helper, 'truncate_local_url')) |
484 | ->resetParameters() |
485 | ->addParameterByName('tag') |
486 | ->addParameterByValue(preg_replace('(^\\w+:)', '', $board_url)); |
487 | $tag->filterChain |
488 | ->add(array($this->link_helper, 'truncate_text')) |
489 | ->resetParameters() |
490 | ->addParameterByName('tag'); |
491 | $tag->filterChain |
492 | ->add(array($this->link_helper, 'cleanup_tag')) |
493 | ->resetParameters() |
494 | ->addParameterByName('tag') |
495 | ->addParameterByName('parser'); |
496 | } |
497 | |
498 | /** |
499 | * Escape a literal to be used in an HTML attribute in an XSL template |
500 | * |
501 | * Escapes "HTML special chars" for obvious reasons and curly braces to avoid them |
502 | * being interpreted as an attribute value template |
503 | * |
504 | * @param string $value Original string |
505 | * @return string Escaped string |
506 | */ |
507 | protected function escape_html_attribute($value) |
508 | { |
509 | return htmlspecialchars(strtr($value, ['{' => '{{', '}' => '}}']), ENT_COMPAT | ENT_XML1, 'UTF-8'); |
510 | } |
511 | |
512 | /** |
513 | * Return the default BBCodes configuration |
514 | * |
515 | * @return array 2D array. Each element has a 'usage' key, a 'template' key, and an optional 'options' key |
516 | */ |
517 | protected function get_default_bbcodes($configurator) |
518 | { |
519 | // For each BBCode, build an associative array matching style_ids to their template |
520 | $templates = array(); |
521 | foreach ($this->data_access->get_styles_templates() as $style_id => $data) |
522 | { |
523 | foreach ($this->extract_templates($data['template']) as $bbcode_name => $template) |
524 | { |
525 | $templates[$bbcode_name][$style_id] = $template; |
526 | } |
527 | |
528 | // Add default templates wherever missing, or for BBCodes that were not specified in |
529 | // this template's bitfield. For instance, prosilver has a custom template for b but its |
530 | // bitfield does not enable it so the default template is used instead |
531 | foreach ($this->default_templates as $bbcode_name => $template) |
532 | { |
533 | if (!isset($templates[$bbcode_name][$style_id]) || !in_array($bbcode_name, $data['bbcodes'], true)) |
534 | { |
535 | $templates[$bbcode_name][$style_id] = $template; |
536 | } |
537 | } |
538 | } |
539 | |
540 | // Replace custom tokens and normalize templates |
541 | foreach ($templates as $bbcode_name => $style_templates) |
542 | { |
543 | foreach ($style_templates as $i => $template) |
544 | { |
545 | if (isset($this->custom_tokens[$bbcode_name])) |
546 | { |
547 | $template = strtr($template, $this->custom_tokens[$bbcode_name]); |
548 | } |
549 | |
550 | $templates[$bbcode_name][$i] = $configurator->templateNormalizer->normalizeTemplate($template); |
551 | } |
552 | } |
553 | |
554 | $bbcodes = array(); |
555 | foreach ($this->default_definitions as $bbcode_name => $usage) |
556 | { |
557 | $bbcodes[$bbcode_name] = array( |
558 | 'usage' => $usage, |
559 | 'template' => $this->merge_templates($templates[$bbcode_name]), |
560 | ); |
561 | } |
562 | |
563 | return $bbcodes; |
564 | } |
565 | |
566 | /** |
567 | * Extract and recompose individual BBCode templates from a style's template file |
568 | * |
569 | * @param string $template Style template (bbcode.html) |
570 | * @return array Associative array matching BBCode names to their template |
571 | */ |
572 | protected function extract_templates($template) |
573 | { |
574 | // Capture the template fragments |
575 | // Allow either phpBB template or the Twig syntax |
576 | preg_match_all('#<!-- BEGIN (.*?) -->(.*?)<!-- END .*? -->#s', $template, $matches, PREG_SET_ORDER) ?: |
577 | preg_match_all('#{% for (.*?) in .*? %}(.*?){% endfor %}#s', $template, $matches, PREG_SET_ORDER); |
578 | |
579 | $fragments = array(); |
580 | foreach ($matches as $match) |
581 | { |
582 | // Normalize the whitespace |
583 | $fragment = preg_replace('#>\\n\\t*<#', '><', trim($match[2])); |
584 | |
585 | $fragments[$match[1]] = $fragment; |
586 | } |
587 | |
588 | // Automatically recompose templates split between *_open and *_close |
589 | foreach ($fragments as $fragment_name => $fragment) |
590 | { |
591 | if (preg_match('#^(\\w+)_close$#', $fragment_name, $match)) |
592 | { |
593 | $bbcode_name = $match[1]; |
594 | |
595 | if (isset($fragments[$bbcode_name . '_open'])) |
596 | { |
597 | $templates[$bbcode_name] = $fragments[$bbcode_name . '_open'] . '<xsl:apply-templates/>' . $fragment; |
598 | } |
599 | } |
600 | } |
601 | |
602 | // Manually recompose and overwrite irregular templates |
603 | $templates['list'] = |
604 | '<xsl:choose> |
605 | <xsl:when test="not(@type)"> |
606 | ' . $fragments['ulist_open_default'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . ' |
607 | </xsl:when> |
608 | <xsl:when test="contains(\'upperlowerdecim\',substring(@type,1,5))"> |
609 | ' . $fragments['olist_open'] . '<xsl:apply-templates/>' . $fragments['olist_close'] . ' |
610 | </xsl:when> |
611 | <xsl:otherwise> |
612 | ' . $fragments['ulist_open'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . ' |
613 | </xsl:otherwise> |
614 | </xsl:choose>'; |
615 | |
616 | $templates['li'] = $fragments['listitem'] . '<xsl:apply-templates/>' . $fragments['listitem_close']; |
617 | |
618 | // Replace the regular quote template with the extended quote template if available |
619 | if (isset($fragments['quote_extended'])) |
620 | { |
621 | $templates['quote'] = $fragments['quote_extended']; |
622 | } |
623 | |
624 | // The [attachment] BBCode uses the inline_attachment template to output a comment that |
625 | // is post-processed by parse_attachments() |
626 | $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']; |
627 | |
628 | // Add fragments as templates |
629 | foreach ($fragments as $fragment_name => $fragment) |
630 | { |
631 | if (preg_match('#^\\w+$#', $fragment_name)) |
632 | { |
633 | $templates[$fragment_name] = $fragment; |
634 | } |
635 | } |
636 | |
637 | // Keep only templates that are named after an existing BBCode |
638 | $templates = array_intersect_key($templates, $this->default_definitions); |
639 | |
640 | return $templates; |
641 | } |
642 | |
643 | /** |
644 | * Merge the templates from any number of styles into one BBCode template |
645 | * |
646 | * When multiple templates are available for the same BBCode (because of multiple styles) we |
647 | * merge them into a single template that uses an xsl:choose construct that determines which |
648 | * style to use at rendering time. |
649 | * |
650 | * @param array $style_templates Associative array matching style_ids to their template |
651 | * @return string |
652 | */ |
653 | protected function merge_templates(array $style_templates) |
654 | { |
655 | // Return the template as-is if there's only one style or all styles share the same template |
656 | if (count(array_unique($style_templates)) === 1) |
657 | { |
658 | return end($style_templates); |
659 | } |
660 | |
661 | // Group identical templates together |
662 | $grouped_templates = array(); |
663 | foreach ($style_templates as $style_id => $style_template) |
664 | { |
665 | $grouped_templates[$style_template][] = '$STYLE_ID=' . $style_id; |
666 | } |
667 | |
668 | // Sort templates by frequency descending |
669 | $templates_cnt = array_map('sizeof', $grouped_templates); |
670 | array_multisort($grouped_templates, $templates_cnt); |
671 | |
672 | // Remove the most frequent template from the list; It becomes the default |
673 | reset($grouped_templates); |
674 | $default_template = key($grouped_templates); |
675 | unset($grouped_templates[$default_template]); |
676 | |
677 | // Build an xsl:choose switch |
678 | $template = '<xsl:choose>'; |
679 | foreach ($grouped_templates as $style_template => $exprs) |
680 | { |
681 | $template .= '<xsl:when test="' . implode(' or ', $exprs) . '">' . $style_template . '</xsl:when>'; |
682 | } |
683 | $template .= '<xsl:otherwise>' . $default_template . '</xsl:otherwise></xsl:choose>'; |
684 | |
685 | return $template; |
686 | } |
687 | } |