Skip to content

Commit 199c6f4

Browse files
committed
1 parent 0c7e674 commit 199c6f4

12 files changed

+1175
-13
lines changed

src/BBCache.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
/**
3+
* Class BBCache
4+
*
5+
* @filesource BBCache.php
6+
* @created 26.04.2018
7+
* @package chillerlan\BBCode
8+
* @author smiley <smiley@chillerlan.net>
9+
* @copyright 2018 smiley
10+
* @license MIT
11+
*/
12+
13+
namespace chillerlan\BBCode;
14+
15+
use Psr\SimpleCache\CacheInterface;
16+
17+
class BBCache implements CacheInterface{
18+
19+
/**
20+
* @var array
21+
*/
22+
protected $cache = [];
23+
24+
/**
25+
* @inheritdoc
26+
*/
27+
public function get($key, $default = null){
28+
return $this->cache[$key] ?? $default;
29+
}
30+
31+
/**
32+
* @inheritdoc
33+
*/
34+
public function set($key, $value, $ttl = null){
35+
$this->cache[$key] = $value;
36+
37+
return true;
38+
}
39+
40+
/**
41+
* @inheritdoc
42+
*/
43+
public function delete($key){
44+
unset($this->cache[$key]);
45+
46+
return true;
47+
}
48+
49+
/**
50+
* @inheritdoc
51+
*/
52+
public function clear(){
53+
$this->cache = [];
54+
55+
return true;
56+
}
57+
58+
/**
59+
* @inheritdoc
60+
*/
61+
public function getMultiple($keys, $default = null){
62+
$data = [];
63+
64+
foreach($keys as $key){
65+
$data[$key] = $this->cache[$key] ?? $default;
66+
}
67+
68+
return $data;
69+
}
70+
71+
/**
72+
* @inheritdoc
73+
*/
74+
public function setMultiple($values, $ttl = null){
75+
76+
foreach($values as $key => $value){
77+
$this->cache[$key] = $value;
78+
}
79+
80+
return true;
81+
}
82+
83+
/**
84+
* @inheritdoc
85+
*/
86+
public function deleteMultiple($keys){
87+
88+
foreach($keys as $key){
89+
unset($this->cache[$key]);
90+
}
91+
92+
return true;
93+
}
94+
95+
/**
96+
* @inheritdoc
97+
*/
98+
public function has($key){
99+
return isset($this->cache[$key]);
100+
}
101+
102+
}

