feature/3-git-content-database #8
@ -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
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')]
|
#[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')]
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
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
|
class SnipServiceFactory
|
||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly string $snipBasePath,
|
private readonly string $snipBasePath,
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user