Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.48% |
193 / 194 |
|
90.91% |
10 / 11 |
CRAP | |
0.00% |
0 / 1 |
factory | |
99.48% |
193 / 194 |
|
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% |
69 / 69 |
|
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 = '<span class="emoji"><xsl:value-of select="."/></span>'; |
362 | |
363 | /** |
364 | * Modify the s9e\TextFormatter configurator after the default settings are set |
365 | * |
366 | * @event core.text_formatter_s9e_configure_after |
367 | * @var Configurator configurator Configurator instance |
368 | * @since 3.2.0-a1 |
369 | * @psalm-ignore-var |
370 | */ |
371 | $vars = ['configurator']; |
372 | extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_after', compact($vars))); |
373 | |
374 | return $configurator; |
375 | } |
376 | |
377 | /** |
378 | * Regenerate and cache a new parser and renderer |
379 | * |
380 | * @return array Associative array with at least two elements: "parser" and "renderer" |
381 | */ |
382 | public function regenerate() |
383 | { |
384 | $configurator = $this->get_configurator(); |
385 | |
386 | // Get the censor helper and remove the Censor plugin if applicable |
387 | if (isset($configurator->Censor)) |
388 | { |
389 | $censor = $configurator->Censor->getHelper(); |
390 | unset($configurator->Censor); |
391 | unset($configurator->tags['censor:tag']); |
392 | } |
393 | |
394 | $objects = $configurator->finalize(); |
395 | |
396 | /** |
397 | * Access the objects returned by finalize() before they are saved to cache |
398 | * |
399 | * @event core.text_formatter_s9e_configure_finalize |
400 | * @var array objects Array containing a "parser" object, a "renderer" object and optionally a "js" string |
401 | * @since 3.2.2-RC1 |
402 | */ |
403 | $vars = array('objects'); |
404 | extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_finalize', compact($vars))); |
405 | |
406 | $parser = $objects['parser']; |
407 | $renderer = $objects['renderer']; |
408 | |
409 | // Cache the parser as-is |
410 | $this->cache->put($this->cache_key_parser, $parser); |
411 | |
412 | // We need to cache the name of the renderer's generated class |
413 | $renderer_data = array('class' => get_class($renderer)); |
414 | if (isset($censor)) |
415 | { |
416 | $renderer_data['censor'] = $censor; |
417 | } |
418 | $this->cache->put($this->cache_key_renderer, $renderer_data); |
419 | |
420 | return array('parser' => $parser, 'renderer' => $renderer); |
421 | } |
422 | |
423 | /** |
424 | * Add a BBCode to given configurator |
425 | * |
426 | * @param Configurator $configurator |
427 | * @param string $usage |
428 | * @param string $template |
429 | * @return void |
430 | */ |
431 | protected function add_bbcode(Configurator $configurator, $usage, $template) |
432 | { |
433 | try |
434 | { |
435 | $configurator->BBCodes->addCustom($usage, new UnsafeTemplate($template)); |
436 | } |
437 | catch (\Exception $e) |
438 | { |
439 | $this->log->add('critical', ANONYMOUS, '', 'LOG_BBCODE_CONFIGURATION_ERROR', false, [$usage, $e->getMessage()]); |
440 | } |
441 | } |
442 | |
443 | /** |
444 | * Configure the Autolink / Autoemail plugins used to linkify text |
445 | * |
446 | * @param Configurator $configurator |
447 | * @return void |
448 | */ |
449 | protected function configure_autolink(Configurator $configurator) |
450 | { |
451 | $configurator->plugins->load('Autoemail'); |
452 | $configurator->plugins->load('Autolink', array('matchWww' => true)); |
453 | |
454 | // Add a tag filter that creates a tag that stores and replace the |
455 | // content of a link created by the Autolink plugin |
456 | $configurator->Autolink->getTag()->filterChain |
457 | ->add(array($this->link_helper, 'generate_link_text_tag')) |
458 | ->resetParameters() |
459 | ->addParameterByName('tag') |
460 | ->addParameterByName('parser'); |
461 | |
462 | // Create a tag that will be used to display the truncated text by |
463 | // replacing the original content with the content of the @text attribute |
464 | $tag = $configurator->tags->add('LINK_TEXT'); |
465 | $tag->attributes->add('text'); |
466 | $tag->template = '<xsl:value-of select="@text"/>'; |
467 | |
468 | $board_url = generate_board_url() . '/'; |
469 | $tag->filterChain |
470 | ->add(array($this->link_helper, 'truncate_local_url')) |
471 | ->resetParameters() |
472 | ->addParameterByName('tag') |
473 | ->addParameterByValue($board_url); |
474 | $tag->filterChain |
475 | ->add(array($this->link_helper, 'truncate_local_url')) |
476 | ->resetParameters() |
477 | ->addParameterByName('tag') |
478 | ->addParameterByValue(preg_replace('(^\\w+:)', '', $board_url)); |
479 | $tag->filterChain |
480 | ->add(array($this->link_helper, 'truncate_text')) |
481 | ->resetParameters() |
482 | ->addParameterByName('tag'); |
483 | $tag->filterChain |
484 | ->add(array($this->link_helper, 'cleanup_tag')) |
485 | ->resetParameters() |
486 | ->addParameterByName('tag') |
487 | ->addParameterByName('parser'); |
488 | } |
489 | |
490 | /** |
491 | * Escape a literal to be used in an HTML attribute in an XSL template |
492 | * |
493 | * Escapes "HTML special chars" for obvious reasons and curly braces to avoid them |
494 | * being interpreted as an attribute value template |
495 | * |
496 | * @param string $value Original string |
497 | * @return string Escaped string |
498 | */ |
499 | protected function escape_html_attribute($value) |
500 | { |
501 | return htmlspecialchars(strtr($value, ['{' => '{{', '}' => '}}']), ENT_COMPAT | ENT_XML1, 'UTF-8'); |
502 | } |
503 | |
504 | /** |
505 | * Return the default BBCodes configuration |
506 | * |
507 | * @return array 2D array. Each element has a 'usage' key, a 'template' key, and an optional 'options' key |
508 | */ |
509 | protected function get_default_bbcodes($configurator) |
510 | { |
511 | // For each BBCode, build an associative array matching style_ids to their template |
512 | $templates = array(); |
513 | foreach ($this->data_access->get_styles_templates() as $style_id => $data) |
514 | { |
515 | foreach ($this->extract_templates($data['template']) as $bbcode_name => $template) |
516 | { |
517 | $templates[$bbcode_name][$style_id] = $template; |
518 | } |
519 | |
520 | // Add default templates wherever missing, or for BBCodes that were not specified in |
521 | // this template's bitfield. For instance, prosilver has a custom template for b but its |
522 | // bitfield does not enable it so the default template is used instead |
523 | foreach ($this->default_templates as $bbcode_name => $template) |
524 | { |
525 | if (!isset($templates[$bbcode_name][$style_id]) || !in_array($bbcode_name, $data['bbcodes'], true)) |
526 | { |
527 | $templates[$bbcode_name][$style_id] = $template; |
528 | } |
529 | } |
530 | } |
531 | |
532 | // Replace custom tokens and normalize templates |
533 | foreach ($templates as $bbcode_name => $style_templates) |
534 | { |
535 | foreach ($style_templates as $i => $template) |
536 | { |
537 | if (isset($this->custom_tokens[$bbcode_name])) |
538 | { |
539 | $template = strtr($template, $this->custom_tokens[$bbcode_name]); |
540 | } |
541 | |
542 | $templates[$bbcode_name][$i] = $configurator->templateNormalizer->normalizeTemplate($template); |
543 | } |
544 | } |
545 | |
546 | $bbcodes = array(); |
547 | foreach ($this->default_definitions as $bbcode_name => $usage) |
548 | { |
549 | $bbcodes[$bbcode_name] = array( |
550 | 'usage' => $usage, |
551 | 'template' => $this->merge_templates($templates[$bbcode_name]), |
552 | ); |
553 | } |
554 | |
555 | return $bbcodes; |
556 | } |
557 | |
558 | /** |
559 | * Extract and recompose individual BBCode templates from a style's template file |
560 | * |
561 | * @param string $template Style template (bbcode.html) |
562 | * @return array Associative array matching BBCode names to their template |
563 | */ |
564 | protected function extract_templates($template) |
565 | { |
566 | // Capture the template fragments |
567 | // Allow either phpBB template or the Twig syntax |
568 | preg_match_all('#<!-- BEGIN (.*?) -->(.*?)<!-- END .*? -->#s', $template, $matches, PREG_SET_ORDER) ?: |
569 | preg_match_all('#{% for (.*?) in .*? %}(.*?){% endfor %}#s', $template, $matches, PREG_SET_ORDER); |
570 | |
571 | $fragments = array(); |
572 | foreach ($matches as $match) |
573 | { |
574 | // Normalize the whitespace |
575 | $fragment = preg_replace('#>\\n\\t*<#', '><', trim($match[2])); |
576 | |
577 | $fragments[$match[1]] = $fragment; |
578 | } |
579 | |
580 | // Automatically recompose templates split between *_open and *_close |
581 | foreach ($fragments as $fragment_name => $fragment) |
582 | { |
583 | if (preg_match('#^(\\w+)_close$#', $fragment_name, $match)) |
584 | { |
585 | $bbcode_name = $match[1]; |
586 | |
587 | if (isset($fragments[$bbcode_name . '_open'])) |
588 | { |
589 | $templates[$bbcode_name] = $fragments[$bbcode_name . '_open'] . '<xsl:apply-templates/>' . $fragment; |
590 | } |
591 | } |
592 | } |
593 | |
594 | // Manually recompose and overwrite irregular templates |
595 | $templates['list'] = |
596 | '<xsl:choose> |
597 | <xsl:when test="not(@type)"> |
598 | ' . $fragments['ulist_open_default'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . ' |
599 | </xsl:when> |
600 | <xsl:when test="contains(\'upperlowerdecim\',substring(@type,1,5))"> |
601 | ' . $fragments['olist_open'] . '<xsl:apply-templates/>' . $fragments['olist_close'] . ' |
602 | </xsl:when> |
603 | <xsl:otherwise> |
604 | ' . $fragments['ulist_open'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . ' |
605 | </xsl:otherwise> |
606 | </xsl:choose>'; |
607 | |
608 | $templates['li'] = $fragments['listitem'] . '<xsl:apply-templates/>' . $fragments['listitem_close']; |
609 | |
610 | // Replace the regular quote template with the extended quote template if available |
611 | if (isset($fragments['quote_extended'])) |
612 | { |
613 | $templates['quote'] = $fragments['quote_extended']; |
614 | } |
615 | |
616 | // The [attachment] BBCode uses the inline_attachment template to output a comment that |
617 | // is post-processed by parse_attachments() |
618 | $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']; |
619 | |
620 | // Add fragments as templates |
621 | foreach ($fragments as $fragment_name => $fragment) |
622 | { |
623 | if (preg_match('#^\\w+$#', $fragment_name)) |
624 | { |
625 | $templates[$fragment_name] = $fragment; |
626 | } |
627 | } |
628 | |
629 | // Keep only templates that are named after an existing BBCode |
630 | $templates = array_intersect_key($templates, $this->default_definitions); |
631 | |
632 | return $templates; |
633 | } |
634 | |
635 | /** |
636 | * Merge the templates from any number of styles into one BBCode template |
637 | * |
638 | * When multiple templates are available for the same BBCode (because of multiple styles) we |
639 | * merge them into a single template that uses an xsl:choose construct that determines which |
640 | * style to use at rendering time. |
641 | * |
642 | * @param array $style_templates Associative array matching style_ids to their template |
643 | * @return string |
644 | */ |
645 | protected function merge_templates(array $style_templates) |
646 | { |
647 | // Return the template as-is if there's only one style or all styles share the same template |
648 | if (count(array_unique($style_templates)) === 1) |
649 | { |
650 | return end($style_templates); |
651 | } |
652 | |
653 | // Group identical templates together |
654 | $grouped_templates = array(); |
655 | foreach ($style_templates as $style_id => $style_template) |
656 | { |
657 | $grouped_templates[$style_template][] = '$STYLE_ID=' . $style_id; |
658 | } |
659 | |
660 | // Sort templates by frequency descending |
661 | $templates_cnt = array_map('sizeof', $grouped_templates); |
662 | array_multisort($grouped_templates, $templates_cnt); |
663 | |
664 | // Remove the most frequent template from the list; It becomes the default |
665 | reset($grouped_templates); |
666 | $default_template = key($grouped_templates); |
667 | unset($grouped_templates[$default_template]); |
668 | |
669 | // Build an xsl:choose switch |
670 | $template = '<xsl:choose>'; |
671 | foreach ($grouped_templates as $style_template => $exprs) |
672 | { |
673 | $template .= '<xsl:when test="' . implode(' or ', $exprs) . '">' . $style_template . '</xsl:when>'; |
674 | } |
675 | $template .= '<xsl:otherwise>' . $default_template . '</xsl:otherwise></xsl:choose>'; |
676 | |
677 | return $template; |
678 | } |
679 | } |