src/BBCode.php

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
<?php
2+
/**
3+
* Class BBCode
4+
*
5+
* @filesource BBCode.php
6+
* @created 19.04.2018
7+
* @package chillerlan\BBCode
8+
* @author smiley <smiley@chillerlan.net>
9+
* @copyright 2018 smiley
10+
* @license MIT
11+
*/
12+
13+
namespace chillerlan\BBCode;
14+
15+
use chillerlan\BBCode\Output\BBCodeOutputInterface;
16+
use chillerlan\Traits\{
17+
ClassLoader, ContainerInterface
18+
};
19+
use Psr\Log\{
20+
LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger
21+
};
22+
use Psr\SimpleCache\CacheInterface;
23+
24+
class BBCode implements LoggerAwareInterface{
25+
use ClassLoader, LoggerAwareTrait;
26+
27+
/**
28+
* @var \chillerlan\BBCode\BBCodeOptions|\chillerlan\Traits\ContainerInterface
29+
*/
30+
protected $options;
31+
32+
/**
33+
* @var \Psr\SimpleCache\CacheInterface
34+
*/
35+
protected $cache;
36+
37+
/**
38+
* @var \chillerlan\BBCode\SanitizerInterface
39+
*/
40+
protected $sanitizerInterface;
41+
42+
/**
43+
* @var \chillerlan\BBCode\Output\BBCodeOutputInterface
44+
*/
45+
protected $outputInterface;
46+
47+
/**
48+
* @var \chillerlan\BBCode\ParserMiddlewareInterface
49+
*/
50+
protected $parserMiddleware;
51+
52+
/**
53+
* @var array
54+
*/
55+
protected $tags = [];
56+
57+
/**
58+
* @var array
59+
*/
60+
protected $noparse = [];
61+
62+
/**
63+
* @var array
64+
*/
65+
protected $allowed = [];
66+
67+
/**
68+
* BBCode constructor.
69+
*
70+
* @param \chillerlan\Traits\ContainerInterface|null $options
71+
* @param \Psr\SimpleCache\CacheInterface|null $cache
72+
* @param \Psr\Log\LoggerInterface|null $logger
73+
*/
74+
public function __construct(ContainerInterface $options = null, CacheInterface $cache = null, LoggerInterface $logger = null){
75+
$this
76+
->setCache($cache ?? new BBCache)
77+
->setLogger($logger ?? new NullLogger);
78+
79+
$this->setOptions($options ?? new BBCodeOptions);
80+
}
81+
82+
/**
83+
* @param array $allowedTags
84+
*
85+
* @return \chillerlan\BBCode\BBCode
86+
*/
87+
public function allowTags(array $allowedTags):BBCode{
88+
$this->allowed = [];
89+
90+
foreach($allowedTags as $tag){
91+
$tag = strtolower($tag);
92+
93+
if(in_array($tag, $this->tags, true)){
94+
$this->allowed[] = $tag;
95+
}
96+
}
97+
98+
return $this;
99+
}
100+
101+
/**
102+
* @param \Psr\SimpleCache\CacheInterface $cache
103+
*
104+
* @return \chillerlan\BBCode\BBCode
105+
*/
106+
public function setCache(CacheInterface $cache):BBCode{
107+
$this->cache = $cache;
108+
109+
return $this;
110+
}
111+
112+
/**
113+
* @todo
114+
*
115+
* @param \chillerlan\Traits\ContainerInterface $options
116+
*
117+
* @throws \chillerlan\BBCode\BBCodeException
118+
* @return \chillerlan\BBCode\BBCode
119+
*/
120+
public function setOptions(ContainerInterface $options):BBCode{
121+
$this->options = $options;
122+
123+
mb_internal_encoding('UTF-8');
124+
125+
if(
126+
ini_set('pcre.backtrack_limit', $this->options->pcre_backtrack_limit) === false
127+
|| ini_set('pcre.recursion_limit', $this->options->pcre_recursion_limit) === false
128+
|| ini_set('pcre.jit', $this->options->pcre_jit) === false
129+
){
130+
throw new BBCodeException('could not alter ini settings');
131+
}
132+
133+
if(ini_get('pcre.backtrack_limit') !== (string)$this->options->pcre_backtrack_limit
134+
|| ini_get('pcre.recursion_limit') !== (string)$this->options->pcre_recursion_limit
135+
|| ini_get('pcre.jit') !== (string)$this->options->pcre_jit
136+
){
137+
throw new BBCodeException('ini settings differ from options');
138+
}
139+
140+
if($this->options->sanitizeInput || $this->options->sanitizeOutput){
141+
$this->sanitizerInterface = $this->loadClass($this->options->sanitizerInterface, SanitizerInterface::class, $this->options);
142+
}
143+
144+
145+
146+
if($this->options->preParse || $this->options->postParse){
147+
$this->parserMiddleware = $this->loadClass($this->options->parserMiddlewareInterface, ParserMiddlewareInterface::class, $this->options, $this->cache, $this->logger);
148+
}
149+
150+
$this->outputInterface = $this->loadClass($this->options->outputInterface, BBCodeOutputInterface::class, $this->options, $this->cache, $this->logger);
151+
152+
$this->tags = $this->outputInterface->getTags();
153+
$this->noparse = $this->outputInterface->getNoparse();
154+
155+
if(is_array($this->options->allowedTags) && !empty($this->options->allowedTags)){
156+
$this->allowTags($this->options->allowedTags);
157+
}
158+
elseif($this->options->allowAvailableTags === true){
159+
$this->allowed = $this->tags;
160+
}
161+
162+
return $this;
163+
}
164+
165+
/**
166+
* Transforms a BBCode string to HTML (or whatevs)
167+
*
168+
* @param string $bbcode
169+
*
170+
* @return string
171+
*/
172+
public function parse(string $bbcode):string{
173+
174+
// sanitize the input if needed
175+
if($this->options->sanitizeInput){
176+
$bbcode = $this->sanitizerInterface->sanitizeInput($bbcode);
177+
}
178+
179+
// run the pre-parser
180+
if($this->options->preParse){
181+
$bbcode = $this->parserMiddleware->pre($bbcode);
182+
}
183+
184+
// @todo: array < 2 elements causes a PREG_BACKTRACK_LIMIT_ERROR! (breaks match pattern)
185+
$singleTags = array_merge(['br', 'hr'], $this->outputInterface->getSingleTags());
186+
187+
// close singletags: [br] -> [br][/br]
188+
$bbcode = preg_replace('#\[('.implode('|', $singleTags).')((?:\s|=)[^]]*)?]#is', '[$1$2][/$1]', $bbcode);
189+
// protect newlines
190+
$bbcode = str_replace(["\r", "\n"], ['', $this->options->placeholder_eol], $bbcode);
191+
// parse the bbcode
192+
$bbcode = $this->parseBBCode($bbcode);
193+
194+
// run the post-parser
195+
if($this->options->postParse){
196+
$bbcode = $this->parserMiddleware->post($bbcode);
197+
}
198+
199+
// replace the newline placeholders
200+
$bbcode = str_replace($this->options->placeholder_eol, PHP_EOL, $bbcode);
201+
202+
// run the sanitizer/html purifier/whatever as a final step
203+
if($this->options->sanitizeOutput){
204+
$bbcode = $this->sanitizerInterface->sanitizeOutput($bbcode);
205+
}
206+
207+
return $bbcode;
208+
}
209+
210+
/**
211+
* @param $bbcode
212+
*
213+
* @return string
214+
*/
215+
protected function parseBBCode($bbcode):string{
216+
static $callback_count = 0;
217+
218+
$callback = false;
219+
220+
if(is_array($bbcode) && count($bbcode) === 4){
221+
[$match, $tag, $attributes, $content] = $bbcode;
222+
223+
$tag = strtolower($tag);
224+
$attributes = $this->parseAttributes($attributes);
225+
$callback = true;
226+
227+
$callback_count++;
228+
}
229+
else if(is_string($bbcode) && !empty($bbcode)){
230+
$match = null;
231+
$tag = null;
232+
$attributes = [];
233+
$content = $bbcode;
234+
}
235+
else{
236+
return '';
237+
}
238+
239+
if($callback_count < (int)$this->options->nestingLimit && !in_array($tag, $this->noparse , true)){
240+
$content = preg_replace_callback('#\[(\w+)((?:\s|=)[^]]*)?]((?:[^[]|\[(?!/?\1((?:\s|=)[^]]*)?])|(?R))*)\[/\1]#', __METHOD__, $content);
241+
$e = preg_last_error();
242+
243+
/**
244+
* 1 - PREG_INTERNAL_ERROR
245+
* 2 - PREG_BACKTRACK_LIMIT_ERROR
246+
* 3 - PREG_RECURSION_LIMIT_ERROR
247+
* 4 - PREG_BAD_UTF8_ERROR
248+
* 5 - PREG_BAD_UTF8_OFFSET_ERROR
249+
* 6 - PREG_JIT_STACKLIMIT_ERROR
250+
*/
251+
if($e !== PREG_NO_ERROR){
252+
$this->logger->debug('preg_error', ['errno' => $e, '$content' => $content]);
253+
254+
$content = $match ?? '';//$content ?? $bbcode ??
255+
}
256+
}
257+
258+
if($callback === true && in_array($tag, $this->allowed, true)){
259+
$content = $this->outputInterface->transform($tag, $attributes, $content, $match, $callback_count);
260+
$callback_count = 0;
261+
}
262+
263+
return $content;
264+
}
265+
266+
/**
267+
* @param string $attributes
268+
*
269+
* @return array
270+
*/
271+
protected function parseAttributes(string $attributes):array{
272+
$attr = [];
273+
274+
// @todo: fix attributes pattern: accept single and double quotes around the value
275+
if(preg_match_all('#(?<name>^|\w+)\=(\'?)(?<value>[^\']*?)\2(?: |$)#', $attributes, $matches, PREG_SET_ORDER) > 0){
276+
277+
foreach($matches as $attribute){
278+
$name = empty($attribute['name']) ? $this->options->placeholder_bbtag : strtolower(trim($attribute['name']));
279+
280+
$attr[$name] = trim($attribute['value'], '"\' ');
281+
}
282+
}
283+
284+
$e = preg_last_error();
285+
286+
if($e !== PREG_NO_ERROR){
287+
$this->logger->debug('preg_error', ['errno' => $e, '$attributes' => $attributes]);
288+
}
289+
290+
return $attr;
291+
}
292+
293+
}

0 commit comments

Comments
 (0)