feature/3-git-content-database #8
| @@ -4,7 +4,7 @@ | ||||
|     "minimum-stability": "stable", | ||||
|     "prefer-stable": true, | ||||
|     "require": { | ||||
|         "php": ">=8.1", | ||||
|         "php": ">=8.2", | ||||
|         "ext-ctype": "*", | ||||
|         "ext-iconv": "*", | ||||
|         "czproject/git-php": "^4.1", | ||||
| @@ -12,26 +12,26 @@ | ||||
|         "doctrine/doctrine-migrations-bundle": "^3.2", | ||||
|         "doctrine/orm": "^2.14", | ||||
|         "league/pipeline": "^1.0", | ||||
|         "symfony/console": "6.2.*", | ||||
|         "symfony/dotenv": "6.2.*", | ||||
|         "symfony/console": "7.0.*", | ||||
|         "symfony/dotenv": "7.0.*", | ||||
|         "symfony/flex": "^2", | ||||
|         "symfony/form": "6.2.*", | ||||
|         "symfony/framework-bundle": "6.2.*", | ||||
|         "symfony/form": "7.0.*", | ||||
|         "symfony/framework-bundle": "7.0.*", | ||||
|         "symfony/monolog-bundle": "^3.0", | ||||
|         "symfony/runtime": "6.2.*", | ||||
|         "symfony/security-bundle": "6.2.*", | ||||
|         "symfony/twig-bundle": "6.2.*", | ||||
|         "symfony/validator": "6.2.*", | ||||
|         "symfony/yaml": "6.2.*", | ||||
|         "symfony/runtime": "7.0.*", | ||||
|         "symfony/security-bundle": "7.0.*", | ||||
|         "symfony/twig-bundle": "7.0.*", | ||||
|         "symfony/validator": "7.0.*", | ||||
|         "symfony/yaml": "7.0.*", | ||||
|         "twig/extra-bundle": "^2.12|^3.0", | ||||
|         "twig/twig": "^2.12|^3.0" | ||||
|     }, | ||||
|     "require-dev": { | ||||
|         "deployer/deployer": "^7.3", | ||||
|         "symfony/debug-bundle": "6.2.*", | ||||
|         "symfony/debug-bundle": "7.0.*", | ||||
|         "symfony/maker-bundle": "^1.48", | ||||
|         "symfony/stopwatch": "6.2.*", | ||||
|         "symfony/web-profiler-bundle": "6.2.*" | ||||
|         "symfony/stopwatch": "7.0.*", | ||||
|         "symfony/web-profiler-bundle": "7.0.*" | ||||
|     }, | ||||
|     "config": { | ||||
|         "allow-plugins": { | ||||
| @@ -78,7 +78,7 @@ | ||||
|     "extra": { | ||||
|         "symfony": { | ||||
|             "allow-contrib": false, | ||||
|             "require": "6.2.*" | ||||
|             "require": "7.0.*" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										2472
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2472
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -44,25 +44,25 @@ class SnipController extends AbstractController | ||||
|     } | ||||
|  | ||||
|     #[Route('/single/{snip}', name: '_single')] | ||||
|     public function single(Snip $snip): Response | ||||
|     public function single(Snip $snip, Pipeline $pl): Response | ||||
|     { | ||||
|         $this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip); | ||||
|  | ||||
|         $snipService = $this->snipServiceFactory->createGit($snip); | ||||
|         return $this->render('snip/single.html.twig', [ | ||||
|             'snip' => $snip, | ||||
|             'content' => (new Pipeline())->parse($snipService->get()), | ||||
|             'content' => $pl->parse($snipService->get()), | ||||
|             'branch' => $snipService->getCommit(), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     #[Route('/raw/{snip}', name: '_raw')] | ||||
|     public function raw(Snip $snip, Request $request): Response | ||||
|     public function raw(Snip $snip, Pipeline $pl, Request $request): Response | ||||
|     { | ||||
|         $this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip); | ||||
|  | ||||
|         $response = new Response( | ||||
|             (new Pipeline())->clean($this->snipServiceFactory->createGit($snip)->get()), | ||||
|             $pl->clean($this->snipServiceFactory->createGit($snip)->get()), | ||||
|             Response::HTTP_OK, | ||||
|             ['Content-Type' => 'text/html'] | ||||
|         ); | ||||
| @@ -77,10 +77,6 @@ class SnipController extends AbstractController | ||||
|         } | ||||
|  | ||||
|         return $response; | ||||
| //        return $this->render('snip/single.html.twig', [ | ||||
| //            'snip' => $snip, | ||||
| //            'content' => $this->snipServiceFactory->create($snip)->get(), | ||||
| //        ]); | ||||
|     } | ||||
|  | ||||
|     #[Route('/edit/{snip}', name: '_edit')] | ||||
|   | ||||
| @@ -103,7 +103,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface | ||||
|     /** | ||||
|      * @see UserInterface | ||||
|      */ | ||||
|     public function eraseCredentials() | ||||
|     public function eraseCredentials(): void | ||||
|     { | ||||
|         // If you store any temporary, sensitive data on the user, clear it here | ||||
|         // $this->plainPassword = null; | ||||
| @@ -132,4 +132,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface | ||||
|  | ||||
|         return $this; | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -6,8 +6,8 @@ use CzProject\GitPhp\Git; | ||||
|  | ||||
| class CustomGit extends Git | ||||
| { | ||||
|     public function open($directory) | ||||
|     public function open($directory): CustomGitRepository | ||||
|     { | ||||
|         return new CustomGitRepository($directory, $this->runner); | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -3,19 +3,31 @@ | ||||
| namespace App\Service\SnipParser; | ||||
|  | ||||
| use App\Service\SnipParser\Stages\HtmlEscapeStage; | ||||
| use App\Service\SnipParser\Stages\IncludeReferenceStage; | ||||
| use App\Service\SnipParser\Stages\UrlReferenceStage; | ||||
| use App\Service\SnipParser\Stages\ReplaceBlocksStage; | ||||
| use App\Service\SnipParser\Stages\ReplaceStage; | ||||
| use League\Pipeline\PipelineBuilder; | ||||
|  | ||||
| class Pipeline | ||||
| { | ||||
|     public function __construct( | ||||
|         private readonly UrlReferenceStage     $referenceStage, | ||||
|         private readonly IncludeReferenceStage $includeStage, | ||||
|     ) {} | ||||
|  | ||||
|     public function parse(string $payload): string | ||||
|     { | ||||
|         $builder = new PipelineBuilder(); | ||||
|         $pipeline = $builder | ||||
|             ->add(new HtmlEscapeStage()) | ||||
|             ->add(new ReplaceStage(PHP_EOL, '<br>')) | ||||
|             ->add(new ReplaceBlocksStage('<pre><code class="hljs">', '</code></pre>', '```')) | ||||
|             ->add(new ReplaceBlocksStage('<code class="hljs">', '</code>', '``')) | ||||
|             ->build(); | ||||
|             ->add($this->referenceStage) | ||||
|             ->add($this->includeStage) | ||||
|             ->build() | ||||
|         ; | ||||
|  | ||||
|         return $pipeline->process($payload); | ||||
|     } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ class HtmlEscapeStage implements StageInterface | ||||
| { | ||||
|     public function __invoke(mixed $payload): string | ||||
|     { | ||||
|         return htmlspecialchars($payload, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); | ||||
|         // https://www.php.net/manual/en/function.htmlspecialchars.php | ||||
|         return htmlspecialchars($payload, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								src/Service/SnipParser/Stages/IncludeReferenceStage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/Service/SnipParser/Stages/IncludeReferenceStage.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <?php | ||||
|  | ||||
| namespace App\Service\SnipParser\Stages; | ||||
|  | ||||
| use App\Repository\SnipRepository; | ||||
| use App\Security\Voter\SnipVoter; | ||||
| use App\Service\SnipParser\Pipeline; | ||||
| use App\Service\SnipServiceFactory; | ||||
| use League\Pipeline\StageInterface; | ||||
| use Symfony\Bundle\SecurityBundle\Security; | ||||
| use Symfony\Component\DependencyInjection\Attribute\Autowire; | ||||
|  | ||||
| class IncludeReferenceStage implements StageInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         #[Autowire(lazy: true)] private readonly Security           $security, | ||||
|         #[Autowire(lazy: true)] private readonly SnipRepository     $snipRepository, | ||||
|         #[Autowire(lazy: true)] private readonly SnipServiceFactory $snipServiceFactory, | ||||
|         #[Autowire(lazy: true)] private readonly Pipeline           $pipeline, | ||||
|     ) {} | ||||
|  | ||||
|     public function __invoke(mixed $payload): string | ||||
|     { | ||||
|         return $this->replaceReferences($payload); | ||||
|     } | ||||
|  | ||||
|     private function replaceReferences(mixed $payload): string | ||||
|     { | ||||
|         // replaces all references (#n) to other snips with links | ||||
|         $pattern = '/\{\{(\d+)\}\}/'; | ||||
|  | ||||
|         return preg_replace_callback($pattern, function ($matches) { | ||||
|             $snip = $this->snipRepository->find($matches[1]); | ||||
|             if ($snip === null) { | ||||
|                 return sprintf('<span title="not found">%s</span>', $matches[0]); | ||||
|             } | ||||
|             if (!$this->security->isGranted(SnipVoter::VIEW, $snip)) { | ||||
|                 return sprintf('<span title="access denied">%s</span>', $matches[0]); | ||||
|             } | ||||
|  | ||||
|             return $this->pipeline->parse($this->snipServiceFactory->create($snip)->get()); | ||||
|         }, $payload); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/Service/SnipParser/Stages/ReplaceStage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/Service/SnipParser/Stages/ReplaceStage.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| <?php | ||||
|  | ||||
| namespace App\Service\SnipParser\Stages; | ||||
|  | ||||
| use League\Pipeline\StageInterface; | ||||
|  | ||||
| class ReplaceStage implements StageInterface | ||||
| { | ||||
|     // replaces a string with another string | ||||
|     public function __construct( | ||||
|         public readonly string $search, | ||||
|         public readonly string $replace, | ||||
|     ) {} | ||||
|  | ||||
|     public function __invoke(mixed $payload): string | ||||
|     { | ||||
|         return str_replace($this->search, $this->replace, $payload); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										43
									
								
								src/Service/SnipParser/Stages/UrlReferenceStage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/Service/SnipParser/Stages/UrlReferenceStage.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| <?php | ||||
|  | ||||
| namespace App\Service\SnipParser\Stages; | ||||
|  | ||||
| use App\Repository\SnipRepository; | ||||
| use App\Security\Voter\SnipVoter; | ||||
| use League\Pipeline\StageInterface; | ||||
| use Symfony\Bundle\SecurityBundle\Security; | ||||
| use Symfony\Component\DependencyInjection\Attribute\Autowire; | ||||
| use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | ||||
|  | ||||
| class UrlReferenceStage implements StageInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         #[Autowire(lazy: true)] private readonly UrlGeneratorInterface $router, | ||||
|         #[Autowire(lazy: true)] private readonly Security              $security, | ||||
|         #[Autowire(lazy: true)] private readonly SnipRepository        $snipRepository, | ||||
|     ) {} | ||||
|  | ||||
|     public function __invoke(mixed $payload): string | ||||
|     { | ||||
|         return $this->replaceReferences($payload); | ||||
|     } | ||||
|  | ||||
|     private function replaceReferences(mixed $payload): string | ||||
|     { | ||||
|         // replaces all references (#n) to other snips with links | ||||
|         $pattern = '/#(\d+)/'; | ||||
|  | ||||
|         return preg_replace_callback($pattern, function ($matches) { | ||||
|             $snip = $this->snipRepository->find($matches[1]); | ||||
|             if ($snip === null) { | ||||
|                 return sprintf('<span title="not found">%s</span>', $matches[0]); | ||||
|             } | ||||
|             if (!$this->security->isGranted(SnipVoter::VIEW, $snip)) { | ||||
|                 return sprintf('<span title="access denied">%s</span>', $matches[0]); | ||||
|             } | ||||
|  | ||||
|             $url = $this->router->generate('snip_single', ['snip' => $snip->getId()]); | ||||
|             return sprintf('<a href="%s" title="Owner: %s">#%s</a>', $url, $snip->getCreatedBy(), $snip->getId()); | ||||
|         }, $payload); | ||||
|     } | ||||
| } | ||||
| @@ -11,7 +11,6 @@ use Symfony\Bundle\SecurityBundle\Security; | ||||
|  | ||||
| class SnipServiceFactory | ||||
| { | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly string                $snipBasePath, | ||||
|         private readonly Security              $security, | ||||
|   | ||||
| @@ -24,7 +24,7 @@ | ||||
|     <div class="card" style="width: 100%;"> | ||||
|         <h4 class="card-header"> | ||||
|             {{ include('snip/badge.html.twig', {snip: snip}) }} | ||||
|             {{ snip }} | ||||
|             {{ snip }} <small class="text-muted">#{{ snip.id }}</small> | ||||
|         </h4> | ||||
|         <div class="card-header"> | ||||
|             <p class="card-text">Current branch: {{ branch }}</p> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user