Structure.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. <?php
  2. /**
  3. * This file is part of the ForkBB <https://github.com/forkbb>.
  4. *
  5. * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
  6. * @license The MIT License (MIT)
  7. */
  8. declare(strict_types=1);
  9. namespace ForkBB\Models\BBCodeList;
  10. use ForkBB\Core\Container;
  11. use ForkBB\Models\Model;
  12. use RuntimeException;
  13. use Throwable;
  14. class Structure extends Model
  15. {
  16. const TAG_PATTERN = '%^(?:ROOT|[a-z\*][a-z\d-]{0,10})$%D';
  17. const ATTR_PATTERN = '%^[a-z-]{2,15}$%D';
  18. /**
  19. * Ключ модели для контейнера
  20. */
  21. protected string $cKey = 'BBStructure';
  22. public function __construct(Container $container)
  23. {
  24. parent::__construct($container);
  25. $this->zDepend = [
  26. 'attrs' => ['no_attr', 'def_attr', 'other_attrs'],
  27. ];
  28. }
  29. public function isInDefault(): bool
  30. {
  31. if (empty($this->tag)) {
  32. return false;
  33. }
  34. $bbcode = include $this->c->bbcode->fileDefault;
  35. foreach ($bbcode as $cur) {
  36. if ($this->tag === $cur['tag']) {
  37. return true;
  38. }
  39. }
  40. return false;
  41. }
  42. public function setDefault(): Structure
  43. {
  44. if (! $this->isInDefault()) {
  45. throw new RuntimeException("There is no default for the '{$this->tag}' tag");
  46. }
  47. $bbcode = include $this->c->bbcode->fileDefault;
  48. foreach ($bbcode as $cur) {
  49. if ($this->tag === $cur['tag']) {
  50. return $this->setModelAttrs($cur);
  51. }
  52. }
  53. }
  54. public function fromString(string $data): Structure
  55. {
  56. return $this->setModelAttrs(\json_decode($data, true, 512, \JSON_THROW_ON_ERROR));
  57. }
  58. public function toString(): string
  59. {
  60. $a = [
  61. 'tag' => $this->tag,
  62. 'type' => $this->type,
  63. 'parents' => $this->parents,
  64. ];
  65. if (null !== $this->auto) {
  66. $a['auto'] = (bool) $this->auto;
  67. }
  68. if (null !== $this->self_nesting) {
  69. $a['self_nesting'] = (int) $this->self_nesting > 0 ? (int) $this->self_nesting : false;
  70. }
  71. if (null !== $this->recursive) {
  72. $a['recursive'] = true;
  73. }
  74. if (null !== $this->text_only) {
  75. $a['text_only'] = true;
  76. }
  77. if (null !== $this->tags_only) {
  78. $a['tags_only'] = true;
  79. }
  80. if (null !== $this->single) {
  81. $a['single'] = true;
  82. }
  83. if (null !== $this->pre) {
  84. $a['pre'] = true;
  85. }
  86. if (
  87. \is_array($this->new_attr)
  88. && ! empty($this->new_attr['allowed'])
  89. && ! empty($this->new_attr['name'])
  90. ) {
  91. $this->setBBAttr($this->new_attr['name'], $this->new_attr, ['required', 'format', 'body_format', 'text_only']);
  92. }
  93. $a['attrs'] = $this->other_attrs;
  94. if (null !== $this->no_attr) {
  95. $a['attrs']['No_attr'] = $this->no_attr;
  96. }
  97. if (null !== $this->def_attr) {
  98. $a['attrs']['Def'] = $this->def_attr;
  99. }
  100. if (empty($a['attrs'])) {
  101. unset($a['attrs']);
  102. }
  103. if (! empty($this->handler) && \is_string($this->handler)) {
  104. $a['handler'] = $this->handler;
  105. }
  106. if (! empty($this->text_handler) && \is_string($this->text_handler)) {
  107. $a['text_handler'] = $this->text_handler;
  108. }
  109. return \json_encode($a, FORK_JSON_ENCODE);
  110. }
  111. protected function gettype(): string
  112. {
  113. $type = $this->getModelAttr('type');
  114. return \is_string($type) ? $type : 'inline';
  115. }
  116. protected function getparents(): array
  117. {
  118. $parents = $this->getModelAttr('parents');
  119. if (\is_array($parents)) {
  120. return $parents;
  121. } elseif ('inline' === $this->type) {
  122. return ['inline', 'block'];
  123. } else {
  124. return ['block'];
  125. }
  126. }
  127. protected function setrecursive($value): void
  128. {
  129. $value = empty($value) ? null : true;
  130. $this->setModelAttr('recursive', $value);
  131. }
  132. protected function settext_only($value): void
  133. {
  134. $value = empty($value) ? null : true;
  135. $this->setModelAttr('text_only', $value);
  136. }
  137. protected function settags_only($value): void
  138. {
  139. $value = empty($value) ? null : true;
  140. $this->setModelAttr('tags_only', $value);
  141. }
  142. protected function setpre($value): void
  143. {
  144. $value = empty($value) ? null : true;
  145. $this->setModelAttr('pre', $value);
  146. }
  147. protected function setsingle($value): void
  148. {
  149. $value = empty($value) ? null : true;
  150. $this->setModelAttr('single', $value);
  151. }
  152. protected function getauto(): bool
  153. {
  154. $auto = $this->getModelAttr('auto');
  155. if (\is_bool($auto)) {
  156. return $auto;
  157. } elseif ('inline' === $this->type) {
  158. return true;
  159. } else {
  160. return false;
  161. }
  162. }
  163. protected function setauto($value): void
  164. {
  165. $value = ! empty($value);
  166. $this->setModelAttr('auto', $value);
  167. }
  168. protected function setself_nesting($value): void
  169. {
  170. $value = (int) $value < 1 ? false : (int) $value;
  171. $this->setModelAttr('self_nesting', $value);
  172. }
  173. protected function getBBAttr(string $name, array $fields): mixed
  174. {
  175. if (empty($this->attrs[$name])) {
  176. return null;
  177. }
  178. $data = $this->attrs[$name];
  179. if (true === $data) {
  180. return true;
  181. } elseif (! \is_array($data)) {
  182. return null;
  183. } else {
  184. $result = [];
  185. foreach ($fields as $field) {
  186. switch ($field) {
  187. case 'format':
  188. case 'body_format':
  189. $value = isset($data[$field]) && \is_string($data[$field]) ? $data[$field] : null;
  190. break;
  191. case 'required':
  192. case 'text_only':
  193. $value = isset($data[$field]) ? true : null;
  194. break;
  195. default:
  196. throw new RuntimeException('Unknown attribute property');
  197. }
  198. $result[$field] = $value;
  199. }
  200. return $result;
  201. }
  202. }
  203. protected function setBBAttr(string $name, mixed $data, array $fields): void
  204. {
  205. $attrs = $this->getModelAttr('attrs');
  206. if (
  207. empty($data['allowed'])
  208. || $data['allowed'] < 1
  209. ) {
  210. unset($attrs[$name]);
  211. } else {
  212. $result = [];
  213. foreach ($fields as $field) {
  214. switch ($field) {
  215. case 'format':
  216. case 'body_format':
  217. $value = ! empty($data[$field]) && \is_string($data[$field]) ? $data[$field] : null;
  218. break;
  219. case 'required':
  220. case 'text_only':
  221. $value = ! empty($data[$field]) ? true : null;
  222. break;
  223. default:
  224. throw new RuntimeException('Unknown attribute property');
  225. }
  226. if (isset($value)) {
  227. $result[$field] = $value;
  228. }
  229. }
  230. $attrs[$name] = empty($result) ? true : $result;
  231. }
  232. $this->setModelAttr('attrs', $attrs);
  233. }
  234. protected function getno_attr(): mixed
  235. {
  236. return $this->getBBAttr('No_attr', ['body_format', 'text_only']);
  237. }
  238. protected function setno_attr(array $value): void
  239. {
  240. $this->setBBAttr('No_attr', $value, ['body_format', 'text_only']);
  241. }
  242. protected function getdef_attr(): mixed
  243. {
  244. return $this->getBBAttr('Def', ['required', 'format', 'body_format', 'text_only']);
  245. }
  246. protected function setdef_attr(array $value): void
  247. {
  248. $this->setBBAttr('Def', $value, ['required', 'format', 'body_format', 'text_only']);
  249. }
  250. protected function getother_attrs(): array
  251. {
  252. $attrs = $this->getModelAttr('attrs');
  253. if (! \is_array($attrs)) {
  254. return [];
  255. }
  256. unset($attrs['No_attr'], $attrs['Def'], $attrs['New']);
  257. $result = [];
  258. foreach ($attrs as $name => $attr) {
  259. $value = $this->getBBAttr($name, ['required', 'format', 'body_format', 'text_only']);
  260. if (null !== $value) {
  261. $result[$name] = $value;
  262. }
  263. }
  264. return $result;
  265. }
  266. protected function setother_attrs(array $attrs): void
  267. {
  268. unset($attrs['No_attr'], $attrs['Def']);
  269. foreach ($attrs as $name => $attr) {
  270. $this->setBBAttr($name, $attr, ['required', 'format', 'body_format', 'text_only']);
  271. }
  272. }
  273. /**
  274. * Ищет ошибку в структуре bb-кода
  275. */
  276. public function getError(): ?array
  277. {
  278. if (
  279. ! \is_string($this->tag)
  280. || ! \preg_match(self::TAG_PATTERN, $this->tag)
  281. ) {
  282. return ['Tag name not specified'];
  283. }
  284. $result = $this->testPHP($this->handler);
  285. if (null !== $result) {
  286. return ['PHP code error in Handler: %s', $result];
  287. }
  288. $result = $this->testPHP($this->text_handler);
  289. if (null !== $result ) {
  290. return ['PHP code error in Text handler: %s', $result];
  291. }
  292. if (
  293. null !== $this->recursive
  294. && null !== $this->tags_only
  295. ) {
  296. return ['Recursive and Tags only are enabled at the same time'];
  297. }
  298. if (
  299. null !== $this->recursive
  300. && null !== $this->single
  301. ) {
  302. return ['Recursive and Single are enabled at the same time'];
  303. }
  304. if (
  305. null !== $this->text_only
  306. && null !== $this->tags_only
  307. ) {
  308. return ['Text only and Tags only are enabled at the same time'];
  309. }
  310. if (\is_array($this->attrs)) {
  311. foreach ($this->attrs as $name => $attr) {
  312. if (
  313. 'No_attr' !== $name
  314. && 'Def' !== $name
  315. && ! \preg_match(self::ATTR_PATTERN, $name)
  316. ) {
  317. return ['Attribute name %s is not valid', $name];
  318. }
  319. if (isset($attr['format'])) {
  320. $result = ['Attribute %1$s, %2$s - regular expression error', $name, 'Format'];
  321. try {
  322. if (
  323. ! \is_string($attr['format'])
  324. || false === @\preg_match($attr['format'], 'abcdef')
  325. ) {
  326. return $result;
  327. }
  328. } catch (Throwable $e) {
  329. return $result;
  330. }
  331. }
  332. if (isset($attr['body_format'])) {
  333. $result = ['Attribute %1$s, %2$s - regular expression error', $name, 'Body format'];
  334. try {
  335. if (
  336. ! \is_string($attr['body_format'])
  337. || false === @\preg_match($attr['body_format'], 'abcdef')
  338. ) {
  339. return $result;
  340. }
  341. } catch (Throwable $e) {
  342. return $result;
  343. }
  344. }
  345. }
  346. }
  347. if (
  348. \is_array($this->new_attr)
  349. && ! empty($this->new_attr['allowed'])
  350. && ! empty($this->new_attr['name'])
  351. ) {
  352. $name = $this->new_attr['name'];
  353. if (
  354. 'No_attr' === $name
  355. || 'Def' === $name
  356. || isset($this->attrs[$name])
  357. || ! \preg_match(self::ATTR_PATTERN, $name)
  358. ) {
  359. return ['Attribute name %s is not valid', $name];
  360. }
  361. if (isset($this->new_attr['format'])) {
  362. $result = ['Attribute %1$s, %2$s - regular expression error', $name, 'Format'];
  363. try {
  364. if (
  365. ! \is_string($this->new_attr['format'])
  366. || false === @\preg_match($this->new_attr['format'], 'abcdef')
  367. ) {
  368. return $result;
  369. }
  370. } catch (Throwable $e) {
  371. return $result;
  372. }
  373. }
  374. if (isset($this->new_attr['body_format'])) {
  375. $result = ['Attribute %1$s, %2$s - regular expression error', $name, 'Body format'];
  376. try {
  377. if (
  378. ! \is_string($this->new_attr['body_format'])
  379. || false === @\preg_match($this->new_attr['body_format'], 'abcdef')
  380. ) {
  381. return $result;
  382. }
  383. } catch (Throwable $e) {
  384. return $result;
  385. }
  386. }
  387. }
  388. return null;
  389. }
  390. protected function testPHP(?string $code): ?string
  391. {
  392. if (
  393. null === $code
  394. || '' === $code
  395. ) {
  396. return null;
  397. }
  398. // тест на парность скобок
  399. $testCode = \preg_replace('%//[^\r\n]*+|#[^\r\n]*+|/\*.*?\*/|\'.*?(?<!\\\\)\'|".*?(?<!\\\\)"%s', '', $code);
  400. if (false === \preg_match_all('%[(){}\[\]]%s', $testCode, $matches)) {
  401. throw new RuntimeException('The preg_match_all() returned an error');
  402. }
  403. $round = 0;
  404. $square = 0;
  405. $curly = 0;
  406. foreach ($matches[0] as $value) {
  407. switch ($value) {
  408. case '(':
  409. ++$round;
  410. break;
  411. case ')':
  412. --$round;
  413. if ($round < 0) {
  414. return '\')\' > \'(\'.';
  415. }
  416. break;
  417. case '[':
  418. ++$square;
  419. break;
  420. case ']':
  421. --$square;
  422. if ($square < 0) {
  423. return '\']\' > \'[\'.';
  424. }
  425. break;
  426. case '{':
  427. ++$curly;
  428. break;
  429. case '}':
  430. --$curly;
  431. if ($curly < 0) {
  432. return '\'}\' > \'{\'.';
  433. }
  434. break;
  435. default:
  436. throw new RuntimeException('Unknown bracket type');
  437. }
  438. }
  439. if (0 !== $round) {
  440. return '\'(\' != \')\'.';
  441. }
  442. if (0 !== $square) {
  443. return '\'[\' != \']\'.';
  444. }
  445. if (0 !== $curly) {
  446. return '\'{\' != \'}\'.';
  447. }
  448. // тест на выполнение DANGER! DANGER! DANGER! O_o
  449. $testCode = "\$testVar = function (\$body, \$attrs, \$parser) { {$code} };\nreturn true;";
  450. try {
  451. $result = @eval($testCode);
  452. if (true !== $result) {
  453. $error = \error_get_last();
  454. $message = $error['message'] ?? 'Unknown error';
  455. $line = $error['line'] ?? '';
  456. return "{$message}: [$line]";
  457. }
  458. } catch (Throwable $e) {
  459. return "{$e->getMessage()}: [{$e->getLine()}]";
  460. }
  461. return null;
  462. }
  463. }