diff --git a/composer.json b/composer.json index 05ba98f4..94ad1985 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,9 @@ "ext-json": "*", "ext-mbstring": "*", "doctrine/annotations": "^1.6", - "erusev/parsedown": "^1.8-dev", "florianv/swap": "^4.0", "friendsofsymfony/ckeditor-bundle": "^2.0", + "league/html-to-markdown": "^4.8", "liip/imagine-bundle": "^2.2", "nyholm/psr7": "^1.1", "ocramius/proxy-manager": "2.1.*", @@ -21,7 +21,7 @@ "php-http/curl-client": "^2.0", "php-http/guzzle6-adapter": "^2.0", "php-http/message": "^1.8", - "s9e/text-formatter": "^2.0", + "s9e/text-formatter": "^2.1", "sensio/framework-extra-bundle": "^5.1", "shivas/versioning-bundle": "^3.1", "symfony/apache-pack": "^1.0", diff --git a/composer.lock b/composer.lock index 050f5aee..27f68c77 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d81e2f85d8e6d5ead84ec2272a7f0618", + "content-hash": "f4b935dcb469ba97b409fd529352d145", "packages": [ { "name": "clue/stream-filter", @@ -1316,52 +1316,6 @@ ], "time": "2019-08-13T17:33:27+00:00" }, - { - "name": "erusev/parsedown", - "version": "1.8.0-beta-7", - "source": { - "type": "git", - "url": "https://github.com/erusev/parsedown.git", - "reference": "fe7a50eceb4a3c867cc9fa9c0aa906b1067d1955" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/fe7a50eceb4a3c867cc9fa9c0aa906b1067d1955", - "reference": "fe7a50eceb4a3c867cc9fa9c0aa906b1067d1955", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35" - }, - "type": "library", - "autoload": { - "psr-0": { - "Parsedown": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Emanuil Rusev", - "email": "hello@erusev.com", - "homepage": "http://erusev.com" - } - ], - "description": "Parser for Markdown.", - "homepage": "http://parsedown.org", - "keywords": [ - "markdown", - "parser" - ], - "time": "2019-03-17T18:47:21+00:00" - }, { "name": "fig/link-util", "version": "1.0.0", @@ -1910,6 +1864,70 @@ ], "time": "2014-01-12T16:20:24+00:00" }, + { + "name": "league/html-to-markdown", + "version": "4.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "e747489191f8e9144a7270eb61f8b9516e99e413" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/e747489191f8e9144a7270eb61f8b9516e99e413", + "reference": "e747489191f8e9144a7270eb61f8b9516e99e413", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "~1.1.0", + "phpunit/phpunit": "4.*", + "scrutinizer/ocular": "~1.1" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "time": "2019-08-02T11:57:39+00:00" + }, { "name": "liip/imagine-bundle", "version": "2.2.0", @@ -9275,7 +9293,6 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "erusev/parsedown": 20, "twig/extra-bundle": 20, "twig/intl-extra": 20, "roave/security-advisories": 20 diff --git a/src/Command/ConvertBBCodeCommand.php b/src/Command/ConvertBBCodeCommand.php new file mode 100644 index 00000000..01452ab1 --- /dev/null +++ b/src/Command/ConvertBBCodeCommand.php @@ -0,0 +1,180 @@ +em = $entityManager; + $this->propertyAccessor = $propertyAccessor; + + $this->converter = new BBCodeToMarkdownConverter(); + + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('Converts BBCode used in old Part-DB versions to newly used Markdown') + ->setHelp('Older versions of Part-DB (<1.0) used BBCode for rich text formatting. + Part-DB now uses Markdown which offers more features but is incompatible with BBCode. + When you upgrade from an pre 1.0 version you have to run this command to convert your comment fields'); + + $this->addOption('dry-run', null, null, 'Do not save changes to DB. In combination with -v or -vv you can check what will be changed!'); + } + + /** + * Returns a list which entities and which properties need to be checked. + * @return array + */ + protected function getTargetsLists() : array + { + return [ + Part::class => ['description', 'comment'], + AttachmentType::class => ['comment'], + Storelocation::class => ['comment'], + Device::class => ['comment'], + Category::class => ['comment'], + Manufacturer::class => ['comment'], + MeasurementUnit::class => ['comment'], + Supplier::class => ['comment'], + Currency::class => ['comment'], + Group::class => ['comment'], + ]; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $targets = $this->getTargetsLists(); + + //Convert for every class target + foreach ($targets as $class => $properties) { + $io->section(sprintf('Convert entities of class %s', $class)); + $io->note(sprintf( + 'Search for entities of type %s that need conversion', + $class + )); + //Determine which entities of this type we need to modify + /** @var EntityRepository $repo */ + $repo = $this->em->getRepository($class); + $qb = $repo->createQueryBuilder('e') + ->select('e'); + //Add fields criteria + foreach ($properties as $key => $property) { + $qb->orWhere('e.' . $property . ' LIKE ?' . $key); + $qb->setParameter($key, static::BBCODE_CRITERIA); + } + + //Fetch resulting classes + $results = $qb->getQuery()->getResult(); + $io->note(sprintf('Found %d entities, that need to be converted!', count($results))); + + //In verbose mode print the names of the entities + foreach ($results as $result) { + /** @var NamedDBElement $result */ + $io->writeln( + 'Convert entity: ' . $result->getName() . ' (' . $result->getIDString() . ')', + OutputInterface::VERBOSITY_VERBOSE + ); + foreach ($properties as $property) { + //Retrieve bbcode from entity + $bbcode = $this->propertyAccessor->getValue($result, $property); + //Check if the current property really contains BBCode + if (!preg_match(static::BBCODE_REGEX, $bbcode)) { + continue; + } + $io->writeln( + 'BBCode (old): ' + . str_replace('\n', ' ', substr($bbcode, 0, 255)), + OutputInterface::VERBOSITY_VERY_VERBOSE + ); + $markdown = $this->converter->convert($bbcode); + $io->writeln( + 'Markdown (new): ' + . str_replace('\n', ' ', substr($markdown, 0, 255)), + OutputInterface::VERBOSITY_VERY_VERBOSE + ); + $io->writeln('', OutputInterface::VERBOSITY_VERY_VERBOSE); + $this->propertyAccessor->setValue($result, $property, $markdown); + } + + } + } + + //If we are not in dry run, save changes to DB + if (!$input->getOption('dry-run')) { + $this->em->flush(); + $io->success('Changes saved to DB successfully!'); + } + } + + +} \ No newline at end of file diff --git a/src/Helpers/BBCodeToMarkdownConverter.php b/src/Helpers/BBCodeToMarkdownConverter.php new file mode 100644 index 00000000..0ff3b8ee --- /dev/null +++ b/src/Helpers/BBCodeToMarkdownConverter.php @@ -0,0 +1,63 @@ +html_to_markdown = new HtmlConverter(); + } + + /** + * Converts the given BBCode to markdown. + * BBCode tags that does not have a markdown aequivalent are outputed as HTML tags. + * @param $bbcode string The Markdown that should be converted. + * @return string The markdown version of the text. + */ + public function convert(string $bbcode) : string + { + //Convert BBCode to html + $xml = TextFormatter::parse($bbcode); + $html = TextFormatter::render($xml); + + //Now convert the HTML to markdown + return $this->html_to_markdown->convert($html); + } +} \ No newline at end of file diff --git a/src/Security/EntityListeners/ElementPermissionListener.php b/src/Security/EntityListeners/ElementPermissionListener.php index 248909b7..aba6a53e 100644 --- a/src/Security/EntityListeners/ElementPermissionListener.php +++ b/src/Security/EntityListeners/ElementPermissionListener.php @@ -42,11 +42,14 @@ use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\PostLoad; use Doctrine\ORM\Mapping\PreUpdate; use ReflectionClass; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Core\Security; /** * The purpose of this class is to hook into the doctrine entity lifecycle and restrict access to entity informations * configured by ColoumnSecurity Annotation. + * If the current programm is running from CLI (like a CLI command), the security checks are disabled. + * (Commands should be able to do everything they like) * * If a user does not have access to an coloumn, it will be filled, with a placeholder, after doctrine loading is finished. * The edit process is also catched, so that these placeholders, does not get saved to database. @@ -56,12 +59,29 @@ class ElementPermissionListener protected $security; protected $reader; protected $em; + protected $disabled; - public function __construct(Security $security, Reader $reader, EntityManagerInterface $em) + public function __construct(Security $security, Reader $reader, EntityManagerInterface $em, KernelInterface $kernel) { $this->security = $security; $this->reader = $reader; $this->em = $em; + //Disable security when the current program is running from CLI + $this->disabled = $this->isRunningFromCLI(); + } + + /** + * This function checks if the current script is run from web or from a terminal. + * @return bool Returns true if the current programm is running from CLI (terminal) + */ + protected function isRunningFromCLI() + { + + if (empty($_SERVER['REMOTE_ADDR']) && !isset($_SERVER['HTTP_USER_AGENT']) && count($_SERVER['argv']) > 0) { + return true; + } + + return false; } /** @@ -76,6 +96,11 @@ class ElementPermissionListener */ public function postLoadHandler(DBElement $element, LifecycleEventArgs $event) { + //Do nothing if security is disabled + if ($this->disabled) { + return; + } + //Read Annotations and properties. $reflectionClass = new ReflectionClass($element); $properties = $reflectionClass->getProperties(); @@ -112,6 +137,11 @@ class ElementPermissionListener */ public function preFlushHandler(DBElement $element, PreFlushEventArgs $eventArgs) { + //Do nothing if security is disabled + if ($this->disabled) { + return; + } + $em = $eventArgs->getEntityManager(); $unitOfWork = $eventArgs->getEntityManager()->getUnitOfWork(); diff --git a/src/Services/MarkdownParser.php b/src/Services/MarkdownParser.php index ccef5ede..6f751d9a 100644 --- a/src/Services/MarkdownParser.php +++ b/src/Services/MarkdownParser.php @@ -31,9 +31,7 @@ namespace App\Services; - -use Symfony\Contracts\Cache\CacheInterface; -use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * This class allows you to convert markdown text to HTML. @@ -41,52 +39,26 @@ use Symfony\Contracts\Cache\ItemInterface; */ class MarkdownParser { - protected $cache; - /** @var \Parsedown */ - protected $parsedown; + protected $translator; - public function __construct(CacheInterface $cache) + public function __construct(TranslatorInterface $translator) { - $this->cache = $cache; - $this->initParsedown(); - } - - protected function initParsedown() - { - $this->parsedown = new \Parsedown(); - $this->parsedown->setSafeMode(true); + $this->translator = $translator; } /** - * Converts the given markdown text to HTML. - * The result is cached. + * Mark the markdown for rendering. + * The rendering of markdown is done on client side * @param string $markdown The markdown text that should be parsed to html. * @param bool $inline_mode Only allow inline markdown codes like (*bold* or **italic**), not something like tables - * @return string The HTML version of the given text. - * @throws \Psr\Cache\InvalidArgumentException + * @return string The markdown in a version that can be parsed on client side. */ - public function parse(string $markdown, bool $inline_mode = false) : string + public function markForRendering(string $markdown, bool $inline_mode = false) : string { return sprintf( - '
Markdown loading...
', - htmlspecialchars($markdown) + '
%s
', + htmlspecialchars($markdown), + $this->translator->trans('markdown.loading') ); - - //Generate key - /*if ($inline_mode) { - $key = 'markdown_i_' . md5($markdown); - } else { - $key = 'markdown_' . md5($markdown); - } - return $this->cache->get($key, function (ItemInterface $item) use ($markdown, $inline_mode) { - //Expire text after 2 months - $item->expiresAfter(311040000); - - if ($inline_mode) { - return $this->parsedown->line($markdown); - } - - return '
' . $this->parsedown->text($markdown) . '
'; - });*/ } } \ No newline at end of file diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index d8e42dc1..729d2681 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -80,7 +80,7 @@ class AppExtension extends AbstractExtension { return [ new TwigFilter('entityURL', [$this, 'generateEntityURL']), - new TwigFilter('markdown', [$this->markdownParser, 'parse'], ['pre_escape' => 'html', 'is_safe' => ['html']]), + new TwigFilter('markdown', [$this->markdownParser, 'markForRendering'], ['pre_escape' => 'html', 'is_safe' => ['html']]), new TwigFilter('moneyFormat', [$this, 'formatCurrency']), new TwigFilter('siFormat', [$this, 'siFormat']), new TwigFilter('amountFormat', [$this, 'amountFormat']), diff --git a/symfony.lock b/symfony.lock index 2f264316..b4d8c321 100644 --- a/symfony.lock +++ b/symfony.lock @@ -105,9 +105,6 @@ "egulias/email-validator": { "version": "2.1.7" }, - "erusev/parsedown": { - "version": "1.7.3" - }, "felixfbecker/advanced-json-rpc": { "version": "v3.0.4" }, @@ -150,6 +147,9 @@ "jdorn/sql-formatter": { "version": "v1.2.17" }, + "league/html-to-markdown": { + "version": "4.8.2" + }, "liip/imagine-bundle": { "version": "1.8", "recipe": { @@ -269,7 +269,7 @@ "version": "1.4.3" }, "s9e/text-formatter": { - "version": "1.4.1" + "version": "2.1.2" }, "sebastian/diff": { "version": "3.0.2" diff --git a/tests/Helpers/BBCodeToMarkdownConverterTest.php b/tests/Helpers/BBCodeToMarkdownConverterTest.php new file mode 100644 index 00000000..4f0d2f65 --- /dev/null +++ b/tests/Helpers/BBCodeToMarkdownConverterTest.php @@ -0,0 +1,70 @@ +converter = new BBCodeToMarkdownConverter(); + } + + public function dataProvider() + { + return [ + ['[b]Bold[/b]', '**Bold**'], + ['[i]Italic[/i]', '*Italic*'], + ['[s]Strike[/s]', 'Strike'], + ['[url]https://foo.bar[/url]', ''], + ['[url=https://foo.bar]test[/url]', '[test](https://foo.bar)'], + ['[center]Centered[/center]', '
Centered
'], + ['test no change', 'test no change'], + ['**Test**', '**Test**'], + ]; + } + + /** + * @dataProvider dataProvider + * @param $bbcode + * @param $expected + */ + public function testConvert($bbcode, $expected) + { + $this->assertEquals($expected, $this->converter->convert($bbcode)); + } +} \ No newline at end of file