feature/3-git-content-database #8

Merged
ardent merged 5 commits from feature/3-git-content-database into main 2023-12-17 22:03:00 +01:00
12 changed files with 1461 additions and 1185 deletions
Showing only changes of commit a9e740d018 - Show all commits

View File

@ -4,7 +4,7 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true, "prefer-stable": true,
"require": { "require": {
"php": ">=8.1", "php": ">=8.2",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"czproject/git-php": "^4.1", "czproject/git-php": "^4.1",
@ -12,26 +12,26 @@
"doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.14", "doctrine/orm": "^2.14",
"league/pipeline": "^1.0", "league/pipeline": "^1.0",
"symfony/console": "6.2.*", "symfony/console": "7.0.*",
"symfony/dotenv": "6.2.*", "symfony/dotenv": "7.0.*",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/form": "6.2.*", "symfony/form": "7.0.*",
"symfony/framework-bundle": "6.2.*", "symfony/framework-bundle": "7.0.*",
"symfony/monolog-bundle": "^3.0", "symfony/monolog-bundle": "^3.0",
"symfony/runtime": "6.2.*", "symfony/runtime": "7.0.*",
"symfony/security-bundle": "6.2.*", "symfony/security-bundle": "7.0.*",
"symfony/twig-bundle": "6.2.*", "symfony/twig-bundle": "7.0.*",
"symfony/validator": "6.2.*", "symfony/validator": "7.0.*",
"symfony/yaml": "6.2.*", "symfony/yaml": "7.0.*",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0" "twig/twig": "^2.12|^3.0"
}, },
"require-dev": { "require-dev": {
"deployer/deployer": "^7.3", "deployer/deployer": "^7.3",
"symfony/debug-bundle": "6.2.*", "symfony/debug-bundle": "7.0.*",
"symfony/maker-bundle": "^1.48", "symfony/maker-bundle": "^1.48",
"symfony/stopwatch": "6.2.*", "symfony/stopwatch": "7.0.*",
"symfony/web-profiler-bundle": "6.2.*" "symfony/web-profiler-bundle": "7.0.*"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
@ -78,7 +78,7 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "6.2.*" "require": "7.0.*"
} }
} }
} }

2472
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -44,25 +44,25 @@ class SnipController extends AbstractController
} }
#[Route('/single/{snip}', name: '_single')] #[Route('/single/{snip}', name: '_single')]
public function single(Snip $snip): Response public function single(Snip $snip, Pipeline $pl): Response
{ {
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip); $this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
$snipService = $this->snipServiceFactory->createGit($snip); $snipService = $this->snipServiceFactory->createGit($snip);
return $this->render('snip/single.html.twig', [ return $this->render('snip/single.html.twig', [
'snip' => $snip, 'snip' => $snip,
'content' => (new Pipeline())->parse($snipService->get()), 'content' => $pl->parse($snipService->get()),
'branch' => $snipService->getCommit(), 'branch' => $snipService->getCommit(),
]); ]);
} }
#[Route('/raw/{snip}', name: '_raw')] #[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); $this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
$response = new Response( $response = new Response(
(new Pipeline())->clean($this->snipServiceFactory->createGit($snip)->get()), $pl->clean($this->snipServiceFactory->createGit($snip)->get()),
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/html'] ['Content-Type' => 'text/html']
); );
@ -77,10 +77,6 @@ class SnipController extends AbstractController
} }
return $response; return $response;
// return $this->render('snip/single.html.twig', [
// 'snip' => $snip,
// 'content' => $this->snipServiceFactory->create($snip)->get(),
// ]);
} }
#[Route('/edit/{snip}', name: '_edit')] #[Route('/edit/{snip}', name: '_edit')]

View File

@ -103,7 +103,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
/** /**
* @see UserInterface * @see UserInterface
*/ */
public function eraseCredentials() public function eraseCredentials(): void
{ {
// If you store any temporary, sensitive data on the user, clear it here // If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null; // $this->plainPassword = null;

View File

@ -6,7 +6,7 @@ use CzProject\GitPhp\Git;
class CustomGit extends Git class CustomGit extends Git
{ {
public function open($directory) public function open($directory): CustomGitRepository
{ {
return new CustomGitRepository($directory, $this->runner); return new CustomGitRepository($directory, $this->runner);
} }

View File

@ -3,19 +3,31 @@
namespace App\Service\SnipParser; namespace App\Service\SnipParser;
use App\Service\SnipParser\Stages\HtmlEscapeStage; 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\ReplaceBlocksStage;
use App\Service\SnipParser\Stages\ReplaceStage;
use League\Pipeline\PipelineBuilder; use League\Pipeline\PipelineBuilder;
class Pipeline class Pipeline
{ {
public function __construct(
private readonly UrlReferenceStage $referenceStage,
private readonly IncludeReferenceStage $includeStage,
) {}
public function parse(string $payload): string public function parse(string $payload): string
{ {
$builder = new PipelineBuilder(); $builder = new PipelineBuilder();
$pipeline = $builder $pipeline = $builder
->add(new HtmlEscapeStage()) ->add(new HtmlEscapeStage())
->add(new ReplaceStage(PHP_EOL, '<br>'))
->add(new ReplaceBlocksStage('<pre><code class="hljs">', '</code></pre>', '```')) ->add(new ReplaceBlocksStage('<pre><code class="hljs">', '</code></pre>', '```'))
->add(new ReplaceBlocksStage('<code class="hljs">', '</code>', '``')) ->add(new ReplaceBlocksStage('<code class="hljs">', '</code>', '``'))
->build(); ->add($this->referenceStage)
->add($this->includeStage)
->build()
;
return $pipeline->process($payload); return $pipeline->process($payload);
} }

View File

@ -8,6 +8,7 @@ class HtmlEscapeStage implements StageInterface
{ {
public function __invoke(mixed $payload): string 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');
} }
} }

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -11,7 +11,6 @@ use Symfony\Bundle\SecurityBundle\Security;
class SnipServiceFactory class SnipServiceFactory
{ {
public function __construct( public function __construct(
private readonly string $snipBasePath, private readonly string $snipBasePath,
private readonly Security $security, private readonly Security $security,

View File

@ -24,7 +24,7 @@
<div class="card" style="width: 100%;"> <div class="card" style="width: 100%;">
<h4 class="card-header"> <h4 class="card-header">
{{ include('snip/badge.html.twig', {snip: snip}) }} {{ include('snip/badge.html.twig', {snip: snip}) }}
{{ snip }} {{ snip }} <small class="text-muted">#{{ snip.id }}</small>
</h4> </h4>
<div class="card-header"> <div class="card-header">
<p class="card-text">Current branch: {{ branch }}</p> <p class="card-text">Current branch: {{ branch }}</p>